Add experimental check for published SBOM (#3903)

* Sbom check MVP

Signed-off-by: Allen Shearin <allen.p.shearin@gmail.com>

* PR suggestion fixes

Signed-off-by: Allen Shearin <allen.p.shearin@gmail.com>

* fix line length

Signed-off-by: Allen Shearin <allen.p.shearin@gmail.com>

* update gitlab client to check 20 latest pipelines in default branch

Signed-off-by: Allen Shearin <allen.p.shearin@gmail.com>

* correct issues

Signed-off-by: Allen Shearin <allen.p.shearin@gmail.com>

* add unit tests for sbom client code

Signed-off-by: Allen Shearin <allen.p.shearin@gmail.com>

* probe name alignment, updated evaluation tests

Signed-off-by: Allen Shearin <allen.p.shearin@gmail.com>

* consolidate probes, reuse available data sources

Signed-off-by: Allen Shearin <allen.p.shearin@gmail.com>

* add autogen doc update

Signed-off-by: Allen Shearin <allen.p.shearin@gmail.com>

* address PR comments, remove CI/CD check code

Signed-off-by: Allen Shearin <allen.p.shearin@gmail.com>

* update unit tests

Signed-off-by: Allen Shearin <allen.p.shearin@gmail.com>

* fix linting errors

Signed-off-by: Allen Shearin <allen.p.shearin@gmail.com>

* revert unnecessary changes, correct check documentation

Signed-off-by: Allen Shearin <allen.p.shearin@gmail.com>

* address PR comments

Signed-off-by: Allen Shearin <allen.p.shearin@gmail.com>

* move release lookback to data collection side

Signed-off-by: Allen Shearin <allen.p.shearin@gmail.com>

---------

Signed-off-by: Allen Shearin <allen.p.shearin@gmail.com>
This commit is contained in:
Allen Shearin 2024-05-17 12:16:54 -06:00 committed by GitHub
parent 956d7c3895
commit 8de90207bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1140 additions and 0 deletions

View File

@ -37,6 +37,7 @@ type RawResults struct {
DependencyUpdateToolResults DependencyUpdateToolData
FuzzingResults FuzzingData
LicenseResults LicenseData
SBOMResults SBOMData
MaintainedResults MaintainedData
Metadata MetadataData
PackagingResults PackagingData
@ -168,6 +169,18 @@ type LicenseData struct {
LicenseFiles []LicenseFile
}
// SBOM details.
type SBOM struct {
Name string // SBOM Filename
File File // SBOM File Object
}
// SBOMData contains the raw results for the SBOM check.
// Some repos may have more than one SBOM.
type SBOMData struct {
SBOMFiles []SBOM
}
// CodeReviewData contains the raw results
// for the Code-Review check.
type CodeReviewData struct {

View File

@ -38,6 +38,7 @@ func getAll(overrideExperimental bool) checker.CheckNameToFnMap {
if _, experimental := os.LookupEnv("SCORECARD_EXPERIMENTAL"); !experimental {
// TODO: remove this check when v6 is released
delete(possibleChecks, CheckWebHooks)
delete(possibleChecks, CheckSBOM)
}
return possibleChecks

75
checks/evaluation/sbom.go Normal file
View File

@ -0,0 +1,75 @@
// Copyright 2024 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/v5/checker"
sce "github.com/ossf/scorecard/v5/errors"
"github.com/ossf/scorecard/v5/finding"
"github.com/ossf/scorecard/v5/probes/hasReleaseSBOM"
"github.com/ossf/scorecard/v5/probes/hasSBOM"
)
// SBOM applies the score policy for the SBOM check.
func SBOM(name string,
findings []finding.Finding,
dl checker.DetailLogger,
) checker.CheckResult {
// We have 4 unique probes, each should have a finding.
expectedProbes := []string{
hasSBOM.Probe,
hasReleaseSBOM.Probe,
}
if !finding.UniqueProbesEqual(findings, expectedProbes) {
e := sce.WithMessage(sce.ErrScorecardInternal, "invalid probe results")
return checker.CreateRuntimeErrorResult(name, e)
}
// Compute the score.
score := 0
m := make(map[string]bool)
var logLevel checker.DetailType
for i := range findings {
f := &findings[i]
switch f.Outcome {
case finding.OutcomeTrue:
logLevel = checker.DetailInfo
switch f.Probe {
case hasSBOM.Probe:
score += scoreProbeOnce(f.Probe, m, 5)
case hasReleaseSBOM.Probe:
score += scoreProbeOnce(f.Probe, m, 5)
}
case finding.OutcomeFalse:
logLevel = checker.DetailWarn
default:
continue // for linting
}
checker.LogFinding(dl, f, logLevel)
}
_, defined := m[hasSBOM.Probe]
if !defined {
return checker.CreateMinScoreResult(name, "SBOM file not detected")
}
_, defined = m[hasReleaseSBOM.Probe]
if defined {
return checker.CreateMaxScoreResult(name, "SBOM file found in release artifacts")
}
return checker.CreateResultWithScore(name, "SBOM file found in project", score)
}

View File

@ -0,0 +1,95 @@
// Copyright 2024 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/v5/checker"
"github.com/ossf/scorecard/v5/finding"
scut "github.com/ossf/scorecard/v5/utests"
)
func TestSBOM(t *testing.T) {
t.Parallel()
tests := []struct {
name string
findings []finding.Finding
result scut.TestReturn
}{
{
name: "No SBOM. Min Score",
findings: []finding.Finding{
{
Probe: "hasSBOM",
Outcome: finding.OutcomeFalse,
},
{
Probe: "hasReleaseSBOM",
Outcome: finding.OutcomeFalse,
},
},
result: scut.TestReturn{
Score: checker.MinResultScore,
NumberOfInfo: 0,
NumberOfWarn: 2,
},
},
{
name: "Only Source SBOM. Half Points",
findings: []finding.Finding{
{
Probe: "hasSBOM",
Outcome: finding.OutcomeTrue,
},
{
Probe: "hasReleaseSBOM",
Outcome: finding.OutcomeFalse,
},
},
result: scut.TestReturn{
Score: 5,
NumberOfInfo: 1,
NumberOfWarn: 1,
},
},
{
name: "SBOM in Release Assets. Max score",
findings: []finding.Finding{
{
Probe: "hasSBOM",
Outcome: finding.OutcomeTrue,
},
{
Probe: "hasReleaseSBOM",
Outcome: finding.OutcomeTrue,
},
},
result: scut.TestReturn{
Score: checker.MaxResultScore,
NumberOfInfo: 2,
NumberOfWarn: 0,
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
dl := scut.TestDetailLogger{}
got := SBOM(tt.name, tt.findings, &dl)
scut.ValidateTestReturn(t, tt.name, &tt.result, &got, &dl)
})
}
}

106
checks/raw/sbom.go Normal file
View File

@ -0,0 +1,106 @@
// Copyright 2024 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"
"regexp"
"github.com/ossf/scorecard/v5/checker"
"github.com/ossf/scorecard/v5/clients"
"github.com/ossf/scorecard/v5/finding"
)
var (
reRootFile = regexp.MustCompile(`^[^.]([^//]*)$`)
reSBOMFile = regexp.MustCompile(
`(?i).+\.(cdx.json|cdx.xml|spdx|spdx.json|spdx.xml|spdx.y[a?]ml|spdx.rdf|spdx.rdf.xml)`,
)
)
const releaseLookBack = 5
// SBOM retrieves the raw data for the SBOM check.
func SBOM(c *checker.CheckRequest) (checker.SBOMData, error) {
var results checker.SBOMData
releases, lerr := c.RepoClient.ListReleases()
if lerr != nil {
return results, fmt.Errorf("RepoClient.ListReleases: %w", lerr)
}
results.SBOMFiles = append(results.SBOMFiles, checkSBOMReleases(releases)...)
// Look for SBOMs in source
repoFiles, err := c.RepoClient.ListFiles(func(file string) (bool, error) {
return reSBOMFile.MatchString(file) && reRootFile.MatchString(file), nil
})
if err != nil {
return results, fmt.Errorf("error during ListFiles: %w", err)
}
results.SBOMFiles = append(results.SBOMFiles, checkSBOMSource(repoFiles)...)
return results, nil
}
func checkSBOMReleases(releases []clients.Release) []checker.SBOM {
var foundSBOMs []checker.SBOM
for i := range releases {
if i >= releaseLookBack {
break
}
v := releases[i]
for _, link := range v.Assets {
if !reSBOMFile.MatchString(link.Name) {
continue
}
foundSBOMs = append(foundSBOMs,
checker.SBOM{
File: checker.File{
Path: link.URL,
Type: finding.FileTypeURL,
},
Name: link.Name,
})
// Only want one sbom from each release
break
}
}
return foundSBOMs
}
func checkSBOMSource(fileList []string) []checker.SBOM {
var foundSBOMs []checker.SBOM
for _, file := range fileList {
// TODO: parse matching file contents to determine schema & version
foundSBOMs = append(foundSBOMs,
checker.SBOM{
File: checker.File{
Path: file,
Type: finding.FileTypeSource,
},
Name: file,
})
}
return foundSBOMs
}

129
checks/raw/sbom_test.go Normal file
View File

@ -0,0 +1,129 @@
// Copyright 2024 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 (
"context"
"testing"
"github.com/golang/mock/gomock"
"github.com/google/go-cmp/cmp"
"github.com/ossf/scorecard/v5/checker"
"github.com/ossf/scorecard/v5/clients"
mockrepo "github.com/ossf/scorecard/v5/clients/mockclients"
"github.com/ossf/scorecard/v5/finding"
scut "github.com/ossf/scorecard/v5/utests"
)
func TestSbom(t *testing.T) {
t.Parallel()
tests := []struct {
name string
releases []clients.Release
files []string
err error
expected checker.SBOMData
}{
{
name: "With Sbom in release artifacts",
releases: []clients.Release{
{
Assets: []clients.ReleaseAsset{
{
Name: "test-sbom.cdx.json",
URL: "https://this.url",
},
},
},
},
files: []string{},
expected: checker.SBOMData{
SBOMFiles: []checker.SBOM{
{
Name: "test-sbom.cdx.json",
File: checker.File{
Type: finding.FileTypeURL,
Path: "https://this.url",
},
},
},
},
err: nil,
},
{
name: "With Sbom in source",
releases: []clients.Release{},
files: []string{"test-sbom.spdx.json"},
err: nil,
expected: checker.SBOMData{
SBOMFiles: []checker.SBOM{
{
Name: "test-sbom.spdx.json",
File: checker.File{
Type: finding.FileTypeSource,
Path: "test-sbom.spdx.json",
},
},
},
},
},
{
name: "Without SBOM",
releases: []clients.Release{},
files: []string{},
expected: checker.SBOMData{},
err: nil,
},
}
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)
mockRepo := mockrepo.NewMockRepoClient(ctrl)
mockRepo.EXPECT().ListReleases().DoAndReturn(
func() ([]clients.Release, error) {
if tt.err != nil {
return nil, tt.err
}
return tt.releases, tt.err
},
).MaxTimes(1)
mockRepo.EXPECT().ListFiles(gomock.Any()).DoAndReturn(func(predicate func(string) (bool, error)) ([]string, error) {
return tt.files, nil
}).AnyTimes()
dl := scut.TestDetailLogger{}
req := checker.CheckRequest{
RepoClient: mockRepo,
Ctx: context.Background(),
Dlogger: &dl,
}
res, err := SBOM(&req)
if tt.err != nil {
if err == nil {
t.Fatalf("Expected error %v, got nil", tt.err)
}
}
if !cmp.Equal(res, tt.expected) {
t.Errorf("Expected %v, got %v for %v", tt.expected, res, tt.name)
}
})
}
}

