Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🌱 Convert SAST check to probes #3571

Merged
merged 15 commits into from
Nov 7, 2023
Merged
35 changes: 35 additions & 0 deletions checker/raw_result.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type RawResults struct {
Metadata MetadataData
PackagingResults PackagingData
PinningDependenciesResults PinningDependenciesData
SASTResults SASTData
SecurityPolicyResults SecurityPolicyData
SignedReleasesResults SignedReleasesData
TokenPermissionsResults TokenPermissionsData
Expand Down Expand Up @@ -226,6 +227,40 @@ type SecurityPolicyFile struct {
File File
}

// SASTData contains the raw results
// for the SAST check.
type SASTData struct {
Workflows []SASTWorkflow
spencerschrock marked this conversation as resolved.
Show resolved Hide resolved
Commits []SASTCommit
NumWorkflows int
spencerschrock marked this conversation as resolved.
Show resolved Hide resolved
}

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"
)
spencerschrock marked this conversation as resolved.
Show resolved Hide resolved

// 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 {
Expand Down
167 changes: 167 additions & 0 deletions checks/evaluation/sast.go
Original file line number Diff line number Diff line change
@@ -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)
}

Check warning on line 77 in checks/evaluation/sast.go

View check run for this annotation

Codecov / codecov/patch

checks/evaluation/sast.go#L74-L77

Added lines #L74 - L77 were not covered by tests

// 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"))

Check warning on line 101 in checks/evaluation/sast.go

View check run for this annotation

Codecov / codecov/patch

checks/evaluation/sast.go#L100-L101

Added lines #L100 - L101 were not covered by tests
}
}

// Sast inconclusive.
if codeQlScore != checker.InconclusiveResultScore {
if codeQlScore == checker.MaxResultScore {
return checker.CreateMaxScoreResult(name, "SAST tool detected: CodeQL")
}

Check warning on line 109 in checks/evaluation/sast.go

View check run for this annotation

Codecov / codecov/patch

checks/evaluation/sast.go#L108-L109

Added lines #L108 - L109 were not covered by tests
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")
}

Check warning on line 117 in checks/evaluation/sast.go

View check run for this annotation

Codecov / codecov/patch

checks/evaluation/sast.go#L114-L117

Added lines #L114 - L117 were not covered by tests

return checker.CreateResultWithScore(name,
checker.NormalizeReason("SAST tool is not run on all commits", sastScore), sastScore)

Check warning on line 120 in checks/evaluation/sast.go

View check run for this annotation

Codecov / codecov/patch

checks/evaluation/sast.go#L119-L120

Added lines #L119 - L120 were not covered by tests
}

// Should never happen.
return checker.CreateRuntimeErrorResult(name, sce.WithMessage(sce.ErrScorecardInternal, "contact team"))

Check warning on line 124 in checks/evaluation/sast.go

View check run for this annotation

Codecov / codecov/patch

checks/evaluation/sast.go#L124

Added line #L124 was not covered by tests
}

// 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"])

Check warning on line 145 in checks/evaluation/sast.go

View check run for this annotation

Codecov / codecov/patch

checks/evaluation/sast.go#L144-L145

Added lines #L144 - L145 were not covered by tests
}
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")

Check warning on line 165 in checks/evaluation/sast.go

View check run for this annotation

Codecov / codecov/patch

checks/evaluation/sast.go#L164-L165

Added lines #L164 - L165 were not covered by tests
}
}
153 changes: 153 additions & 0 deletions checks/evaluation/sast_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
Loading
Loading