Skip to content

Commit

Permalink
🌱 convert vulnerabilities check to probe (ossf#3487)
Browse files Browse the repository at this point in the history
* 🌱 convert vulnerabilities check to probe

Signed-off-by: AdamKorcz <[email protected]>

* rename probe + nits

Signed-off-by: AdamKorcz <[email protected]>

* edit def.yml

Signed-off-by: AdamKorcz <[email protected]>

* Add vuln ID dynamically to def.yml

Signed-off-by: AdamKorcz <[email protected]>

* Elaborate the purpose of test data in unit test

Signed-off-by: AdamKorcz <[email protected]>

* Move logging out of loop and change logic of negativeFindings()

Signed-off-by: AdamKorcz <[email protected]>

* preserve number of vulns found in output

Signed-off-by: AdamKorcz <[email protected]>

* Preserve grouping of vulns

Signed-off-by: AdamKorcz <[email protected]>

* fix linter issues

Signed-off-by: AdamKorcz <[email protected]>

* Add remediation data

Signed-off-by: AdamKorcz <[email protected]>

* use checker.LogFindings()

Signed-off-by: AdamKorcz <[email protected]>

---------

Signed-off-by: AdamKorcz <[email protected]>
  • Loading branch information
AdamKorcz authored Oct 25, 2023
1 parent f2bbd0a commit de022da
Show file tree
Hide file tree
Showing 8 changed files with 368 additions and 62 deletions.
5 changes: 2 additions & 3 deletions checks/evaluation/finding.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,9 @@ func negativeFindings(findings []finding.Finding) []finding.Finding {
var ff []finding.Finding
for i := range findings {
f := &findings[i]
if f.Outcome == finding.OutcomePositive {
continue
if f.Outcome == finding.OutcomeNegative {
ff = append(ff, *f)
}
ff = append(ff, *f)
}
return ff
}
42 changes: 17 additions & 25 deletions checks/evaluation/vulnerabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,45 +16,37 @@ package evaluation

import (
"fmt"
"strings"

"github.com/google/osv-scanner/pkg/grouper"

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

// Vulnerabilities applies the score policy for the Vulnerabilities check.
func Vulnerabilities(name string, dl checker.DetailLogger,
r *checker.VulnerabilitiesData,
func Vulnerabilities(name string,
findings []finding.Finding,
dl checker.DetailLogger,
) checker.CheckResult {
if r == nil {
e := sce.WithMessage(sce.ErrScorecardInternal, "empty raw data")
return checker.CreateRuntimeErrorResult(name, e)
expectedProbes := []string{
hasOSVVulnerabilities.Probe,
}

aliasVulnerabilities := []grouper.IDAliases{}
for _, vuln := range r.Vulnerabilities {
aliasVulnerabilities = append(aliasVulnerabilities, grouper.IDAliases(vuln))
if !finding.UniqueProbesEqual(findings, expectedProbes) {
e := sce.WithMessage(sce.ErrScorecardInternal, "invalid probe results")
return checker.CreateRuntimeErrorResult(name, e)
}

IDs := grouper.Group(aliasVulnerabilities)
score := checker.MaxResultScore - len(IDs)
vulnsFound := negativeFindings(findings)
numVulnsFound := len(vulnsFound)
checker.LogFindings(vulnsFound, dl)

score := checker.MaxResultScore - numVulnsFound

if score < checker.MinResultScore {
score = checker.MinResultScore
}

if len(IDs) > 0 {
for _, v := range IDs {
dl.Warn(&checker.LogMessage{
Text: fmt.Sprintf("Project is vulnerable to: %s", strings.Join(v.IDs, " / ")),
})
}

return checker.CreateResultWithScore(name,
fmt.Sprintf("%v existing vulnerabilities detected", len(IDs)), score)
}

return checker.CreateMaxScoreResult(name, "no vulnerabilities detected")
return checker.CreateResultWithScore(name,
fmt.Sprintf("%v existing vulnerabilities detected", numVulnsFound), score)
}
119 changes: 88 additions & 31 deletions checks/evaluation/vulnerabilities_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,62 +17,119 @@ package evaluation
import (
"testing"

"github.com/ossf/scorecard/v4/checker"
"github.com/ossf/scorecard/v4/clients"
sce "github.com/ossf/scorecard/v4/errors"
"github.com/ossf/scorecard/v4/finding"
scut "github.com/ossf/scorecard/v4/utests"
)

// TestVulnerabilities tests the vulnerabilities checker.
func TestVulnerabilities(t *testing.T) {
t.Parallel()
//nolint
type args struct {
name string
r *checker.VulnerabilitiesData
}
tests := []struct {
name string
args args
want checker.CheckResult
findings []finding.Finding
result scut.TestReturn
expected []struct {
lineNumber uint
}
}{
{
name: "no vulnerabilities",
args: args{
name: "vulnerabilities_test.go",
r: &checker.VulnerabilitiesData{
Vulnerabilities: []clients.Vulnerability{},
findings: []finding.Finding {
{
Probe: "hasOSVVulnerabilities",
Outcome: finding.OutcomePositive,
},
},
want: checker.CheckResult{
result: scut.TestReturn{
Score: 10,
},
},
{
name: "one vulnerability",
args: args{
name: "vulnerabilities_test.go",
r: &checker.VulnerabilitiesData{
Vulnerabilities: []clients.Vulnerability{
{
ID: "CVE-2019-1234",
},
},
name: "three vulnerabilities",
findings: []finding.Finding {
{
Probe: "hasOSVVulnerabilities",
Outcome: finding.OutcomeNegative,
},
{
Probe: "hasOSVVulnerabilities",
Outcome: finding.OutcomeNegative,
},
{
Probe: "hasOSVVulnerabilities",
Outcome: finding.OutcomeNegative,
},
},
want: checker.CheckResult{
Score: 9,
result: scut.TestReturn{
Score: 7,
NumberOfWarn: 3,
},
},
{
name: "one vulnerability",
args: args{
name: "vulnerabilities_test.go",
name: "twelve vulnerabilities to check that score is not less than 0",
findings: []finding.Finding {
{
Probe: "hasOSVVulnerabilities",
Outcome: finding.OutcomeNegative,
},
{
Probe: "hasOSVVulnerabilities",
Outcome: finding.OutcomeNegative,
},
{
Probe: "hasOSVVulnerabilities",
Outcome: finding.OutcomeNegative,
},
{
Probe: "hasOSVVulnerabilities",
Outcome: finding.OutcomeNegative,
},
{
Probe: "hasOSVVulnerabilities",
Outcome: finding.OutcomeNegative,
},
{
Probe: "hasOSVVulnerabilities",
Outcome: finding.OutcomeNegative,
},
{
Probe: "hasOSVVulnerabilities",
Outcome: finding.OutcomeNegative,
},
{
Probe: "hasOSVVulnerabilities",
Outcome: finding.OutcomeNegative,
},
{
Probe: "hasOSVVulnerabilities",
Outcome: finding.OutcomeNegative,
},
{
Probe: "hasOSVVulnerabilities",
Outcome: finding.OutcomeNegative,
},
{
Probe: "hasOSVVulnerabilities",
Outcome: finding.OutcomeNegative,
},
{
Probe: "hasOSVVulnerabilities",
Outcome: finding.OutcomeNegative,
},
},
result: scut.TestReturn{
Score: 0,
NumberOfWarn: 12,
},
want: checker.CheckResult{
},
{
name: "invalid findings",
findings: []finding.Finding {},
result: scut.TestReturn{
Score: -1,
Error: sce.ErrScorecardInternal,
},
},
}
Expand All @@ -81,9 +138,9 @@ func TestVulnerabilities(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
dl := scut.TestDetailLogger{}
res := Vulnerabilities(tt.args.name, &dl, tt.args.r)
if res.Score != tt.want.Score {
t.Errorf("Vulnerabilities() = %v, want %v", res.Score, tt.want.Score)
got := Vulnerabilities(tt.name, tt.findings, &dl)
if !scut.ValidateTestReturn(t, tt.name, &tt.result, &got, &dl) {
t.Errorf("got %v, expected %v", got, tt.result)
}
})
}
Expand Down
14 changes: 11 additions & 3 deletions checks/vulnerabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import (
"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/probes"
"github.com/ossf/scorecard/v4/probes/zrunner"
)

// CheckVulnerabilities is the registered name for the OSV check.
Expand All @@ -45,9 +47,15 @@ func Vulnerabilities(c *checker.CheckRequest) checker.CheckResult {
}

// Set the raw results.
if c.RawResults != nil {
c.RawResults.VulnerabilitiesResults = rawData
pRawResults := getRawResults(c)
pRawResults.VulnerabilitiesResults = rawData

// Evaluate the probes.
findings, err := zrunner.Run(pRawResults, probes.Vulnerabilities)
if err != nil {
e := sce.WithMessage(sce.ErrScorecardInternal, err.Error())
return checker.CreateRuntimeErrorResult(CheckVulnerabilities, e)
}

return evaluation.Vulnerabilities(CheckVulnerabilities, c.Dlogger, &rawData)
return evaluation.Vulnerabilities(CheckVulnerabilities, findings, c.Dlogger)
}
4 changes: 4 additions & 0 deletions probes/entries.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"github.com/ossf/scorecard/v4/probes/hasFSFOrOSIApprovedLicense"
"github.com/ossf/scorecard/v4/probes/hasLicenseFile"
"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/securityPolicyContainsLinks"
"github.com/ossf/scorecard/v4/probes/securityPolicyContainsText"
Expand Down Expand Up @@ -91,6 +92,9 @@ var (
Contributors = []ProbeImpl{
contributorsFromOrgOrCompany.Run,
}
Vulnerabilities = []ProbeImpl{
hasOSVVulnerabilities.Run,
}
)

//nolint:gochecknoinits
Expand Down
33 changes: 33 additions & 0 deletions probes/hasOSVVulnerabilities/def.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# 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: hasOSVVulnerabilities
short: Check whether the project has known vulnerabilities
motivation: >
This check determines whether the project has open, unfixed vulnerabilities in its own codebase or its dependencies using the OSV (Open Source Vulnerabilities) service. An open vulnerability may be exploited by attackers and should be fixed as soon as possible.
implementation: >
The implementation fetches data from OSV.dev about the project which shows whether a given project has known, unfixed vulnerabilities. The implementation uses the number of known, unfixed vulnerabilities to score.
outcome:
- The probe returns one negative outcome for each vulnerability found in OSV.
- If there are no known vulnerabilities from the raw results, the probe returns one positive outcome.
remediation:
effort: High
text:
- Fix the ${{ metadata.osvid }} by following information from https://osv.dev/${{ metadata.osvid }}.
- If the vulnerability is in a dependency, update the dependency to a non-vulnerable version. If no update is available, consider whether to remove the dependency.
- If you believe the vulnerability does not affect your project, the vulnerability can be ignored. To ignore, create an osv-scanner.toml file next to the dependency manifest (e.g. package-lock.json) and specify the ID to ignore and reason. Details on the structure of osv-scanner.toml can be found on OSV-Scanner repository.
markdown:
- Fix the ${{ metadata.osvid }} by following information from [OSV](https://osv.dev/${{ metadata.osvid }}).
- If the vulnerability is in a dependency, update the dependency to a non-vulnerable version. If no update is available, consider whether to remove the dependency.
- If you believe the vulnerability does not affect your project, the vulnerability can be ignored. To ignore, create an osv-scanner.toml ([example](https://github.com/google/osv.dev/blob/eb99b02ec8895fe5b87d1e76675ddad79a15f817/vulnfeeds/osv-scanner.toml)) file next to the dependency manifest (e.g. package-lock.json) and specify the ID to ignore and reason. Details on the structure of osv-scanner.toml can be found on [OSV-Scanner repository](https://github.com/google/osv-scanner#ignore-vulnerabilities-by-id).
76 changes: 76 additions & 0 deletions probes/hasOSVVulnerabilities/impl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// 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 hasOSVVulnerabilities

import (
"embed"
"fmt"
"strings"

"github.com/google/osv-scanner/pkg/grouper"

"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 = "hasOSVVulnerabilities"

func Run(raw *checker.RawResults) ([]finding.Finding, string, error) {
if raw == nil {
return nil, "", fmt.Errorf("%w: raw", uerror.ErrNil)
}

var findings []finding.Finding

// if no vulns were found
if len(raw.VulnerabilitiesResults.Vulnerabilities) == 0 {
f, err := finding.NewWith(fs, Probe,
"Project does not contain OSV vulnerabilities", nil,
finding.OutcomePositive)
if err != nil {
return nil, Probe, fmt.Errorf("create finding: %w", err)
}
findings = append(findings, *f)
return findings, Probe, nil
}

aliasVulnerabilities := []grouper.IDAliases{}
for _, vuln := range raw.VulnerabilitiesResults.Vulnerabilities {
aliasVulnerabilities = append(aliasVulnerabilities, grouper.IDAliases(vuln))
}

IDs := grouper.Group(aliasVulnerabilities)

for _, vuln := range IDs {
f, err := finding.NewWith(fs, Probe,
"Project contains OSV vulnerabilities", nil,
finding.OutcomeNegative)
if err != nil {
return nil, Probe, fmt.Errorf("create finding: %w", err)
}
f = f.WithMessage(fmt.Sprintf("Project is vulnerable to: %s",
strings.Join(vuln.IDs, " / ")))
f = f.WithRemediationMetadata(map[string]string{
"osvid": strings.Join(vuln.IDs[:], ","),
})
findings = append(findings, *f)
}
return findings, Probe, nil
}
Loading

0 comments on commit de022da

Please sign in to comment.