71
checks/sbom.go Normal file
View File

@ -0,0 +1,71 @@
// Copyright 2024 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 checks
import (
"os"
"github.com/ossf/scorecard/v5/checker"
"github.com/ossf/scorecard/v5/checks/evaluation"
"github.com/ossf/scorecard/v5/checks/raw"
sce "github.com/ossf/scorecard/v5/errors"
"github.com/ossf/scorecard/v5/probes"
"github.com/ossf/scorecard/v5/probes/zrunner"
)
// SBOM is the registered name for SBOM.
const CheckSBOM = "SBOM"
//nolint:gochecknoinits
func init() {
if err := registerCheck(CheckSBOM, SBOM, nil); err != nil {
// this should never happen
panic(err)
}
}
// SBOM runs SBOM check.
func SBOM(c *checker.CheckRequest) checker.CheckResult {
_, enabled := os.LookupEnv("SCORECARD_EXPERIMENTAL")
if !enabled {
c.Dlogger.Warn(&checker.LogMessage{
Text: "SCORECARD_EXPERIMENTAL is not set, not running the SBOM check",
})
e := sce.WithMessage(sce.ErrUnsupportedCheck, "SCORECARD_EXPERIMENTAL is not set, not running the SBOM check")
return checker.CreateRuntimeErrorResult(CheckSBOM, e)
}
rawData, err := raw.SBOM(c)
if err != nil {
e := sce.WithMessage(sce.ErrScorecardInternal, err.Error())
return checker.CreateRuntimeErrorResult(CheckSBOM, e)
}
// Set the raw results.
pRawResults := getRawResults(c)
pRawResults.SBOMResults = rawData
// Evaluate the probes.
findings, err := zrunner.Run(pRawResults, probes.SBOM)
if err != nil {
e := sce.WithMessage(sce.ErrScorecardInternal, err.Error())
return checker.CreateRuntimeErrorResult(CheckSBOM, e)
}
ret := evaluation.SBOM(CheckSBOM, findings, c.Dlogger)
ret.Findings = findings
return ret
}

