Skip to content

Commit

Permalink
Convert SAST checks to probes
Browse files Browse the repository at this point in the history
Signed-off-by: AdamKorcz <[email protected]>
  • Loading branch information
AdamKorcz committed Oct 14, 2023
1 parent 8eaf0d7 commit 3cd75ac
Show file tree
Hide file tree
Showing 22 changed files with 1,703 additions and 553 deletions.
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
}

// SecurityPolicyData contains the raw results
// for the Security-Policy 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
}

// DangerousWorkflowType represents a type of dangerous workflow.
type SASTWorkflowType string

const (
// DangerousWorkflowScriptInjection represents a script injection.
CodeQLWorkflow SASTWorkflowType = "CodeQL"
// DangerousWorkflowUntrustedCheckout represents an untrusted checkout.
SonarWorkflow SASTWorkflowType = "Sonar"
)

// DangerousWorkflow represents a dangerous 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
// Compute the score.
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,
})
} else if f.Outcome == finding.OutcomeNegative {
sonarScore = checker.MinResultScore
dl.Warn(&checker.LogMessage{
Text: f.Message,
})
}
}
}

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

func getSASTScore(f *finding.Finding, dl checker.DetailLogger) int {
switch f.Outcome {
case finding.OutcomeNotApplicable:
dl.Warn(&checker.LogMessage{
Text: "no pull requests merged into dev branch",
})
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["totalTested"], f.Values["totalMerged"])
}
return checker.CreateProportionalScore(f.Values["totalTested"], f.Values["totalMerged"])
}

func getCodeQLScore(f *finding.Finding, dl checker.DetailLogger) int {
switch f.Outcome {
case finding.OutcomeNotApplicable:
dl.Warn(&checker.LogMessage{
Text: "no pull requests merged into dev branch",
})
return checker.InconclusiveResultScore
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:
checker.CreateProportionalScore(f.Values["totalTested"], f.Values["totalMerged"])
}
return checker.InconclusiveResultScore // reconsider this
}
134 changes: 134 additions & 0 deletions checks/evaluation/sast_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// 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) {
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 CodeCQ is installed",
findings: []finding.Finding{
{
Probe: "sastToolCodeQLInstalled",
Outcome: finding.OutcomePositive,
},
{
Probe: "sastToolRunsOnAllCommits",
Outcome: finding.OutcomePositive,
Values: map[string]int{
"totalTested": 1,
"totalMerged": 2,
},
},
{
Probe: "sastToolSonarInstalled",
Outcome: finding.OutcomePositive,
},
},
result: scut.TestReturn{
Score: 10,
NumberOfInfo: 3,
},
},
{
name: "Sonar is installed. CodeQL is not installed. Sast is OutcomeNotApplicable",
findings: []finding.Finding{
{
Probe: "sastToolCodeQLInstalled",
Outcome: finding.OutcomeNegative,
},
{
Probe: "sastToolRunsOnAllCommits",
Outcome: finding.OutcomeNotApplicable,
},
{
Probe: "sastToolSonarInstalled",
Outcome: finding.OutcomePositive,
},
},
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{
"totalTested": 1,
"totalMerged": 3,
},
},
{
Probe: "sastToolSonarInstalled",
Outcome: finding.OutcomeNegative,
},
},
result: scut.TestReturn{
Score: 3,
NumberOfWarn: 3,
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

0 comments on commit 3cd75ac

Please sign in to comment.