mirror of
https://github.com/ossf/scorecard.git
synced 2024-08-15 19:30:40 +03:00
🌱 Convert SAST check to probes (#3571)
* Convert SAST checks to probes Signed-off-by: AdamKorcz <adam@adalogics.com> * Update checks/evaluation/sast.go Co-authored-by: Raghav Kaul <8695110+raghavkaul@users.noreply.github.com> Signed-off-by: AdamKorcz <44787359+AdamKorcz@users.noreply.github.com> * preserve file info when logging positive Sonar findings Signed-off-by: AdamKorcz <adam@adalogics.com> * rebase Signed-off-by: AdamKorcz <adam@adalogics.com> * Remove warning logging Signed-off-by: AdamKorcz <adam@adalogics.com> * add outcome and message to finding on the same line Signed-off-by: AdamKorcz <adam@adalogics.com> * codeql workflow -> codeql action Signed-off-by: AdamKorcz <adam@adalogics.com> * 'the Sonar' -> 'Sonar' in probe def.yml Signed-off-by: AdamKorcz <adam@adalogics.com> * fix typo Signed-off-by: AdamKorcz <adam@adalogics.com> * Change how probe creates location Signed-off-by: AdamKorcz <adam@adalogics.com> * Change names of values Signed-off-by: AdamKorcz <adam@adalogics.com> * change 'SAST tool detected: xx' to 'SAST tool installed: xx' Signed-off-by: AdamKorcz <adam@adalogics.com> * make text in probe def.yml easier to read Signed-off-by: AdamKorcz <adam@adalogics.com> * Change 'to' to 'two' Signed-off-by: AdamKorcz <adam@adalogics.com> * Minor change Signed-off-by: AdamKorcz <adam@adalogics.com> --------- Signed-off-by: AdamKorcz <adam@adalogics.com> Signed-off-by: AdamKorcz <44787359+AdamKorcz@users.noreply.github.com> Co-authored-by: Raghav Kaul <8695110+raghavkaul@users.noreply.github.com>
This commit is contained in:
parent
f422f692fe
commit
47e04c102a
@ -39,6 +39,7 @@ type RawResults struct {
|
||||
Metadata MetadataData
|
||||
PackagingResults PackagingData
|
||||
PinningDependenciesResults PinningDependenciesData
|
||||
SASTResults SASTData
|
||||
SecurityPolicyResults SecurityPolicyData
|
||||
SignedReleasesResults SignedReleasesData
|
||||
TokenPermissionsResults TokenPermissionsData
|
||||
@ -226,6 +227,40 @@ type SecurityPolicyFile struct {
|
||||
File File
|
||||
}
|
||||
|
||||
// SASTData contains the raw results
|
||||
// for the SAST check.
|
||||
type SASTData struct {
|
||||
Workflows []SASTWorkflow
|
||||
Commits []SASTCommit
|
||||
NumWorkflows int
|
||||
}
|
||||
|
||||
type SASTCommit struct {
|
||||
CommittedDate time.Time
|
||||
Message string
|
||||
SHA string
|
||||
CheckRuns []clients.CheckRun
|
||||
AssociatedMergeRequest clients.PullRequest
|
||||
Committer clients.User
|
||||
Compliant bool
|
||||
}
|
||||
|
||||
// SASTWorkflowType represents a type of SAST workflow.
|
||||
type SASTWorkflowType string
|
||||
|
||||
const (
|
||||
// CodeQLWorkflow represents a workflow that runs CodeQL.
|
||||
CodeQLWorkflow SASTWorkflowType = "CodeQL"
|
||||
// SonarWorkflow represents a workflow that runs Sonar.
|
||||
SonarWorkflow SASTWorkflowType = "Sonar"
|
||||
)
|
||||
|
||||
// SASTWorkflow represents a SAST workflow.
|
||||
type SASTWorkflow struct {
|
||||
Type SASTWorkflowType
|
||||
File File
|
||||
}
|
||||
|
||||
// SecurityPolicyData contains the raw results
|
||||
// for the Security-Policy check.
|
||||
type SecurityPolicyData struct {
|
||||
|
167
checks/evaluation/sast.go
Normal file
167
checks/evaluation/sast.go
Normal file
@ -0,0 +1,167 @@
|
||||
// Copyright 2023 OpenSSF Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package evaluation
|
||||
|
||||
import (
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
sce "github.com/ossf/scorecard/v4/errors"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
"github.com/ossf/scorecard/v4/probes/sastToolCodeQLInstalled"
|
||||
"github.com/ossf/scorecard/v4/probes/sastToolRunsOnAllCommits"
|
||||
"github.com/ossf/scorecard/v4/probes/sastToolSonarInstalled"
|
||||
)
|
||||
|
||||
// SAST applies the score policy for the SAST check.
|
||||
func SAST(name string,
|
||||
findings []finding.Finding, dl checker.DetailLogger,
|
||||
) checker.CheckResult {
|
||||
// We have 3 unique probes, each should have a finding.
|
||||
expectedProbes := []string{
|
||||
sastToolCodeQLInstalled.Probe,
|
||||
sastToolRunsOnAllCommits.Probe,
|
||||
sastToolSonarInstalled.Probe,
|
||||
}
|
||||
|
||||
if !finding.UniqueProbesEqual(findings, expectedProbes) {
|
||||
e := sce.WithMessage(sce.ErrScorecardInternal, "invalid probe results")
|
||||
return checker.CreateRuntimeErrorResult(name, e)
|
||||
}
|
||||
|
||||
var sastScore, codeQlScore, sonarScore int
|
||||
// Assign sastScore, codeQlScore and sonarScore
|
||||
for i := range findings {
|
||||
f := &findings[i]
|
||||
switch f.Probe {
|
||||
case sastToolRunsOnAllCommits.Probe:
|
||||
sastScore = getSASTScore(f, dl)
|
||||
case sastToolCodeQLInstalled.Probe:
|
||||
codeQlScore = getCodeQLScore(f, dl)
|
||||
case sastToolSonarInstalled.Probe:
|
||||
if f.Outcome == finding.OutcomePositive {
|
||||
sonarScore = checker.MaxResultScore
|
||||
dl.Info(&checker.LogMessage{
|
||||
Text: f.Message,
|
||||
Type: f.Location.Type,
|
||||
Path: f.Location.Path,
|
||||
Offset: *f.Location.LineStart,
|
||||
EndOffset: *f.Location.LineEnd,
|
||||
Snippet: *f.Location.Snippet,
|
||||
})
|
||||
} else if f.Outcome == finding.OutcomeNegative {
|
||||
sonarScore = checker.MinResultScore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if sonarScore == checker.MaxResultScore {
|
||||
return checker.CreateMaxScoreResult(name, "SAST tool detected")
|
||||
}
|
||||
|
||||
if sastScore == checker.InconclusiveResultScore &&
|
||||
codeQlScore == checker.InconclusiveResultScore {
|
||||
// That can never happen since sastToolInCheckRuns can never
|
||||
// retun checker.InconclusiveResultScore.
|
||||
return checker.CreateRuntimeErrorResult(name, sce.ErrScorecardInternal)
|
||||
}
|
||||
|
||||
// Both scores are conclusive.
|
||||
// We assume the CodeQl config uses a cron and is not enabled as pre-submit.
|
||||
// TODO: verify the above comment in code.
|
||||
// We encourage developers to have sast check run on every pre-submit rather
|
||||
// than as cron jobs through the score computation below.
|
||||
// Warning: there is a hidden assumption that *any* sast tool is equally good.
|
||||
if sastScore != checker.InconclusiveResultScore &&
|
||||
codeQlScore != checker.InconclusiveResultScore {
|
||||
switch {
|
||||
case sastScore == checker.MaxResultScore:
|
||||
return checker.CreateMaxScoreResult(name, "SAST tool is run on all commits")
|
||||
case codeQlScore == checker.MinResultScore:
|
||||
return checker.CreateResultWithScore(name,
|
||||
checker.NormalizeReason("SAST tool is not run on all commits", sastScore), sastScore)
|
||||
|
||||
// codeQl is enabled and sast has 0+ (but not all) PRs checks.
|
||||
case codeQlScore == checker.MaxResultScore:
|
||||
const sastWeight = 3
|
||||
const codeQlWeight = 7
|
||||
score := checker.AggregateScoresWithWeight(map[int]int{sastScore: sastWeight, codeQlScore: codeQlWeight})
|
||||
return checker.CreateResultWithScore(name, "SAST tool detected but not run on all commits", score)
|
||||
default:
|
||||
return checker.CreateRuntimeErrorResult(name, sce.WithMessage(sce.ErrScorecardInternal, "contact team"))
|
||||
}
|
||||
}
|
||||
|
||||
// Sast inconclusive.
|
||||
if codeQlScore != checker.InconclusiveResultScore {
|
||||
if codeQlScore == checker.MaxResultScore {
|
||||
return checker.CreateMaxScoreResult(name, "SAST tool detected: CodeQL")
|
||||
}
|
||||
return checker.CreateMinScoreResult(name, "no SAST tool detected")
|
||||
}
|
||||
|
||||
// CodeQl inconclusive.
|
||||
if sastScore != checker.InconclusiveResultScore {
|
||||
if sastScore == checker.MaxResultScore {
|
||||
return checker.CreateMaxScoreResult(name, "SAST tool is run on all commits")
|
||||
}
|
||||
|
||||
return checker.CreateResultWithScore(name,
|
||||
checker.NormalizeReason("SAST tool is not run on all commits", sastScore), sastScore)
|
||||
}
|
||||
|
||||
// Should never happen.
|
||||
return checker.CreateRuntimeErrorResult(name, sce.WithMessage(sce.ErrScorecardInternal, "contact team"))
|
||||
}
|
||||
|
||||
// getSASTScore returns the proportional score of how many commits
|
||||
// run SAST tools.
|
||||
func getSASTScore(f *finding.Finding, dl checker.DetailLogger) int {
|
||||
switch f.Outcome {
|
||||
case finding.OutcomeNotApplicable:
|
||||
dl.Warn(&checker.LogMessage{
|
||||
Text: f.Message,
|
||||
})
|
||||
return checker.InconclusiveResultScore
|
||||
case finding.OutcomePositive:
|
||||
dl.Info(&checker.LogMessage{
|
||||
Text: f.Message,
|
||||
})
|
||||
case finding.OutcomeNegative:
|
||||
dl.Warn(&checker.LogMessage{
|
||||
Text: f.Message,
|
||||
})
|
||||
default:
|
||||
checker.CreateProportionalScore(f.Values["totalPullRequestsAnalyzed"], f.Values["totalPullRequestsMerged"])
|
||||
}
|
||||
return checker.CreateProportionalScore(f.Values["totalPullRequestsAnalyzed"], f.Values["totalPullRequestsMerged"])
|
||||
}
|
||||
|
||||
// getCodeQLScore returns positive the project runs CodeQL and negative
|
||||
// if it doesn't.
|
||||
func getCodeQLScore(f *finding.Finding, dl checker.DetailLogger) int {
|
||||
switch f.Outcome {
|
||||
case finding.OutcomePositive:
|
||||
dl.Info(&checker.LogMessage{
|
||||
Text: f.Message,
|
||||
})
|
||||
return checker.MaxResultScore
|
||||
case finding.OutcomeNegative:
|
||||
dl.Warn(&checker.LogMessage{
|
||||
Text: f.Message,
|
||||
})
|
||||
return checker.MinResultScore
|
||||
default:
|
||||
panic("Should not happen")
|
||||
}
|
||||
}
|
153
checks/evaluation/sast_test.go
Normal file
153
checks/evaluation/sast_test.go
Normal file
@ -0,0 +1,153 @@
|
||||
// Copyright 2023 OpenSSF Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package evaluation
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
sce "github.com/ossf/scorecard/v4/errors"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
scut "github.com/ossf/scorecard/v4/utests"
|
||||
)
|
||||
|
||||
func TestSAST(t *testing.T) {
|
||||
snippet := "some code snippet"
|
||||
sline := uint(10)
|
||||
eline := uint(46)
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
findings []finding.Finding
|
||||
result scut.TestReturn
|
||||
}{
|
||||
{
|
||||
name: "SAST - Missing a probe",
|
||||
findings: []finding.Finding{
|
||||
{
|
||||
Probe: "sastToolCodeQLInstalled",
|
||||
Outcome: finding.OutcomePositive,
|
||||
},
|
||||
{
|
||||
Probe: "sastToolRunsOnAllCommits",
|
||||
Outcome: finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
result: scut.TestReturn{
|
||||
Score: checker.InconclusiveResultScore,
|
||||
Error: sce.ErrScorecardInternal,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Sonar and codeQL is installed",
|
||||
findings: []finding.Finding{
|
||||
{
|
||||
Probe: "sastToolCodeQLInstalled",
|
||||
Outcome: finding.OutcomePositive,
|
||||
},
|
||||
{
|
||||
Probe: "sastToolRunsOnAllCommits",
|
||||
Outcome: finding.OutcomePositive,
|
||||
Values: map[string]int{
|
||||
"totalPullRequestsAnalyzed": 1,
|
||||
"totalPullRequestsMerged": 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
Probe: "sastToolSonarInstalled",
|
||||
Outcome: finding.OutcomePositive,
|
||||
Location: &finding.Location{
|
||||
Type: finding.FileTypeSource,
|
||||
Path: "path/to/file.txt",
|
||||
LineStart: &sline,
|
||||
LineEnd: &eline,
|
||||
Snippet: &snippet,
|
||||
},
|
||||
},
|
||||
},
|
||||
result: scut.TestReturn{
|
||||
Score: 10,
|
||||
NumberOfInfo: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: `Sonar is installed. CodeQL is not installed.
|
||||
Does not have info about whether SAST runs
|
||||
on every commit.`,
|
||||
findings: []finding.Finding{
|
||||
{
|
||||
Probe: "sastToolCodeQLInstalled",
|
||||
Outcome: finding.OutcomeNegative,
|
||||
},
|
||||
{
|
||||
Probe: "sastToolRunsOnAllCommits",
|
||||
Outcome: finding.OutcomeNotApplicable,
|
||||
},
|
||||
{
|
||||
Probe: "sastToolSonarInstalled",
|
||||
Outcome: finding.OutcomePositive,
|
||||
Location: &finding.Location{
|
||||
Type: finding.FileTypeSource,
|
||||
Path: "path/to/file.txt",
|
||||
LineStart: &sline,
|
||||
LineEnd: &eline,
|
||||
Snippet: &snippet,
|
||||
},
|
||||
},
|
||||
},
|
||||
result: scut.TestReturn{
|
||||
Score: 10,
|
||||
NumberOfInfo: 1,
|
||||
NumberOfWarn: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Sonar and CodeQL are not installed",
|
||||
findings: []finding.Finding{
|
||||
{
|
||||
Probe: "sastToolCodeQLInstalled",
|
||||
Outcome: finding.OutcomeNegative,
|
||||
},
|
||||
{
|
||||
Probe: "sastToolRunsOnAllCommits",
|
||||
Outcome: finding.OutcomeNegative,
|
||||
Values: map[string]int{
|
||||
"totalPullRequestsAnalyzed": 1,
|
||||
"totalPullRequestsMerged": 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
Probe: "sastToolSonarInstalled",
|
||||
Outcome: finding.OutcomeNegative,
|
||||
},
|
||||
},
|
||||
result: scut.TestReturn{
|
||||
Score: 3,
|
||||
NumberOfWarn: 2,
|
||||
NumberOfInfo: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dl := scut.TestDetailLogger{}
|
||||
got := SAST(tt.name, tt.findings, &dl)
|
||||
if !scut.ValidateTestReturn(t, tt.name, &tt.result, &got, &dl) {
|
||||
t.Errorf("got %v, expected %v", got, tt.result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
295
checks/raw/sast.go
Normal file
295
checks/raw/sast.go
Normal file
@ -0,0 +1,295 @@
|
||||
// Copyright 2023 OpenSSF Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package raw
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/rhysd/actionlint"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/checks/fileparser"
|
||||
sce "github.com/ossf/scorecard/v4/errors"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
)
|
||||
|
||||
const CheckSAST = "SAST"
|
||||
|
||||
var errInvalid = errors.New("invalid")
|
||||
|
||||
var sastTools = map[string]bool{
|
||||
"github-advanced-security": true,
|
||||
"github-code-scanning": true,
|
||||
"lgtm-com": true,
|
||||
"sonarcloud": true,
|
||||
}
|
||||
|
||||
var allowedConclusions = map[string]bool{"success": true, "neutral": true}
|
||||
|
||||
// SAST checks for presence of static analysis tools.
|
||||
func SAST(c *checker.CheckRequest) (checker.SASTData, error) {
|
||||
var data checker.SASTData
|
||||
|
||||
commits, err := sastToolInCheckRuns(c)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
data.Commits = commits
|
||||
|
||||
codeQLWorkflows, err := codeQLInCheckDefinitions(c)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
|
||||
data.Workflows = append(data.Workflows, codeQLWorkflows...)
|
||||
|
||||
sonarWorkflows, err := getSonarWorkflows(c)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
data.Workflows = append(data.Workflows, sonarWorkflows...)
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func sastToolInCheckRuns(c *checker.CheckRequest) ([]checker.SASTCommit, error) {
|
||||
var sastCommits []checker.SASTCommit
|
||||
commits, err := c.RepoClient.ListCommits()
|
||||
if err != nil {
|
||||
return sastCommits,
|
||||
sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("RepoClient.ListCommits: %v", err))
|
||||
}
|
||||
|
||||
for i := range commits {
|
||||
pr := commits[i].AssociatedMergeRequest
|
||||
// TODO(#575): We ignore associated PRs if Scorecard is being run on a fork
|
||||
// but the PR was created in the original repo.
|
||||
if pr.MergedAt.IsZero() {
|
||||
continue
|
||||
}
|
||||
|
||||
checked := false
|
||||
crs, err := c.RepoClient.ListCheckRunsForRef(pr.HeadSHA)
|
||||
if err != nil {
|
||||
return sastCommits,
|
||||
sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("Client.Checks.ListCheckRunsForRef: %v", err))
|
||||
}
|
||||
// Note: crs may be `nil`: in this case
|
||||
// the loop below will be skipped.
|
||||
for _, cr := range crs {
|
||||
if cr.Status != "completed" {
|
||||
continue
|
||||
}
|
||||
if !allowedConclusions[cr.Conclusion] {
|
||||
continue
|
||||
}
|
||||
if sastTools[cr.App.Slug] {
|
||||
c.Dlogger.Debug(&checker.LogMessage{
|
||||
Path: cr.URL,
|
||||
Type: finding.FileTypeURL,
|
||||
Text: fmt.Sprintf("tool detected: %v", cr.App.Slug),
|
||||
})
|
||||
checked = true
|
||||
break
|
||||
}
|
||||
}
|
||||
sastCommit := checker.SASTCommit{
|
||||
CommittedDate: commits[i].CommittedDate,
|
||||
Message: commits[i].Message,
|
||||
SHA: commits[i].SHA,
|
||||
AssociatedMergeRequest: commits[i].AssociatedMergeRequest,
|
||||
Committer: commits[i].Committer,
|
||||
Compliant: checked,
|
||||
}
|
||||
sastCommits = append(sastCommits, sastCommit)
|
||||
}
|
||||
return sastCommits, nil
|
||||
}
|
||||
|
||||
func codeQLInCheckDefinitions(c *checker.CheckRequest) ([]checker.SASTWorkflow, error) {
|
||||
var workflowPaths []string
|
||||
var sastWorkflows []checker.SASTWorkflow
|
||||
err := fileparser.OnMatchingFileContentDo(c.RepoClient, fileparser.PathMatcher{
|
||||
Pattern: ".github/workflows/*",
|
||||
CaseSensitive: false,
|
||||
}, searchGitHubActionWorkflowCodeQL, &workflowPaths)
|
||||
if err != nil {
|
||||
return sastWorkflows, err
|
||||
}
|
||||
for _, path := range workflowPaths {
|
||||
sastWorkflow := checker.SASTWorkflow{
|
||||
File: checker.File{
|
||||
Path: path,
|
||||
Offset: checker.OffsetDefault,
|
||||
Type: finding.FileTypeSource,
|
||||
},
|
||||
Type: checker.CodeQLWorkflow,
|
||||
}
|
||||
|
||||
sastWorkflows = append(sastWorkflows, sastWorkflow)
|
||||
}
|
||||
return sastWorkflows, nil
|
||||
}
|
||||
|
||||
// Check file content.
|
||||
var searchGitHubActionWorkflowCodeQL fileparser.DoWhileTrueOnFileContent = func(path string,
|
||||
content []byte,
|
||||
args ...interface{},
|
||||
) (bool, error) {
|
||||
if !fileparser.IsWorkflowFile(path) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if len(args) != 1 {
|
||||
return false, fmt.Errorf(
|
||||
"searchGitHubActionWorkflowCodeQL requires exactly 1 arguments: %w", errInvalid)
|
||||
}
|
||||
|
||||
// Verify the type of the data.
|
||||
paths, ok := args[0].(*[]string)
|
||||
if !ok {
|
||||
return false, fmt.Errorf(
|
||||
"searchGitHubActionWorkflowCodeQL expects arg[0] of type *[]string: %w", errInvalid)
|
||||
}
|
||||
|
||||
workflow, errs := actionlint.Parse(content)
|
||||
if len(errs) > 0 && workflow == nil {
|
||||
return false, fileparser.FormatActionlintError(errs)
|
||||
}
|
||||
|
||||
for _, job := range workflow.Jobs {
|
||||
for _, step := range job.Steps {
|
||||
e, ok := step.Exec.(*actionlint.ExecAction)
|
||||
if !ok || e == nil || e.Uses == nil {
|
||||
continue
|
||||
}
|
||||
// Parse out repo / SHA.
|
||||
uses := strings.TrimPrefix(e.Uses.Value, "actions://")
|
||||
action, _, _ := strings.Cut(uses, "@")
|
||||
if action == "github/codeql-action/analyze" {
|
||||
*paths = append(*paths, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
type sonarConfig struct {
|
||||
url string
|
||||
file checker.File
|
||||
}
|
||||
|
||||
func getSonarWorkflows(c *checker.CheckRequest) ([]checker.SASTWorkflow, error) {
|
||||
var config []sonarConfig
|
||||
var sastWorkflows []checker.SASTWorkflow
|
||||
err := fileparser.OnMatchingFileContentDo(c.RepoClient, fileparser.PathMatcher{
|
||||
Pattern: "*",
|
||||
CaseSensitive: false,
|
||||
}, validateSonarConfig, &config)
|
||||
if err != nil {
|
||||
return sastWorkflows, err
|
||||
}
|
||||
for _, result := range config {
|
||||
sastWorkflow := checker.SASTWorkflow{
|
||||
File: checker.File{
|
||||
Path: result.file.Path,
|
||||
Offset: result.file.Offset,
|
||||
EndOffset: result.file.EndOffset,
|
||||
Type: result.file.Type,
|
||||
Snippet: result.url,
|
||||
},
|
||||
Type: checker.SonarWorkflow,
|
||||
}
|
||||
|
||||
sastWorkflows = append(sastWorkflows, sastWorkflow)
|
||||
}
|
||||
return sastWorkflows, nil
|
||||
}
|
||||
|
||||
// Check file content.
|
||||
var validateSonarConfig fileparser.DoWhileTrueOnFileContent = func(pathfn string,
|
||||
content []byte,
|
||||
args ...interface{},
|
||||
) (bool, error) {
|
||||
if !strings.EqualFold(path.Base(pathfn), "pom.xml") {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if len(args) != 1 {
|
||||
return false, fmt.Errorf(
|
||||
"validateSonarConfig requires exactly 1 argument: %w", errInvalid)
|
||||
}
|
||||
|
||||
// Verify the type of the data.
|
||||
pdata, ok := args[0].(*[]sonarConfig)
|
||||
if !ok {
|
||||
return false, fmt.Errorf(
|
||||
"validateSonarConfig expects arg[0] of type *[]sonarConfig]: %w", errInvalid)
|
||||
}
|
||||
|
||||
regex := regexp.MustCompile(`<sonar\.host\.url>\s*(\S+)\s*<\/sonar\.host\.url>`)
|
||||
match := regex.FindSubmatch(content)
|
||||
|
||||
if len(match) < 2 {
|
||||
return true, nil
|
||||
}
|
||||
offset, err := findLine(content, []byte("<sonar.host.url>"))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
endOffset, err := findLine(content, []byte("</sonar.host.url>"))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
*pdata = append(*pdata, sonarConfig{
|
||||
url: string(match[1]),
|
||||
file: checker.File{
|
||||
Path: pathfn,
|
||||
Type: finding.FileTypeSource,
|
||||
Offset: offset,
|
||||
EndOffset: endOffset,
|
||||
},
|
||||
})
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func findLine(content, data []byte) (uint, error) {
|
||||
r := bytes.NewReader(content)
|
||||
scanner := bufio.NewScanner(r)
|
||||
|
||||
line := 0
|
||||
// https://golang.org/pkg/bufio/#Scanner.Scan
|
||||
for scanner.Scan() {
|
||||
line++
|
||||
if strings.Contains(scanner.Text(), string(data)) {
|
||||
return uint(line), nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return 0, fmt.Errorf("scanner.Err(): %w", err)
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
161
checks/raw/sast_test.go
Normal file
161
checks/raw/sast_test.go
Normal file
@ -0,0 +1,161 @@
|
||||
// Copyright 2021 OpenSSF Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package raw
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
mockrepo "github.com/ossf/scorecard/v4/clients/mockclients"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
)
|
||||
|
||||
func TestSAST(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
files []string
|
||||
commits []clients.Commit
|
||||
expected checker.SASTData
|
||||
}{
|
||||
{
|
||||
name: "has codeql 1",
|
||||
files: []string{
|
||||
".github/workflows/workflow-not-pinned.yaml",
|
||||
".github/workflows/pom.xml",
|
||||
},
|
||||
commits: []clients.Commit{
|
||||
{
|
||||
AssociatedMergeRequest: clients.PullRequest{
|
||||
Number: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: checker.SASTData{
|
||||
Workflows: []checker.SASTWorkflow{
|
||||
{
|
||||
Type: checker.CodeQLWorkflow,
|
||||
File: checker.File{
|
||||
Path: ".github/workflows/workflow-not-pinned.yaml",
|
||||
Offset: checker.OffsetDefault,
|
||||
Type: finding.FileTypeSource,
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: checker.SonarWorkflow,
|
||||
File: checker.File{
|
||||
Path: ".github/workflows/pom.xml",
|
||||
Type: finding.FileTypeSource,
|
||||
Snippet: "https://sonarqube.private.domain",
|
||||
Offset: 2,
|
||||
EndOffset: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "has codeql 2",
|
||||
files: []string{".github/workflows/github-workflow-multiple-unpinned-uses.yaml"},
|
||||
commits: []clients.Commit{
|
||||
{
|
||||
AssociatedMergeRequest: clients.PullRequest{
|
||||
Number: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: checker.SASTData{
|
||||
Workflows: []checker.SASTWorkflow{
|
||||
{
|
||||
Type: checker.CodeQLWorkflow,
|
||||
File: checker.File{
|
||||
Path: ".github/workflows/github-workflow-multiple-unpinned-uses.yaml",
|
||||
Offset: checker.OffsetDefault,
|
||||
Type: finding.FileTypeSource,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Does not use CodeQL",
|
||||
files: []string{".github/workflows/github-workflow-download-lines.yaml"},
|
||||
expected: checker.SASTData{
|
||||
Workflows: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Airflows CodeQL workflow - Has CodeQL",
|
||||
files: []string{".github/workflows/airflows-codeql.yaml"},
|
||||
commits: []clients.Commit{
|
||||
{
|
||||
AssociatedMergeRequest: clients.PullRequest{
|
||||
Number: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: checker.SASTData{
|
||||
Workflows: []checker.SASTWorkflow{
|
||||
{
|
||||
Type: checker.CodeQLWorkflow,
|
||||
File: checker.File{
|
||||
Path: ".github/workflows/airflows-codeql.yaml",
|
||||
Offset: checker.OffsetDefault,
|
||||
Type: finding.FileTypeSource,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt // Re-initializing variable so it is not changed while executing the closure below
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
mockRepoClient := mockrepo.NewMockRepoClient(ctrl)
|
||||
mockRepoClient.EXPECT().ListFiles(gomock.Any()).Return(tt.files, nil).AnyTimes()
|
||||
mockRepoClient.EXPECT().ListCommits().DoAndReturn(func() ([]clients.Commit, error) {
|
||||
return tt.commits, nil
|
||||
})
|
||||
mockRepoClient.EXPECT().GetFileContent(gomock.Any()).DoAndReturn(func(file string) ([]byte, error) {
|
||||
// This will read the file and return the content
|
||||
content, err := os.ReadFile("./testdata/" + file)
|
||||
if err != nil {
|
||||
return content, fmt.Errorf("%w", err)
|
||||
}
|
||||
return content, nil
|
||||
}).AnyTimes()
|
||||
req := checker.CheckRequest{
|
||||
RepoClient: mockRepoClient,
|
||||
}
|
||||
sastWorkflowsGot, err := SAST(&req)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if diff := cmp.Diff(tt.expected, sastWorkflowsGot); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
109
checks/raw/testdata/.github/workflows/airflows-codeql.yaml
vendored
Normal file
109
checks/raw/testdata/.github/workflows/airflows-codeql.yaml
vendored
Normal file
@ -0,0 +1,109 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
---
|
||||
name: "CodeQL"
|
||||
|
||||
on: # yamllint disable-line rule:truthy
|
||||
push:
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: '0 2 * * *'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
group: codeql-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
selective-checks:
|
||||
name: Selective checks
|
||||
runs-on: ubuntu-20.04
|
||||
outputs:
|
||||
needs-python-scans: ${{ steps.selective-checks.outputs.needs-python-scans }}
|
||||
needs-javascript-scans: ${{ steps.selective-checks.outputs.needs-javascript-scans }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
persist-credentials: false
|
||||
- name: Selective checks
|
||||
id: selective-checks
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
TARGET_COMMIT_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
if [[ ${EVENT_NAME} == "pull_request" ]]; then
|
||||
# Run selective checks
|
||||
./scripts/ci/selective_ci_checks.sh "${TARGET_COMMIT_SHA}"
|
||||
else
|
||||
# Run all checks
|
||||
./scripts/ci/selective_ci_checks.sh
|
||||
fi
|
||||
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [selective-checks]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Override automatic language detection by changing the below list
|
||||
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
|
||||
language: ['python', 'javascript']
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
pull-requests: read
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
if: |
|
||||
matrix.language == 'python' && needs.selective-checks.outputs.needs-python-scans == 'true' ||
|
||||
matrix.language == 'javascript' && needs.selective-checks.outputs.needs-javascript-scans == 'true'
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
if: |
|
||||
matrix.language == 'python' && needs.selective-checks.outputs.needs-python-scans == 'true' ||
|
||||
matrix.language == 'javascript' && needs.selective-checks.outputs.needs-javascript-scans == 'true'
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
if: |
|
||||
matrix.language == 'python' && needs.selective-checks.outputs.needs-python-scans == 'true' ||
|
||||
matrix.language == 'javascript' && needs.selective-checks.outputs.needs-javascript-scans == 'true'
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
if: |
|
||||
matrix.language == 'python' && needs.selective-checks.outputs.needs-python-scans == 'true' ||
|
||||
matrix.language == 'javascript' && needs.selective-checks.outputs.needs-javascript-scans == 'true'
|
4
checks/raw/testdata/.github/workflows/pom.xml
vendored
Normal file
4
checks/raw/testdata/.github/workflows/pom.xml
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
<sonar.coverage.jacoco.xmlReportPaths>target/jacoco-report/jacoco.xml</sonar.coverage.jacoco.xmlReportPaths>
|
||||
<sonar.host.url>https://sonarqube.private.domain</sonar.host.url>
|
||||
<sonar.projectKey>${projectKey}</sonar.projectKey>
|
||||
<sonar.moduleKey>${project.artifactId}</sonar.moduleKey>
|
370
checks/sast.go
370
checks/sast.go
@ -15,36 +15,17 @@
|
||||
package checks
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/rhysd/actionlint"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/checks/fileparser"
|
||||
"github.com/ossf/scorecard/v4/checks/evaluation"
|
||||
"github.com/ossf/scorecard/v4/checks/raw"
|
||||
sce "github.com/ossf/scorecard/v4/errors"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
"github.com/ossf/scorecard/v4/probes"
|
||||
"github.com/ossf/scorecard/v4/probes/zrunner"
|
||||
)
|
||||
|
||||
// CheckSAST is the registered name for SAST.
|
||||
const CheckSAST = "SAST"
|
||||
|
||||
var errInvalid = errors.New("invalid")
|
||||
|
||||
var sastTools = map[string]bool{
|
||||
"github-advanced-security": true,
|
||||
"github-code-scanning": true,
|
||||
"lgtm-com": true,
|
||||
"sonarcloud": true,
|
||||
}
|
||||
|
||||
var allowedConclusions = map[string]bool{"success": true, "neutral": true}
|
||||
|
||||
//nolint:gochecknoinits
|
||||
func init() {
|
||||
if err := registerCheck(CheckSAST, SAST, nil); err != nil {
|
||||
@ -55,342 +36,23 @@ func init() {
|
||||
|
||||
// SAST runs SAST check.
|
||||
func SAST(c *checker.CheckRequest) checker.CheckResult {
|
||||
sastScore, nonCompliantPRs, sastErr := sastToolInCheckRuns(c)
|
||||
if sastErr != nil {
|
||||
return checker.CreateRuntimeErrorResult(CheckSAST, sastErr)
|
||||
}
|
||||
|
||||
codeQlScore, codeQlErr := codeQLInCheckDefinitions(c)
|
||||
if codeQlErr != nil {
|
||||
return checker.CreateRuntimeErrorResult(CheckSAST, codeQlErr)
|
||||
}
|
||||
sonarScore, sonarErr := sonarEnabled(c)
|
||||
if sonarErr != nil {
|
||||
return checker.CreateRuntimeErrorResult(CheckSAST, sonarErr)
|
||||
}
|
||||
|
||||
if sonarScore == checker.MaxResultScore {
|
||||
return checker.CreateMaxScoreResult(CheckSAST, "SAST tool detected")
|
||||
}
|
||||
|
||||
// Both results are inconclusive.
|
||||
// Can never happen.
|
||||
if sastScore == checker.InconclusiveResultScore &&
|
||||
codeQlScore == checker.InconclusiveResultScore {
|
||||
// That can never happen since sastToolInCheckRuns can never
|
||||
// retun checker.InconclusiveResultScore.
|
||||
return checker.CreateRuntimeErrorResult(CheckSAST, sce.ErrScorecardInternal)
|
||||
}
|
||||
|
||||
// Both scores are conclusive.
|
||||
// We assume the CodeQl config uses a cron and is not enabled as pre-submit.
|
||||
// TODO: verify the above comment in code.
|
||||
// We encourage developers to have sast check run on every pre-submit rather
|
||||
// than as cron jobs through the score computation below.
|
||||
// Warning: there is a hidden assumption that *any* sast tool is equally good.
|
||||
if sastScore != checker.InconclusiveResultScore &&
|
||||
codeQlScore != checker.InconclusiveResultScore {
|
||||
switch {
|
||||
case sastScore == checker.MaxResultScore:
|
||||
return checker.CreateMaxScoreResult(CheckSAST, "SAST tool is run on all commits")
|
||||
case codeQlScore == checker.MinResultScore:
|
||||
return checker.CreateResultWithScore(CheckSAST,
|
||||
checker.NormalizeReason("SAST tool is not run on all commits", sastScore), sastScore)
|
||||
|
||||
// codeQl is enabled and sast has 0+ (but not all) PRs checks.
|
||||
case codeQlScore == checker.MaxResultScore:
|
||||
const sastWeight = 3
|
||||
const codeQlWeight = 7
|
||||
c.Dlogger.Debug(&checker.LogMessage{
|
||||
Text: getNonCompliantPRMessage(nonCompliantPRs),
|
||||
})
|
||||
score := checker.AggregateScoresWithWeight(map[int]int{sastScore: sastWeight, codeQlScore: codeQlWeight})
|
||||
return checker.CreateResultWithScore(CheckSAST, "SAST tool detected but not run on all commits", score)
|
||||
default:
|
||||
return checker.CreateRuntimeErrorResult(CheckSAST, sce.WithMessage(sce.ErrScorecardInternal, "contact team"))
|
||||
}
|
||||
}
|
||||
|
||||
// Sast inconclusive.
|
||||
if codeQlScore != checker.InconclusiveResultScore {
|
||||
if codeQlScore == checker.MaxResultScore {
|
||||
return checker.CreateMaxScoreResult(CheckSAST, "SAST tool detected")
|
||||
}
|
||||
return checker.CreateMinScoreResult(CheckSAST, "no SAST tool detected")
|
||||
}
|
||||
|
||||
// CodeQl inconclusive.
|
||||
if sastScore != checker.InconclusiveResultScore {
|
||||
if sastScore == checker.MaxResultScore {
|
||||
return checker.CreateMaxScoreResult(CheckSAST, "SAST tool is run on all commits")
|
||||
}
|
||||
|
||||
c.Dlogger.Debug(&checker.LogMessage{
|
||||
Text: getNonCompliantPRMessage(nonCompliantPRs),
|
||||
})
|
||||
return checker.CreateResultWithScore(CheckSAST,
|
||||
checker.NormalizeReason("SAST tool is not run on all commits", sastScore), sastScore)
|
||||
}
|
||||
|
||||
// Should never happen.
|
||||
return checker.CreateRuntimeErrorResult(CheckSAST, sce.WithMessage(sce.ErrScorecardInternal, "contact team"))
|
||||
}
|
||||
|
||||
func sastToolInCheckRuns(c *checker.CheckRequest) (int, map[int]int, error) {
|
||||
commits, err := c.RepoClient.ListCommits()
|
||||
rawData, err := raw.SAST(c)
|
||||
if err != nil {
|
||||
return checker.InconclusiveResultScore, nil,
|
||||
sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("RepoClient.ListCommits: %v", err))
|
||||
e := sce.WithMessage(sce.ErrScorecardInternal, err.Error())
|
||||
return checker.CreateRuntimeErrorResult(CheckSAST, e)
|
||||
}
|
||||
|
||||
totalMerged := 0
|
||||
totalTested := 0
|
||||
nonCompliantPRs := make(map[int]int)
|
||||
for i := range commits {
|
||||
pr := commits[i].AssociatedMergeRequest
|
||||
// TODO(#575): We ignore associated PRs if Scorecard is being run on a fork
|
||||
// but the PR was created in the original repo.
|
||||
if pr.MergedAt.IsZero() {
|
||||
continue
|
||||
}
|
||||
totalMerged++
|
||||
checked := false
|
||||
crs, err := c.RepoClient.ListCheckRunsForRef(pr.HeadSHA)
|
||||
if err != nil {
|
||||
return checker.InconclusiveResultScore, nil,
|
||||
sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("Client.Checks.ListCheckRunsForRef: %v", err))
|
||||
}
|
||||
// Note: crs may be `nil`: in this case
|
||||
// the loop below will be skipped.
|
||||
for _, cr := range crs {
|
||||
if cr.Status != "completed" {
|
||||
continue
|
||||
}
|
||||
if !allowedConclusions[cr.Conclusion] {
|
||||
continue
|
||||
}
|
||||
if sastTools[cr.App.Slug] {
|
||||
c.Dlogger.Debug(&checker.LogMessage{
|
||||
Path: cr.URL,
|
||||
Type: finding.FileTypeURL,
|
||||
Text: fmt.Sprintf("tool detected: %v", cr.App.Slug),
|
||||
})
|
||||
totalTested++
|
||||
checked = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !checked {
|
||||
nonCompliantPRs[pr.Number] = pr.Number
|
||||
}
|
||||
}
|
||||
if totalMerged == 0 {
|
||||
c.Dlogger.Warn(&checker.LogMessage{
|
||||
Text: "no pull requests merged into dev branch",
|
||||
})
|
||||
return checker.InconclusiveResultScore, nil, nil
|
||||
}
|
||||
// Set the raw results.
|
||||
pRawResults := getRawResults(c)
|
||||
pRawResults.SASTResults = rawData
|
||||
|
||||
if totalTested == totalMerged {
|
||||
c.Dlogger.Info(&checker.LogMessage{
|
||||
Text: fmt.Sprintf("all commits (%v) are checked with a SAST tool", totalMerged),
|
||||
})
|
||||
} else {
|
||||
c.Dlogger.Warn(&checker.LogMessage{
|
||||
Text: fmt.Sprintf("%v commits out of %v are checked with a SAST tool", totalTested, totalMerged),
|
||||
})
|
||||
}
|
||||
|
||||
return checker.CreateProportionalScore(totalTested, totalMerged), nonCompliantPRs, nil
|
||||
}
|
||||
|
||||
func codeQLInCheckDefinitions(c *checker.CheckRequest) (int, error) {
|
||||
var workflowPaths []string
|
||||
err := fileparser.OnMatchingFileContentDo(c.RepoClient, fileparser.PathMatcher{
|
||||
Pattern: ".github/workflows/*",
|
||||
CaseSensitive: false,
|
||||
}, searchGitHubActionWorkflowCodeQL, &workflowPaths)
|
||||
// Evaluate the probes.
|
||||
findings, err := zrunner.Run(pRawResults, probes.SAST)
|
||||
if err != nil {
|
||||
return checker.InconclusiveResultScore, err
|
||||
e := sce.WithMessage(sce.ErrScorecardInternal, err.Error())
|
||||
return checker.CreateRuntimeErrorResult(CheckSAST, e)
|
||||
}
|
||||
|
||||
for _, path := range workflowPaths {
|
||||
c.Dlogger.Debug(&checker.LogMessage{
|
||||
Path: path,
|
||||
Type: finding.FileTypeSource,
|
||||
Offset: checker.OffsetDefault,
|
||||
Text: "CodeQL detected",
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: check if it's enabled as cron or presubmit.
|
||||
// TODO: check which branches it is enabled on. We should find main.
|
||||
if len(workflowPaths) > 0 {
|
||||
c.Dlogger.Info(&checker.LogMessage{
|
||||
Text: "SAST tool detected: CodeQL",
|
||||
})
|
||||
return checker.MaxResultScore, nil
|
||||
}
|
||||
|
||||
c.Dlogger.Warn(&checker.LogMessage{
|
||||
Text: "CodeQL tool not detected",
|
||||
})
|
||||
return checker.MinResultScore, nil
|
||||
}
|
||||
|
||||
// Check file content.
|
||||
var searchGitHubActionWorkflowCodeQL fileparser.DoWhileTrueOnFileContent = func(path string,
|
||||
content []byte,
|
||||
args ...interface{},
|
||||
) (bool, error) {
|
||||
if !fileparser.IsWorkflowFile(path) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if len(args) != 1 {
|
||||
return false, fmt.Errorf(
|
||||
"searchGitHubActionWorkflowCodeQL requires exactly 1 arguments: %w", errInvalid)
|
||||
}
|
||||
|
||||
// Verify the type of the data.
|
||||
paths, ok := args[0].(*[]string)
|
||||
if !ok {
|
||||
return false, fmt.Errorf(
|
||||
"searchGitHubActionWorkflowCodeQL expects arg[0] of type *[]string: %w", errInvalid)
|
||||
}
|
||||
|
||||
workflow, errs := actionlint.Parse(content)
|
||||
if len(errs) > 0 && workflow == nil {
|
||||
return false, fileparser.FormatActionlintError(errs)
|
||||
}
|
||||
|
||||
for _, job := range workflow.Jobs {
|
||||
for _, step := range job.Steps {
|
||||
e, ok := step.Exec.(*actionlint.ExecAction)
|
||||
if !ok || e == nil || e.Uses == nil {
|
||||
continue
|
||||
}
|
||||
// Parse out repo / SHA.
|
||||
uses := strings.TrimPrefix(e.Uses.Value, "actions://")
|
||||
action, _, _ := strings.Cut(uses, "@")
|
||||
if action == "github/codeql-action/analyze" {
|
||||
*paths = append(*paths, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
type sonarConfig struct {
|
||||
url string
|
||||
file checker.File
|
||||
}
|
||||
|
||||
func sonarEnabled(c *checker.CheckRequest) (int, error) {
|
||||
var config []sonarConfig
|
||||
err := fileparser.OnMatchingFileContentDo(c.RepoClient, fileparser.PathMatcher{
|
||||
Pattern: "*",
|
||||
CaseSensitive: false,
|
||||
}, validateSonarConfig, &config)
|
||||
if err != nil {
|
||||
return checker.InconclusiveResultScore, err
|
||||
}
|
||||
for _, result := range config {
|
||||
c.Dlogger.Info(&checker.LogMessage{
|
||||
Path: result.file.Path,
|
||||
Type: result.file.Type,
|
||||
Offset: result.file.Offset,
|
||||
EndOffset: result.file.EndOffset,
|
||||
Text: "Sonar configuration detected",
|
||||
Snippet: result.url,
|
||||
})
|
||||
}
|
||||
|
||||
if len(config) > 0 {
|
||||
return checker.MaxResultScore, nil
|
||||
}
|
||||
|
||||
return checker.MinResultScore, nil
|
||||
}
|
||||
|
||||
// Check file content.
|
||||
var validateSonarConfig fileparser.DoWhileTrueOnFileContent = func(pathfn string,
|
||||
content []byte,
|
||||
args ...interface{},
|
||||
) (bool, error) {
|
||||
if !strings.EqualFold(path.Base(pathfn), "pom.xml") {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if len(args) != 1 {
|
||||
return false, fmt.Errorf(
|
||||
"validateSonarConfig requires exactly 1 argument: %w", errInvalid)
|
||||
}
|
||||
|
||||
// Verify the type of the data.
|
||||
pdata, ok := args[0].(*[]sonarConfig)
|
||||
if !ok {
|
||||
return false, fmt.Errorf(
|
||||
"validateSonarConfig expects arg[0] of type *[]sonarConfig]: %w", errInvalid)
|
||||
}
|
||||
|
||||
regex := regexp.MustCompile(`<sonar\.host\.url>\s*(\S+)\s*<\/sonar\.host\.url>`)
|
||||
match := regex.FindSubmatch(content)
|
||||
|
||||
if len(match) < 2 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
offset, err := findLine(content, []byte("<sonar.host.url>"))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
endOffset, err := findLine(content, []byte("</sonar.host.url>"))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
*pdata = append(*pdata, sonarConfig{
|
||||
url: string(match[1]),
|
||||
file: checker.File{
|
||||
Path: pathfn,
|
||||
Type: finding.FileTypeSource,
|
||||
Offset: offset,
|
||||
EndOffset: endOffset,
|
||||
},
|
||||
})
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func findLine(content, data []byte) (uint, error) {
|
||||
r := bytes.NewReader(content)
|
||||
scanner := bufio.NewScanner(r)
|
||||
|
||||
line := 0
|
||||
// https://golang.org/pkg/bufio/#Scanner.Scan
|
||||
for scanner.Scan() {
|
||||
line++
|
||||
if strings.Contains(scanner.Text(), string(data)) {
|
||||
return uint(line), nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return 0, fmt.Errorf("scanner.Err(): %w", err)
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func getNonCompliantPRMessage(intMap map[int]int) string {
|
||||
var sb strings.Builder
|
||||
for _, value := range intMap {
|
||||
if len(sb.String()) != 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%d", value))
|
||||
}
|
||||
return fmt.Sprintf("List of pull requests without CI test: %s", sb.String())
|
||||
// Return the score evaluation.
|
||||
return evaluation.SAST(CheckSAST, findings, c.Dlogger)
|
||||
}
|
||||
|
@ -37,8 +37,8 @@ func Test_SAST(t *testing.T) {
|
||||
//nolint: govet, goerr113
|
||||
tests := []struct {
|
||||
name string
|
||||
commits []clients.Commit
|
||||
err error
|
||||
commits []clients.Commit
|
||||
searchresult clients.SearchResponse
|
||||
checkRuns []clients.CheckRun
|
||||
searchRequest clients.SearchRequest
|
||||
@ -124,6 +124,7 @@ func Test_SAST(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
path: "",
|
||||
expected: checker.CheckResult{
|
||||
Score: 10,
|
||||
},
|
||||
@ -152,44 +153,90 @@ func Test_SAST(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Failed SAST checker should return success status",
|
||||
name: "Airflow Workflow has CodeQL but has no check runs.",
|
||||
err: nil,
|
||||
commits: []clients.Commit{
|
||||
{
|
||||
AssociatedMergeRequest: clients.PullRequest{
|
||||
MergedAt: time.Now().Add(time.Hour - 1),
|
||||
},
|
||||
},
|
||||
{
|
||||
AssociatedMergeRequest: clients.PullRequest{
|
||||
MergedAt: time.Now().Add(time.Hour - 10),
|
||||
},
|
||||
},
|
||||
{
|
||||
AssociatedMergeRequest: clients.PullRequest{
|
||||
MergedAt: time.Now().Add(time.Hour - 20),
|
||||
},
|
||||
},
|
||||
{
|
||||
AssociatedMergeRequest: clients.PullRequest{
|
||||
MergedAt: time.Now().Add(time.Hour - 30),
|
||||
},
|
||||
},
|
||||
},
|
||||
path: ".github/workflows/github-workflow-sast-codeql.yaml",
|
||||
checkRuns: []clients.CheckRun{
|
||||
{
|
||||
Status: "completed",
|
||||
App: clients.CheckRunApp{
|
||||
Slug: "lgtm-com",
|
||||
},
|
||||
},
|
||||
},
|
||||
searchresult: clients.SearchResponse{},
|
||||
path: ".github/workflows/airflow-codeql-workflow.yaml",
|
||||
expected: checker.CheckResult{
|
||||
Score: 7,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Failed SAST checker with checkRuns not completed",
|
||||
name: "Airflow Workflow has CodeQL and two check runs.",
|
||||
err: nil,
|
||||
commits: []clients.Commit{
|
||||
{
|
||||
AssociatedMergeRequest: clients.PullRequest{
|
||||
MergedAt: time.Now().Add(time.Hour - 1),
|
||||
},
|
||||
},
|
||||
},
|
||||
searchresult: clients.SearchResponse{},
|
||||
checkRuns: []clients.CheckRun{
|
||||
{
|
||||
Status: "completed",
|
||||
Conclusion: "success",
|
||||
App: clients.CheckRunApp{
|
||||
Slug: "lgtm-com",
|
||||
},
|
||||
},
|
||||
{
|
||||
Status: "completed",
|
||||
Conclusion: "success",
|
||||
App: clients.CheckRunApp{
|
||||
Slug: "lgtm-com",
|
||||
},
|
||||
},
|
||||
},
|
||||
path: ".github/workflows/airflow-codeql-workflow.yaml",
|
||||
expected: checker.CheckResult{
|
||||
Score: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: `Airflow Workflow has CodeQL and two check runs one of
|
||||
which has wrong type of conlusion. The other is 'success'`,
|
||||
err: nil,
|
||||
commits: []clients.Commit{
|
||||
{
|
||||
AssociatedMergeRequest: clients.PullRequest{
|
||||
MergedAt: time.Now().Add(time.Hour - 1),
|
||||
},
|
||||
},
|
||||
},
|
||||
searchresult: clients.SearchResponse{},
|
||||
checkRuns: []clients.CheckRun{
|
||||
{
|
||||
Status: "completed",
|
||||
Conclusion: "wrongConclusionValue",
|
||||
App: clients.CheckRunApp{
|
||||
Slug: "lgtm-com",
|
||||
},
|
||||
},
|
||||
{
|
||||
Status: "completed",
|
||||
Conclusion: "success",
|
||||
App: clients.CheckRunApp{
|
||||
Slug: "lgtm-com",
|
||||
},
|
||||
},
|
||||
},
|
||||
path: ".github/workflows/airflow-codeql-workflow.yaml",
|
||||
expected: checker.CheckResult{
|
||||
Score: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: `Airflow Workflow has CodeQL and two commits none of which
|
||||
ran the workflow.`,
|
||||
err: nil,
|
||||
commits: []clients.Commit{
|
||||
{
|
||||
AssociatedMergeRequest: clients.PullRequest{
|
||||
@ -198,87 +245,30 @@ func Test_SAST(t *testing.T) {
|
||||
},
|
||||
{
|
||||
AssociatedMergeRequest: clients.PullRequest{
|
||||
MergedAt: time.Now().Add(time.Hour - 10),
|
||||
},
|
||||
},
|
||||
{
|
||||
AssociatedMergeRequest: clients.PullRequest{
|
||||
MergedAt: time.Now().Add(time.Hour - 20),
|
||||
},
|
||||
},
|
||||
{
|
||||
AssociatedMergeRequest: clients.PullRequest{
|
||||
MergedAt: time.Now().Add(time.Hour - 30),
|
||||
},
|
||||
},
|
||||
},
|
||||
path: ".github/workflows/github-workflow-sast-no-codeql.yaml",
|
||||
checkRuns: []clients.CheckRun{
|
||||
{
|
||||
App: clients.CheckRunApp{
|
||||
Slug: "lgtm-com",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: checker.CheckResult{
|
||||
Score: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Failed SAST with PullRequest not merged",
|
||||
commits: []clients.Commit{
|
||||
{
|
||||
AssociatedMergeRequest: clients.PullRequest{
|
||||
Number: 1,
|
||||
MergedAt: time.Now().Add(time.Hour - 2),
|
||||
},
|
||||
},
|
||||
},
|
||||
searchresult: clients.SearchResponse{},
|
||||
checkRuns: []clients.CheckRun{
|
||||
{
|
||||
Status: "notCompletedForTestingOnly",
|
||||
Conclusion: "notSuccessForTestingOnly",
|
||||
App: clients.CheckRunApp{
|
||||
Slug: "lgtm-com",
|
||||
},
|
||||
},
|
||||
{
|
||||
Status: "notCompletedForTestingOnly",
|
||||
Conclusion: "notSuccessForTestingOnly",
|
||||
App: clients.CheckRunApp{
|
||||
Slug: "lgtm-com",
|
||||
},
|
||||
},
|
||||
},
|
||||
path: ".github/workflows/airflow-codeql-workflow.yaml",
|
||||
expected: checker.CheckResult{
|
||||
Score: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Merged PullRequest in a different repo",
|
||||
commits: []clients.Commit{
|
||||
{
|
||||
AssociatedMergeRequest: clients.PullRequest{
|
||||
MergedAt: time.Now(),
|
||||
Number: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
searchresult: clients.SearchResponse{},
|
||||
checkRuns: []clients.CheckRun{
|
||||
{
|
||||
App: clients.CheckRunApp{
|
||||
Slug: "lgtm-com",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: checker.CheckResult{
|
||||
Score: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sonartype config 1 line",
|
||||
path: "pom-1line.xml",
|
||||
expected: checker.CheckResult{
|
||||
Score: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sonartype config 2 lines",
|
||||
path: "pom-2lines.xml",
|
||||
expected: checker.CheckResult{
|
||||
Score: 10,
|
||||
Score: 7,
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -333,126 +323,3 @@ func Test_SAST(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_validateSonarConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//nolint: govet
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
offset uint
|
||||
endOffset uint
|
||||
url string
|
||||
score int
|
||||
}{
|
||||
{
|
||||
name: "sonartype config 1 line",
|
||||
path: "./testdata/pom-1line.xml",
|
||||
offset: 2,
|
||||
endOffset: 2,
|
||||
url: "https://sonarqube.private.domain",
|
||||
},
|
||||
{
|
||||
name: "sonartype config 2 lines",
|
||||
path: "./testdata/pom-2lines.xml",
|
||||
offset: 2,
|
||||
endOffset: 4,
|
||||
url: "https://sonarqube.private.domain",
|
||||
},
|
||||
{
|
||||
name: "wrong filename",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var config []sonarConfig
|
||||
var content []byte
|
||||
var err error
|
||||
var path string
|
||||
if tt.path != "" {
|
||||
content, err = os.ReadFile(tt.path)
|
||||
if err != nil {
|
||||
t.Errorf("ReadFile: %v", err)
|
||||
}
|
||||
path = "pom.xml"
|
||||
}
|
||||
_, err = validateSonarConfig(path, content, &config)
|
||||
if err != nil {
|
||||
t.Errorf("Caught error: %v", err)
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
if len(config) != 0 {
|
||||
t.Errorf("Expected no result, got %d for %v", len(config), tt.name)
|
||||
}
|
||||
return
|
||||
}
|
||||
if len(config) != 1 {
|
||||
t.Errorf("Expected 1 result, got %d for %v", len(config), tt.name)
|
||||
}
|
||||
|
||||
if config[0].file.Offset != tt.offset {
|
||||
t.Errorf("Expected offset %d, got %d for %v", tt.offset,
|
||||
config[0].file.Offset, tt.name)
|
||||
}
|
||||
|
||||
if config[0].file.EndOffset != tt.endOffset {
|
||||
t.Errorf("Expected offset %d, got %d for %v", tt.endOffset,
|
||||
config[0].file.EndOffset, tt.name)
|
||||
}
|
||||
|
||||
if config[0].url != tt.url {
|
||||
t.Errorf("Expected offset %v, got %v for %v", tt.url,
|
||||
config[0].url, tt.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_searchGitHubActionWorkflowCodeQL_invalid(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//nolint: govet
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
args []any
|
||||
}{
|
||||
{
|
||||
name: "too few arguments",
|
||||
path: ".github/workflows/github-workflow-sast-codeql.yaml",
|
||||
args: []any{},
|
||||
},
|
||||
{
|
||||
name: "wrong arguments",
|
||||
path: ".github/workflows/github-workflow-sast-codeql.yaml",
|
||||
args: []any{
|
||||
&[]int{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var content []byte
|
||||
var err error
|
||||
if tt.path != "" {
|
||||
content, err = os.ReadFile("./testdata/" + tt.path)
|
||||
if err != nil {
|
||||
t.Errorf("ReadFile: %v", err)
|
||||
}
|
||||
}
|
||||
_, err = searchGitHubActionWorkflowCodeQL(tt.path, content, tt.args...)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error but err was nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
109
checks/testdata/.github/workflows/airflow-codeql-workflow.yaml
vendored
Normal file
109
checks/testdata/.github/workflows/airflow-codeql-workflow.yaml
vendored
Normal file
@ -0,0 +1,109 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
---
|
||||
name: "CodeQL"
|
||||
|
||||
on: # yamllint disable-line rule:truthy
|
||||
push:
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: '0 2 * * *'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
group: codeql-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
selective-checks:
|
||||
name: Selective checks
|
||||
runs-on: ubuntu-20.04
|
||||
outputs:
|
||||
needs-python-scans: ${{ steps.selective-checks.outputs.needs-python-scans }}
|
||||
needs-javascript-scans: ${{ steps.selective-checks.outputs.needs-javascript-scans }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
persist-credentials: false
|
||||
- name: Selective checks
|
||||
id: selective-checks
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
TARGET_COMMIT_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
if [[ ${EVENT_NAME} == "pull_request" ]]; then
|
||||
# Run selective checks
|
||||
./scripts/ci/selective_ci_checks.sh "${TARGET_COMMIT_SHA}"
|
||||
else
|
||||
# Run all checks
|
||||
./scripts/ci/selective_ci_checks.sh
|
||||
fi
|
||||
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [selective-checks]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Override automatic language detection by changing the below list
|
||||
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
|
||||
language: ['python', 'javascript']
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
pull-requests: read
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
if: |
|
||||
matrix.language == 'python' && needs.selective-checks.outputs.needs-python-scans == 'true' ||
|
||||
matrix.language == 'javascript' && needs.selective-checks.outputs.needs-javascript-scans == 'true'
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
if: |
|
||||
matrix.language == 'python' && needs.selective-checks.outputs.needs-python-scans == 'true' ||
|
||||
matrix.language == 'javascript' && needs.selective-checks.outputs.needs-javascript-scans == 'true'
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
if: |
|
||||
matrix.language == 'python' && needs.selective-checks.outputs.needs-python-scans == 'true' ||
|
||||
matrix.language == 'javascript' && needs.selective-checks.outputs.needs-javascript-scans == 'true'
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
if: |
|
||||
matrix.language == 'python' && needs.selective-checks.outputs.needs-python-scans == 'true' ||
|
||||
matrix.language == 'javascript' && needs.selective-checks.outputs.needs-javascript-scans == 'true'
|
@ -47,7 +47,7 @@ var _ = Describe("E2E TEST:"+checks.CheckSAST, func() {
|
||||
Score: 10,
|
||||
NumberOfWarn: 1,
|
||||
NumberOfInfo: 1,
|
||||
NumberOfDebug: 1,
|
||||
NumberOfDebug: 0,
|
||||
}
|
||||
result := checks.SAST(&req)
|
||||
// New version.
|
||||
|
@ -266,6 +266,16 @@ func (f *Finding) WithRemediationMetadata(values map[string]string) *Finding {
|
||||
return f
|
||||
}
|
||||
|
||||
// WithValue adds a value to f.Values.
|
||||
// No copy is made.
|
||||
func (f *Finding) WithValue(k string, v int) *Finding {
|
||||
if f.Values == nil {
|
||||
f.Values = make(map[string]int)
|
||||
}
|
||||
f.Values[k] = v
|
||||
return f
|
||||
}
|
||||
|
||||
// UnmarshalYAML is a custom unmarshalling function
|
||||
// to transform the string into an enum.
|
||||
func (o *Outcome) UnmarshalYAML(n *yaml.Node) error {
|
||||
|
@ -38,6 +38,9 @@ import (
|
||||
"github.com/ossf/scorecard/v4/probes/hasLicenseFileAtTopDir"
|
||||
"github.com/ossf/scorecard/v4/probes/hasOSVVulnerabilities"
|
||||
"github.com/ossf/scorecard/v4/probes/packagedWithAutomatedWorkflow"
|
||||
"github.com/ossf/scorecard/v4/probes/sastToolCodeQLInstalled"
|
||||
"github.com/ossf/scorecard/v4/probes/sastToolRunsOnAllCommits"
|
||||
"github.com/ossf/scorecard/v4/probes/sastToolSonarInstalled"
|
||||
"github.com/ossf/scorecard/v4/probes/securityPolicyContainsLinks"
|
||||
"github.com/ossf/scorecard/v4/probes/securityPolicyContainsText"
|
||||
"github.com/ossf/scorecard/v4/probes/securityPolicyContainsVulnerabilityDisclosure"
|
||||
@ -97,6 +100,11 @@ var (
|
||||
Vulnerabilities = []ProbeImpl{
|
||||
hasOSVVulnerabilities.Run,
|
||||
}
|
||||
SAST = []ProbeImpl{
|
||||
sastToolCodeQLInstalled.Run,
|
||||
sastToolRunsOnAllCommits.Run,
|
||||
sastToolSonarInstalled.Run,
|
||||
}
|
||||
DangerousWorkflows = []ProbeImpl{
|
||||
hasDangerousWorkflowScriptInjection.Run,
|
||||
hasDangerousWorkflowUntrustedCheckout.Run,
|
||||
|
29
probes/sastToolCodeQLInstalled/def.yml
Normal file
29
probes/sastToolCodeQLInstalled/def.yml
Normal file
@ -0,0 +1,29 @@
|
||||
# Copyright 2023 OpenSSF Scorecard Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
id: sastToolCodeQLInstalled
|
||||
short: Check that the project uses the CodeQL github actions
|
||||
motivation: >
|
||||
SAST is testing run on source code before the application is run. Using SAST tools can prevent known classes of bugs from being inadvertently introduced in the codebase.
|
||||
implementation: >
|
||||
The implementation checks whether the project invokes the github/codeql-action/analyze action.
|
||||
outcome:
|
||||
- If the project uses the github/codeql-action/analyze action, the probe returns one finding with OutcomePositive (1).
|
||||
- If the project does not use the github/codeql-action/analyze action, the probe returns one finding with OutcomeNegative (0).
|
||||
remediation:
|
||||
effort: Medium
|
||||
text:
|
||||
- Follow the steps in https://docs.github.com/en/code-security/codeql-cli/getting-started-with-the-codeql-cli/preparing-your-code-for-codeql-analysis to integrate CodeQL for your project.
|
||||
markdown:
|
||||
- Follow the steps in https://docs.github.com/en/code-security/codeql-cli/getting-started-with-the-codeql-cli/preparing-your-code-for-codeql-analysis to integrate CodeQL for your project.
|
57
probes/sastToolCodeQLInstalled/impl.go
Normal file
57
probes/sastToolCodeQLInstalled/impl.go
Normal file
@ -0,0 +1,57 @@
|
||||
// Copyright 2023 OpenSSF Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// nolint:stylecheck
|
||||
package sastToolCodeQLInstalled
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
"github.com/ossf/scorecard/v4/probes/internal/utils/uerror"
|
||||
)
|
||||
|
||||
//go:embed *.yml
|
||||
var fs embed.FS
|
||||
|
||||
const Probe = "sastToolCodeQLInstalled"
|
||||
|
||||
func Run(raw *checker.RawResults) ([]finding.Finding, string, error) {
|
||||
if raw == nil {
|
||||
return nil, "", fmt.Errorf("%w: raw", uerror.ErrNil)
|
||||
}
|
||||
|
||||
r := raw.SASTResults
|
||||
|
||||
for _, wf := range r.Workflows {
|
||||
if wf.Type == checker.CodeQLWorkflow {
|
||||
f, err := finding.NewWith(fs, Probe,
|
||||
"SAST tool installed: CodeQL", nil,
|
||||
finding.OutcomePositive)
|
||||
if err != nil {
|
||||
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||
}
|
||||
return []finding.Finding{*f}, Probe, nil
|
||||
}
|
||||
}
|
||||
f, err := finding.NewWith(fs, Probe,
|
||||
"CodeQL tool not installed", nil,
|
||||
finding.OutcomeNegative)
|
||||
if err != nil {
|
||||
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||
}
|
||||
return []finding.Finding{*f}, Probe, nil
|
||||
}
|
100
probes/sastToolCodeQLInstalled/impl_test.go
Normal file
100
probes/sastToolCodeQLInstalled/impl_test.go
Normal file
@ -0,0 +1,100 @@
|
||||
// Copyright 2023 OpenSSF Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// nolint:stylecheck
|
||||
package sastToolCodeQLInstalled
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
)
|
||||
|
||||
func Test_Run(t *testing.T) {
|
||||
t.Parallel()
|
||||
// nolint:govet
|
||||
tests := []struct {
|
||||
name string
|
||||
raw *checker.RawResults
|
||||
outcomes []finding.Outcome
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "codeql present",
|
||||
err: nil,
|
||||
raw: &checker.RawResults{
|
||||
SASTResults: checker.SASTData{
|
||||
Workflows: []checker.SASTWorkflow{
|
||||
{
|
||||
Type: checker.CodeQLWorkflow,
|
||||
},
|
||||
{
|
||||
Type: checker.SonarWorkflow,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "codeql not present",
|
||||
err: nil,
|
||||
raw: &checker.RawResults{
|
||||
SASTResults: checker.SASTData{
|
||||
Workflows: []checker.SASTWorkflow{
|
||||
{
|
||||
Type: checker.SonarWorkflow,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomeNegative,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt // Re-initializing variable so it is not changed while executing the closure below
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
findings, s, err := Run(tt.raw)
|
||||
if !cmp.Equal(tt.err, err, cmpopts.EquateErrors()) {
|
||||
t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(tt.err, err, cmpopts.EquateErrors()))
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if diff := cmp.Diff(Probe, s); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(len(tt.outcomes), len(findings)); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
for i := range tt.outcomes {
|
||||
outcome := &tt.outcomes[i]
|
||||
f := &findings[i]
|
||||
if diff := cmp.Diff(*outcome, f.Outcome); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
30
probes/sastToolRunsOnAllCommits/def.yml
Normal file
30
probes/sastToolRunsOnAllCommits/def.yml
Normal file
@ -0,0 +1,30 @@
|
||||
# Copyright 2023 OpenSSF Scorecard Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
id: sastToolRunsOnAllCommits
|
||||
short: Checks that a SAST tool runs on all commits in the projects CI.
|
||||
motivation: >
|
||||
SAST is testing run on source code before the application is run. Using SAST tools can prevent known classes of bugs from being inadvertently introduced in the codebase.
|
||||
implementation: >
|
||||
The implementation iterates through the projects commits and checks whether any of the check runs for the commits associated merge request was any of the SAST tools that Scorecard supports.
|
||||
outcome:
|
||||
- If the project had no commits merged, the probe returns a finding with OutcomeNotApplicable.
|
||||
- If the project runs SAST tools successfuly on every pull request before merging, the probe returns one finding with OutcomePositive (1). In addition, the finding will include two values. 1) How many commits were tested by a SAST tool, and 2) How many commits in total were merged.
|
||||
- If the project does not run any SAST tools successfuly on every pull request before merging, the probe returns one finding with OutcomeNegative (0). In addition, the finding will include two values. 1) How many commits were tested by a SAST tool, and 2) How many commits in total were merged.
|
||||
remediation:
|
||||
effort: Low
|
||||
text:
|
||||
- Ensure that your SAST tools run on every commit.
|
||||
markdown:
|
||||
- Ensure that your SAST tools run on every commit.
|
74
probes/sastToolRunsOnAllCommits/impl.go
Normal file
74
probes/sastToolRunsOnAllCommits/impl.go
Normal file
@ -0,0 +1,74 @@
|
||||
// Copyright 2023 OpenSSF Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// nolint:stylecheck
|
||||
package sastToolRunsOnAllCommits
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
"github.com/ossf/scorecard/v4/probes/internal/utils/uerror"
|
||||
)
|
||||
|
||||
//go:embed *.yml
|
||||
var fs embed.FS
|
||||
|
||||
const Probe = "sastToolRunsOnAllCommits" //#nosec
|
||||
|
||||
func Run(raw *checker.RawResults) ([]finding.Finding, string, error) {
|
||||
if raw == nil {
|
||||
return nil, "", fmt.Errorf("%w: raw", uerror.ErrNil)
|
||||
}
|
||||
|
||||
r := raw.SASTResults
|
||||
|
||||
f, err := finding.NewWith(fs, Probe,
|
||||
"", nil,
|
||||
finding.OutcomePositive)
|
||||
if err != nil {
|
||||
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||
}
|
||||
|
||||
totalPullRequestsMerged := len(r.Commits)
|
||||
totalPullRequestsAnalyzed := 0
|
||||
|
||||
for i := range r.Commits {
|
||||
wf := &r.Commits[i]
|
||||
if wf.Compliant {
|
||||
totalPullRequestsAnalyzed++
|
||||
}
|
||||
}
|
||||
|
||||
if totalPullRequestsMerged == 0 {
|
||||
f = f.WithOutcome(finding.OutcomeNotApplicable)
|
||||
f = f.WithMessage("no pull requests merged into dev branch")
|
||||
return []finding.Finding{*f}, Probe, nil
|
||||
}
|
||||
|
||||
f = f.WithValue("totalPullRequestsAnalyzed", totalPullRequestsAnalyzed)
|
||||
f = f.WithValue("totalPullRequestsMerged", totalPullRequestsMerged)
|
||||
|
||||
if totalPullRequestsAnalyzed == totalPullRequestsMerged {
|
||||
msg := fmt.Sprintf("all commits (%v) are checked with a SAST tool", totalPullRequestsMerged)
|
||||
f = f.WithOutcome(finding.OutcomePositive).WithMessage(msg)
|
||||
} else {
|
||||
msg := fmt.Sprintf("%v commits out of %v are checked with a SAST tool",
|
||||
totalPullRequestsAnalyzed, totalPullRequestsMerged)
|
||||
f = f.WithOutcome(finding.OutcomeNegative).WithMessage(msg)
|
||||
}
|
||||
return []finding.Finding{*f}, Probe, nil
|
||||
}
|
128
probes/sastToolRunsOnAllCommits/impl_test.go
Normal file
128
probes/sastToolRunsOnAllCommits/impl_test.go
Normal file
@ -0,0 +1,128 @@
|
||||
// Copyright 2023 OpenSSF Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// nolint:stylecheck
|
||||
package sastToolRunsOnAllCommits
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
)
|
||||
|
||||
func Test_Run(t *testing.T) {
|
||||
t.Parallel()
|
||||
// nolint:govet
|
||||
tests := []struct {
|
||||
name string
|
||||
raw *checker.RawResults
|
||||
outcomes []finding.Outcome
|
||||
err error
|
||||
expectedFindings []finding.Finding
|
||||
}{
|
||||
{
|
||||
name: "sonar present",
|
||||
err: nil,
|
||||
raw: &checker.RawResults{
|
||||
SASTResults: checker.SASTData{
|
||||
Commits: []checker.SASTCommit{
|
||||
{
|
||||
Compliant: false,
|
||||
},
|
||||
{
|
||||
Compliant: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomeNegative,
|
||||
},
|
||||
expectedFindings: []finding.Finding{
|
||||
{
|
||||
Probe: "sastToolRunsOnAllCommits",
|
||||
Message: "1 commits out of 2 are checked with a SAST tool",
|
||||
Values: map[string]int{
|
||||
"totalPullRequestsAnalyzed": 1,
|
||||
"totalPullRequestsMerged": 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sonar present",
|
||||
err: nil,
|
||||
raw: &checker.RawResults{
|
||||
SASTResults: checker.SASTData{
|
||||
Commits: []checker.SASTCommit{
|
||||
{
|
||||
Compliant: true,
|
||||
},
|
||||
{
|
||||
Compliant: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive,
|
||||
},
|
||||
expectedFindings: []finding.Finding{
|
||||
{
|
||||
Probe: "sastToolRunsOnAllCommits",
|
||||
Message: "all commits (2) are checked with a SAST tool",
|
||||
Outcome: finding.OutcomePositive,
|
||||
Values: map[string]int{
|
||||
"totalPullRequestsAnalyzed": 2,
|
||||
"totalPullRequestsMerged": 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt // Re-initializing variable so it is not changed while executing the closure below
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
findings, s, err := Run(tt.raw)
|
||||
if !cmp.Equal(tt.err, err, cmpopts.EquateErrors()) {
|
||||
t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(tt.err, err, cmpopts.EquateErrors()))
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if diff := cmp.Diff(Probe, s); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(len(tt.outcomes), len(findings)); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
for i := range tt.outcomes {
|
||||
outcome := &tt.outcomes[i]
|
||||
f := &findings[i]
|
||||
if diff := cmp.Diff(*outcome, f.Outcome); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
if !cmp.Equal(tt.expectedFindings, findings, cmpopts.EquateErrors()) {
|
||||
t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(tt.expectedFindings, findings, cmpopts.EquateErrors()))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
29
probes/sastToolSonarInstalled/def.yml
Normal file
29
probes/sastToolSonarInstalled/def.yml
Normal file
@ -0,0 +1,29 @@
|
||||
# Copyright 2023 OpenSSF Scorecard Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
id: sastToolSonarInstalled
|
||||
short: Check that the project uses Sonar.
|
||||
motivation: >
|
||||
SAST is testing run on source code before the application is run. Using SAST tools can prevent known classes of bugs from being inadvertently introduced in the codebase.
|
||||
implementation: >
|
||||
The implementation checks the projects pom.xml for use of Sonar.
|
||||
outcome:
|
||||
- If the project uses Sonar, the probe returns one finding with OutcomePositive (1).
|
||||
- If the project does not the Sonar, the probe returns one finding with OutcomeNegative (0).
|
||||
remediation:
|
||||
effort: Medium
|
||||
text:
|
||||
- Follow the steps in https://docs.sonarsource.com/sonarqube/latest/setup-and-upgrade/overview/ to integrate Sonar into your project.
|
||||
markdown:
|
||||
- Follow the steps in https://docs.sonarsource.com/sonarqube/latest/setup-and-upgrade/overview/ to integrate CodeQL into your project.
|
60
probes/sastToolSonarInstalled/impl.go
Normal file
60
probes/sastToolSonarInstalled/impl.go
Normal file
@ -0,0 +1,60 @@
|
||||
// Copyright 2023 OpenSSF Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// nolint:stylecheck
|
||||
package sastToolSonarInstalled
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
"github.com/ossf/scorecard/v4/probes/internal/utils/uerror"
|
||||
)
|
||||
|
||||
//go:embed *.yml
|
||||
var fs embed.FS
|
||||
|
||||
const Probe = "sastToolSonarInstalled"
|
||||
|
||||
func Run(raw *checker.RawResults) ([]finding.Finding, string, error) {
|
||||
if raw == nil {
|
||||
return nil, "", fmt.Errorf("%w: raw", uerror.ErrNil)
|
||||
}
|
||||
|
||||
r := raw.SASTResults
|
||||
|
||||
for _, wf := range r.Workflows {
|
||||
wf := wf
|
||||
if wf.Type != checker.SonarWorkflow {
|
||||
continue
|
||||
}
|
||||
loc := wf.File.Location()
|
||||
f, err := finding.NewWith(fs, Probe,
|
||||
"SAST tool installed: Sonar", loc,
|
||||
finding.OutcomePositive)
|
||||
if err != nil {
|
||||
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||
}
|
||||
return []finding.Finding{*f}, Probe, nil
|
||||
}
|
||||
f, err := finding.NewWith(fs, Probe,
|
||||
"Sonar tool not installed", nil,
|
||||
finding.OutcomeNegative)
|
||||
if err != nil {
|
||||
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||
}
|
||||
return []finding.Finding{*f}, Probe, nil
|
||||
}
|
100
probes/sastToolSonarInstalled/impl_test.go
Normal file
100
probes/sastToolSonarInstalled/impl_test.go
Normal file
@ -0,0 +1,100 @@
|
||||
// Copyright 2023 OpenSSF Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// nolint:stylecheck
|
||||
package sastToolSonarInstalled
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
)
|
||||
|
||||
func Test_Run(t *testing.T) {
|
||||
t.Parallel()
|
||||
// nolint:govet
|
||||
tests := []struct {
|
||||
name string
|
||||
raw *checker.RawResults
|
||||
outcomes []finding.Outcome
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "sonar present",
|
||||
err: nil,
|
||||
raw: &checker.RawResults{
|
||||
SASTResults: checker.SASTData{
|
||||
Workflows: []checker.SASTWorkflow{
|
||||
{
|
||||
Type: checker.CodeQLWorkflow,
|
||||
},
|
||||
{
|
||||
Type: checker.SonarWorkflow,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sonar not present",
|
||||
err: nil,
|
||||
raw: &checker.RawResults{
|
||||
SASTResults: checker.SASTData{
|
||||
Workflows: []checker.SASTWorkflow{
|
||||
{
|
||||
Type: checker.CodeQLWorkflow,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomeNegative,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt // Re-initializing variable so it is not changed while executing the closure below
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
findings, s, err := Run(tt.raw)
|
||||
if !cmp.Equal(tt.err, err, cmpopts.EquateErrors()) {
|
||||
t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(tt.err, err, cmpopts.EquateErrors()))
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if diff := cmp.Diff(Probe, s); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(len(tt.outcomes), len(findings)); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
for i := range tt.outcomes {
|
||||
outcome := &tt.outcomes[i]
|
||||
f := &findings[i]
|
||||
if diff := cmp.Diff(*outcome, f.Outcome); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user