116
checks/sbom_test.go Normal file
View File

@ -0,0 +1,116 @@
// Copyright 2024 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 checks
import (
"context"
"testing"
"github.com/golang/mock/gomock"
"github.com/ossf/scorecard/v5/checker"
"github.com/ossf/scorecard/v5/clients"
mockrepo "github.com/ossf/scorecard/v5/clients/mockclients"
scut "github.com/ossf/scorecard/v5/utests"
)
func TestSbom(t *testing.T) {
tests := []struct {
name string
releases []clients.Release
files []string
err error
expected scut.TestReturn
}{
{
name: "With Sbom in release artifacts",
releases: []clients.Release{
{
Assets: []clients.ReleaseAsset{
{
Name: "test-sbom.cdx.json",
URL: "https://this.url",
},
},
},
},
files: []string{},
expected: scut.TestReturn{
Score: checker.MaxResultScore,
NumberOfInfo: 2,
NumberOfWarn: 0,
},
err: nil,
},
{
name: "With Sbom in source",
releases: []clients.Release{},
files: []string{"test-sbom.spdx.json"},
err: nil,
expected: scut.TestReturn{
Score: 5,
NumberOfInfo: 1,
NumberOfWarn: 1,
},
},
{
name: "Without SBOM",
releases: []clients.Release{},
files: []string{},
expected: scut.TestReturn{
Score: checker.MinResultScore,
NumberOfInfo: 0,
NumberOfWarn: 2,
},
err: nil,
},
}
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.Setenv("SCORECARD_EXPERIMENTAL", "true")
ctrl := gomock.NewController(t)
mockRepo := mockrepo.NewMockRepoClient(ctrl)
mockRepo.EXPECT().ListReleases().DoAndReturn(
func() ([]clients.Release, error) {
if tt.err != nil {
return nil, tt.err
}
return tt.releases, tt.err
},
).MaxTimes(1)
mockRepo.EXPECT().ListFiles(gomock.Any()).DoAndReturn(func(predicate func(string) (bool, error)) ([]string, error) {
return tt.files, nil
}).AnyTimes()
dl := scut.TestDetailLogger{}
req := checker.CheckRequest{
RepoClient: mockRepo,
Ctx: context.Background(),
Dlogger: &dl,
}
res := SBOM(&req)
if tt.err != nil {
if res.Error == nil {
t.Fatalf("Expected error %v, got nil", tt.err)
}
}
scut.ValidateTestReturn(t, tt.name, &tt.expected, &res, &dl)
})
}
}

View File

@ -126,6 +126,7 @@ func (client *Client) InitRepo(inputRepo clients.Repo, commitSHA string, commitD
// Setup licensesHandler.
client.licenses.init(client.ctx, client.repourl)
return nil
}

View File

@ -541,6 +541,38 @@ is therefore not a definitive indication that the project is at risk.
**Remediation steps**
- Run CodeQL checks in your CI/CD by following the instructions [here](https://github.com/github/codeql-action#usage).
## SBOM
Risk: `Medium` (possible inaccurate reporting of dependencies/vulnerabilities)
This check tries to determine if the project maintains a Software Bill of Materials (SBOM)
either as a file in the source or a release artifact.
An SBOM can give users information about what dependencies your project has which
allows them to identify vulnerabilities in the software supply chain.
Standards to be used during checks;
- OSSF SBOM Everywhere SIG naming and directory conventions:
- <https://github.com/ossf/SBOM-everywhere/blob/main/reference/SBOM_naming.md#consistent-naming-conventions>
This check currently looks for the existence of an SBOM in the
source of a project and as a pipeline or release artifact.
An SBOM Exists (one or more) (5/10 points):
- Any SBOM found counts for this test either in source. pipeline or release.
- A SBOM stored with your source code is not ideal, but is a good first step.
\* It is recommended to publish with your release artifacts.
An SBOM is published as a release artifact (5/10 points):
- This is the preferred way to store an SBOM, and will be awarded full points.
- Checks release artifacts for an SBOM file matching established standards
**Remediation steps**
- For Gitlab, see more information [here](https://docs.gitlab.com/ee/user/application_security/dependency_scanning/index.html#cyclonedx-software-bill-of-materials).
- For GitHub, see more information [here](https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-supply-chain-security).
- Alternatively, there are other tools available to generate [CycloneDX](https://cyclonedx.org/tool-center/) and [SPDX](https://spdx.dev/use/tools/) SBOMs.
## Security-Policy
Risk: `Medium` (possible insecure reporting of vulnerabilities)

View File

@ -558,6 +558,46 @@ checks:
- >-
Run CodeQL checks in your CI/CD by following the instructions
[here](https://github.com/github/codeql-action#usage).
SBOM:
risk: Medium
short: Determines if the project maintains a Software Bill of Materials.
repos: GitHub, Gitlab
tags: supply-chain, security, vulnerabilities, dependencies, SBOM
description: |
Risk: `Medium` (possible inaccurate reporting of dependencies/vulnerabilities)
This check tries to determine if the project maintains a Software Bill of Materials (SBOM)
either as a file in the source or a release artifact.
An SBOM can give users information about what dependencies your project has which
allows them to identify vulnerabilities in the software supply chain.
Standards to be used during checks;
- OSSF SBOM Everywhere SIG naming and directory conventions:
- <https://github.com/ossf/SBOM-everywhere/blob/main/reference/SBOM_naming.md#consistent-naming-conventions>
This check currently looks for the existence of an SBOM in the
source of a project and as a pipeline or release artifact.
An SBOM Exists (one or more) (5/10 points):
- Any SBOM found counts for this test either in source. pipeline or release.
- A SBOM stored with your source code is not ideal, but is a good first step.
\* It is recommended to publish with your release artifacts.
An SBOM is published as a release artifact (5/10 points):
- This is the preferred way to store an SBOM, and will be awarded full points.
- Checks release artifacts for an SBOM file matching established standards
remediation:
- >-
For Gitlab, see more information
[here](https://docs.gitlab.com/ee/user/application_security/dependency_scanning/index.html#cyclonedx-software-bill-of-materials).
- >-
For GitHub, see more information
[here](https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-supply-chain-security).
- >-
Alternatively, there are other tools available to generate [CycloneDX](https://cyclonedx.org/tool-center/) and [SPDX](https://spdx.dev/use/tools/) SBOMs.
Security-Policy:
risk: Medium
short: Determines if the project has published a security policy.

View File

@ -40,6 +40,7 @@ const (
Packaging CheckName = "Packaging"
PinnedDependencies CheckName = "Pinned-Dependencies"
SAST CheckName = "SAST"
SBOM CheckName = "SBOM"
SecurityPolicy CheckName = "Security-Policy"
SignedReleases CheckName = "Signed-Releases"
TokenPermissions CheckName = "Token-Permissions"

View File

@ -345,6 +345,12 @@ func assignRawData(probeCheckName string, request *checker.CheckRequest, ret *Sc
return sce.WithMessage(sce.ErrScorecardInternal, err.Error())
}
ret.RawResults.SASTResults = rawData
case checks.CheckSBOM:
rawData, err := raw.SBOM(request)
if err != nil {
return sce.WithMessage(sce.ErrScorecardInternal, err.Error())
}
ret.RawResults.SBOMResults = rawData
case checks.CheckSecurityPolicy:
rawData, err := raw.SecurityPolicy(request)
if err != nil {

View File

@ -39,6 +39,8 @@ import (
"github.com/ossf/scorecard/v5/probes/hasOpenSSFBadge"
"github.com/ossf/scorecard/v5/probes/hasPermissiveLicense"
"github.com/ossf/scorecard/v5/probes/hasRecentCommits"
"github.com/ossf/scorecard/v5/probes/hasReleaseSBOM"
"github.com/ossf/scorecard/v5/probes/hasSBOM"
"github.com/ossf/scorecard/v5/probes/hasUnverifiedBinaryArtifacts"
"github.com/ossf/scorecard/v5/probes/issueActivityByProjectMember"
"github.com/ossf/scorecard/v5/probes/jobLevelPermissions"
@ -130,6 +132,10 @@ var (
CITests = []ProbeImpl{
testsRunInCI.Run,
}
SBOM = []ProbeImpl{
hasSBOM.Run,
hasReleaseSBOM.Run,
}
SignedReleases = []ProbeImpl{
releasesAreSigned.Run,
releasesHaveProvenance.Run,

View File

@ -0,0 +1,36 @@
# Copyright 2024 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: hasReleaseSBOM
short: Check that the project publishes an SBOM as part of its release artifacts.
motivation: >
An SBOM can give users information about how the source code components and dependencies. They help facilitate sotware supplychain security and aid in identifying upstream vulnerabilities in a codebase.
implementation: >
The implementation checks whether a SBOM artifact is included in release artifacts.
outcome:
- If SBOM artifacts are found, the probe returns OutcomeTrue for each SBOM artifact up to 5.
- If an SBOM artifact is not found, the probe returns a single OutcomeFalse.
remediation:
onOutcome: False
effort: Low
text:
- For Github projects, start with [this guide](https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-supply-chain-security) to determine which steps are needed to generate an adequate SBOM.
- For Gitlab projects, see existing [Dependency Scanning](https://docs.gitlab.com/ee/user/application_security/dependency_scanning/index.html#cyclonedx-software-bill-of-materials) and [Container Scanning](https://docs.gitlab.com/ee/user/application_security/container_scanning/index.html#cyclonedx-software-bill-of-materials) tools.
- Alternatively, there are other tools available to generate [CycloneDX](https://cyclonedx.org/tool-center/) and [SPDX](https://spdx.dev/use/tools/) SBOMs.
ecosystem:
languages:
- all
clients:
- github
- gitlab

View File

@ -0,0 +1,82 @@
// Copyright 2024 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 hasReleaseSBOM
import (
"embed"
"fmt"
"github.com/ossf/scorecard/v5/checker"
"github.com/ossf/scorecard/v5/finding"
"github.com/ossf/scorecard/v5/internal/probes"
"github.com/ossf/scorecard/v5/probes/internal/utils/uerror"
)
func init() {
probes.MustRegister(Probe, Run, []probes.CheckName{probes.SBOM})
}
//go:embed *.yml
var fs embed.FS
const (
Probe = "hasReleaseSBOM"
AssetNameKey = "assetName"
AssetURLKey = "assetURL"
missingSbom = "Project is not publishing an SBOM file as part of a release or CICD"
)
func Run(raw *checker.RawResults) ([]finding.Finding, string, error) {
if raw == nil {
return nil, "", fmt.Errorf("%w: raw", uerror.ErrNil)
}
var findings []finding.Finding
var msg string
SBOMFiles := raw.SBOMResults.SBOMFiles
for i := range SBOMFiles {
SBOMFile := SBOMFiles[i]
if SBOMFile.File.Type != finding.FileTypeURL {
continue
}
loc := SBOMFile.File.Location()
msg = "Project publishes an SBOM file as part of a release or CICD"
f, err := finding.NewTrue(fs, Probe, msg, loc)
if err != nil {
return nil, Probe, fmt.Errorf("create finding: %w", err)
}
f.Values = map[string]string{
AssetNameKey: SBOMFile.Name,
AssetURLKey: SBOMFile.File.Path,
}
findings = append(findings, *f)
}
if len(findings) == 0 {
msg = missingSbom
f, err := finding.NewFalse(fs, Probe, msg, nil)
if err != nil {
return nil, Probe, fmt.Errorf("create finding: %w", err)
}
findings = append(findings, *f)
}
return findings, Probe, nil
}

View File

@ -0,0 +1,122 @@
// Copyright 2024 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 hasReleaseSBOM
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/ossf/scorecard/v5/checker"
"github.com/ossf/scorecard/v5/finding"
"github.com/ossf/scorecard/v5/probes/internal/utils/test"
"github.com/ossf/scorecard/v5/probes/internal/utils/uerror"
)
func Test_Run(t *testing.T) {
t.Parallel()
//nolint:govet
tests := []struct {
name string
raw *checker.RawResults
outcomes []finding.Outcome
err error
}{
{
name: "Release SBOM file found and outcome should be positive",
raw: &checker.RawResults{
SBOMResults: checker.SBOMData{
SBOMFiles: []checker.SBOM{
{
File: checker.File{
Path: "SBOM.cdx.json",
Type: finding.FileTypeURL,
},
},
},
},
},
outcomes: []finding.Outcome{
finding.OutcomeTrue,
},
},
{
name: "Release SBOM file not found and outcome should be negative",
raw: &checker.RawResults{
SBOMResults: checker.SBOMData{
SBOMFiles: []checker.SBOM{
{
File: checker.File{
Path: "SBOM.cdx.json",
Type: finding.FileTypeSource,
},
},
},
},
},
outcomes: []finding.Outcome{
finding.OutcomeFalse,
},
},
{
name: "SBOM file not found and outcome should be negative",
raw: &checker.RawResults{
SBOMResults: checker.SBOMData{
SBOMFiles: []checker.SBOM{},
},
},
outcomes: []finding.Outcome{
finding.OutcomeFalse,
},
},
{
name: "nil license files and outcome should be negative",
raw: &checker.RawResults{
SBOMResults: checker.SBOMData{
SBOMFiles: nil,
},
},
outcomes: []finding.Outcome{
finding.OutcomeFalse,
},
},
{
name: "no raw data",
raw: nil,
err: uerror.ErrNil,
outcomes: nil,
},
}
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)
}
test.AssertOutcomes(t, findings, tt.outcomes)
})
}
}

36
probes/hasSBOM/def.yml Normal file
View File

@ -0,0 +1,36 @@
# Copyright 2024 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: hasSBOM
short: Check that the project has an SBOM file
motivation: >
An SBOM can give users information about how the source code components and dependencies. They help facilitate sotware supplychain security and aid in identifying upstream vulnerabilities in a codebase.
implementation: >
The implementation checks whether an SBOM file is present in the source code.
outcome:
- If an SBOM file(s) is found, the probe returns OutcomeTrue for each SBOM artifact up to 5.
- If an SBOM file is not found, the probe returns a single OutcomeFalse.
remediation:
onOutcome: False
effort: Low
text:
- For Github projects, start with [this guide](https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-supply-chain-security) to determine which steps are needed to generate an adequate SBOM.
- For Gitlab projects, see existing [Dependency Scanning](https://docs.gitlab.com/ee/user/application_security/dependency_scanning/index.html#cyclonedx-software-bill-of-materials) and [Container Scanning](https://docs.gitlab.com/ee/user/application_security/container_scanning/index.html#cyclonedx-software-bill-of-materials) tools.
- Alternatively, there are other tools available to generate [CycloneDX](https://cyclonedx.org/tool-center/) and [SPDX](https://spdx.dev/use/tools/) SBOMs.
ecosystem:
languages:
- all
clients:
- github
- gitlab

69
probes/hasSBOM/impl.go Normal file
View File

@ -0,0 +1,69 @@
// Copyright 2024 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 hasSBOM
import (
"embed"
"fmt"
"github.com/ossf/scorecard/v5/checker"
"github.com/ossf/scorecard/v5/finding"
"github.com/ossf/scorecard/v5/internal/probes"
"github.com/ossf/scorecard/v5/probes/internal/utils/uerror"
)
func init() {
probes.MustRegister(Probe, Run, []probes.CheckName{probes.SBOM})
}
//go:embed *.yml
var fs embed.FS
const Probe = "hasSBOM"
func Run(raw *checker.RawResults) ([]finding.Finding, string, error) {
if raw == nil {
return nil, "", fmt.Errorf("%w: raw", uerror.ErrNil)
}
var findings []finding.Finding
var msg string
SBOMFiles := raw.SBOMResults.SBOMFiles
if len(SBOMFiles) == 0 {
msg = "Project does not have a SBOM file"
f, err := finding.NewFalse(fs, Probe, msg, nil)
if err != nil {
return nil, Probe, fmt.Errorf("create finding: %w", err)
}
findings = append(findings, *f)
return findings, Probe, nil
}
for i := range SBOMFiles {
SBOMFile := SBOMFiles[i]
loc := SBOMFile.File.Location()
msg = "Project has a SBOM file"
f, err := finding.NewTrue(fs, Probe, msg, loc)
if err != nil {
return nil, Probe, fmt.Errorf("create finding: %w", err)
}
findings = append(findings, *f)
}
return findings, Probe, nil
}

103
probes/hasSBOM/impl_test.go Normal file
View File

@ -0,0 +1,103 @@
// Copyright 2024 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 hasSBOM
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/ossf/scorecard/v5/checker"
"github.com/ossf/scorecard/v5/finding"
"github.com/ossf/scorecard/v5/probes/internal/utils/test"
"github.com/ossf/scorecard/v5/probes/internal/utils/uerror"
)
func Test_Run(t *testing.T) {
t.Parallel()
//nolint:govet
tests := []struct {
name string
raw *checker.RawResults
outcomes []finding.Outcome
err error
}{
{
name: "SBOM file found and outcome should be positive",
raw: &checker.RawResults{
SBOMResults: checker.SBOMData{
SBOMFiles: []checker.SBOM{
{
File: checker.File{
Path: "SBOM.cdx.json",
},
},
},
},
},
outcomes: []finding.Outcome{
finding.OutcomeTrue,
},
},
{
name: "nil SBOM files and outcome should be negative",
raw: &checker.RawResults{
SBOMResults: checker.SBOMData{
SBOMFiles: nil,
},
},
outcomes: []finding.Outcome{
finding.OutcomeFalse,
},
},
{
name: "0 SBOM files and outcome should be negative",
raw: &checker.RawResults{
SBOMResults: checker.SBOMData{
SBOMFiles: []checker.SBOM{},
},
},
outcomes: []finding.Outcome{
finding.OutcomeFalse,
},
},
{
name: "no raw data",
raw: nil,
err: uerror.ErrNil,
outcomes: nil,
},
}
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)
}
test.AssertOutcomes(t, findings, tt.outcomes)
})
}
}