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

feat: sbom verifier improvements #1205

Merged
merged 10 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -392,15 +392,15 @@ e2e-sbom-setup:
${GITHUB_WORKSPACE}/bin/oras attach \
--artifact-type application/spdx+json \
${TEST_REGISTRY}/sbom:v0 \
.staging/sbom/_manifest/spdx_2.2/manifest.spdx.json:application/spdx+json
.staging/sbom/_manifest/spdx_2.2/manifest.spdx.json
${GITHUB_WORKSPACE}/bin/oras attach \
--artifact-type application/spdx+json \
${TEST_REGISTRY}/sbom:unsigned \
.staging/sbom/_manifest/spdx_2.2/manifest.spdx.json:application/spdx+json
.staging/sbom/_manifest/spdx_2.2/manifest.spdx.json
${GITHUB_WORKSPACE}/bin/oras attach \
--artifact-type application/spdx+json \
${TEST_REGISTRY}/all:v0 \
.staging/sbom/_manifest/spdx_2.2/manifest.spdx.json:application/spdx+json
.staging/sbom/_manifest/spdx_2.2/manifest.spdx.json

# Push Signature to sbom
.staging/notation/notation sign -u ${TEST_REGISTRY_USERNAME} -p ${TEST_REGISTRY_PASSWORD} ${TEST_REGISTRY}/sbom@`oras discover -o json --artifact-type application/spdx+json ${TEST_REGISTRY}/sbom:v0 | jq -r ".manifests[0].digest"`
Expand Down
2 changes: 1 addition & 1 deletion charts/ratify/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ $ helm upgrade -n gatekeeper-system [RELEASE_NAME] ratify/ratify
| sbom.enabled | Enables/disables installation of sbom verification configuration | `false` |
| sbom.notaryProjectSignatureRequired | requires validation of sbom notation signature | `false` |
| sbom.disallowedLicenses | list of disallowed licenses | [] |
| sbom.disallowedPackages | list of disallowed packages defined by package name and version | [] |
| sbom.disallowedPackages | list of disallowed packages defined by package name and version. For example: --set sbom.disallowedPackages[0].name="busybox" --set sbom.disallowedPackages[0].version="1.36.1-r0" | [] |
| resources.limits.cpu | CPU limits of Ratify Deployment | `1000m` |
| resources.limits.memory | Memory limits of Ratify Deployment | `512Mi` |
| resources.requests.cpu | CPU request of Ratify Deployment | `600m` |
Expand Down
14 changes: 14 additions & 0 deletions config/samples/config_v1beta1_verifier_sbom_deny.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
apiVersion: config.ratify.deislabs.io/v1beta1
kind: Verifier
metadata:
name: verifier-sbom
spec:
name: sbom
artifactTypes: application/spdx+json
parameters:
disallowedLicenses:
- Zlib
disallowedPackages:
- name: musl-utils
version: 1.2.3-r4
nestedReferences: application/vnd.cncf.notary.signature
23 changes: 11 additions & 12 deletions plugins/verifier/sbom/sbom.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@
Name: input.Name,
Type: verifierType,
IsSuccess: false,
Message: fmt.Sprintf("Error fetching reference manifest for subject: %s reference descriptor: %v", subjectReference, referenceDescriptor.Descriptor),
}, err
Message: fmt.Sprintf("Error fetching reference manifest for subject: %s reference descriptor: %v, err: %v", subjectReference, referenceDescriptor.Descriptor, err),
}, nil

Check warning on line 90 in plugins/verifier/sbom/sbom.go

View check run for this annotation

Codecov / codecov/patch

plugins/verifier/sbom/sbom.go#L89-L90

Added lines #L89 - L90 were not covered by tests
}

if len(referenceManifest.Blobs) == 0 {
Expand All @@ -107,13 +107,13 @@
Name: input.Name,
Type: verifierType,
IsSuccess: false,
Message: fmt.Sprintf("Error fetching blob for subject: %s digest: %s", subjectReference, blobDesc.Digest),
}, err
Message: fmt.Sprintf("Error fetching blob for subject: %s digest: %s, err: %v", subjectReference, blobDesc.Digest, err),
}, nil

Check warning on line 111 in plugins/verifier/sbom/sbom.go

View check run for this annotation

Codecov / codecov/patch

plugins/verifier/sbom/sbom.go#L110-L111

Added lines #L110 - L111 were not covered by tests
}

switch artifactType {
case SpdxJSONMediaType:
return processSpdxJSONMediaType(input.Name, verifierType, refBlob, input.DisallowedLicenses, input.DisallowedPackages)
return processSpdxJSONMediaType(input.Name, verifierType, refBlob, input.DisallowedLicenses, input.DisallowedPackages), nil
default:
return &verifier.VerifierResult{
Name: input.Name,
Expand Down Expand Up @@ -159,7 +159,7 @@
}

// parse through the spdx blob and returns the verifier result
func processSpdxJSONMediaType(name string, verifierType string, refBlob []byte, disallowedLicenses []string, disallowedPackages []utils.PackageInfo) (*verifier.VerifierResult, error) {
func processSpdxJSONMediaType(name string, verifierType string, refBlob []byte, disallowedLicenses []string, disallowedPackages []utils.PackageInfo) *verifier.VerifierResult {
var err error
var spdxDoc *v2_3.Document
if spdxDoc, err = jsonLoader.Read(bytes.NewReader(refBlob)); spdxDoc != nil && err == nil {
Expand All @@ -181,8 +181,8 @@
Name: name,
IsSuccess: false,
Extensions: extensionData,
Message: "SBOM validation failed.",
}, err
Message: "SBOM validation failed. Please review extensions data for license and package violation found.",
}
}
}

Expand All @@ -194,14 +194,14 @@
CreationInfo: spdxDoc.CreationInfo,
},
Message: "SBOM verification success. No license or package violation found.",
}, nil
}
}
return &verifier.VerifierResult{
Name: name,
Type: verifierType,
IsSuccess: false,
Message: fmt.Sprintf("SBOM failed to parse: %v", err),
}, err
}
}

// iterate through all package info and check against the deny list
Expand All @@ -213,8 +213,7 @@
for _, packageInfo := range packageLicenses {
// if license contains disallowed, add to violation
for _, disallowed := range disallowedLicense {
license := packageInfo.License
if license != "" && strings.Contains(strings.ToLower(license), strings.ToLower(disallowed)) {
if utils.ContainsLicense(strings.ToLower(packageInfo.License), strings.ToLower(disallowed)) {
violationLicense = append(violationLicense, packageInfo)
}
}
Expand Down
33 changes: 23 additions & 10 deletions plugins/verifier/sbom/sbom_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package main
import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/deislabs/ratify/plugins/verifier/sbom/utils"
Expand All @@ -27,10 +28,7 @@ func TestProcessSPDXJsonMediaType(t *testing.T) {
if err != nil {
t.Fatalf("error reading %s", filepath.Join("testdata", "bom.json"))
}
vr, err := processSpdxJSONMediaType("test", "", b, nil, nil)
if err != nil {
t.Fatalf("expected to process spdx json file: %s", filepath.Join("testdata", "bom.json"))
}
vr := processSpdxJSONMediaType("test", "", b, nil, nil)
if !vr.IsSuccess {
t.Fatalf("expected to successfully verify schema")
}
Expand All @@ -41,8 +39,9 @@ func TestProcessInvalidSPDXJsonMediaType(t *testing.T) {
if err != nil {
t.Fatalf("error reading %s", filepath.Join("testdata", "invalid-bom.json"))
}
_, err = processSpdxJSONMediaType("test", "", b, nil, nil)
if err == nil {
report := processSpdxJSONMediaType("test", "", b, nil, nil)

if !strings.Contains(report.Message, "SBOM failed to parse") {
t.Fatalf("expected to have an error processing spdx json file: %s", filepath.Join("testdata", "bom.json"))
}
}
Expand Down Expand Up @@ -98,6 +97,12 @@ func TestGetViolations(t *testing.T) {
expectedPackageViolations []utils.PackageLicense
enabled bool
}{
{
description: "disallowed packages with no version",
disallowedLicenses: []string{"MPL"},
expectedLicenseViolations: nil,
expectedPackageViolations: nil,
},
{
description: "disallowed packages with no version",
disallowedPackages: []utils.PackageInfo{disallowedPackageNoVersion},
Expand All @@ -113,7 +118,6 @@ func TestGetViolations(t *testing.T) {
},
{
description: "invalid disallow package",
disallowedLicenses: []string{"BSD-3-Clause", "Zlib"},
disallowedPackages: []utils.PackageInfo{disallowedPackageNoName},
expectedLicenseViolations: []utils.PackageLicense{},
expectedPackageViolations: []utils.PackageLicense{},
Expand Down Expand Up @@ -151,9 +155,18 @@ func TestGetViolations(t *testing.T) {

for _, tc := range cases {
t.Run("test scenario", func(t *testing.T) {
report, err := processSpdxJSONMediaType("test", "", b, tc.disallowedLicenses, tc.disallowedPackages)
if err != nil {
t.Fatalf("unexpected error processing spdx json file: %s", filepath.Join("testdata", "bom.json"))
report := processSpdxJSONMediaType("test", "", b, tc.disallowedLicenses, tc.disallowedPackages)

if len(tc.expectedPackageViolations) != 0 || len(tc.expectedLicenseViolations) != 0 {
if report.IsSuccess {
t.Fatalf("Test %s failed. Expected IsSuccess: true, got: false", tc.description)
}
}

if len(tc.expectedPackageViolations) == 0 && len(tc.expectedLicenseViolations) == 0 {
if !report.IsSuccess {
t.Fatalf("Test %s failed. Expected IsSuccess: false, got: true", tc.description)
}
}

if len(tc.expectedPackageViolations) != 0 {
Expand Down
25 changes: 25 additions & 0 deletions plugins/verifier/sbom/utils/spdxutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
package utils

import (
"strings"

"github.com/spdx/tools-golang/spdx"
)

Expand All @@ -31,3 +33,26 @@
}
return output
}

// returns true if the licenseExpression contains the disallowed license
// this implements a whole word match
func ContainsLicense(spdxLicenseExpression string, disallowed string) bool {
if len(spdxLicenseExpression) == 0 {
return false
}

Check warning on line 42 in plugins/verifier/sbom/utils/spdxutils.go

View check run for this annotation

Codecov / codecov/patch

plugins/verifier/sbom/utils/spdxutils.go#L41-L42

Added lines #L41 - L42 were not covered by tests

// if the licenseExpression is exactly the same as the disallowed license, return true
if spdxLicenseExpression == disallowed {
return true
}

disallowed1 := disallowed + " "
disallowed2 := " " + disallowed

// look for whole word match
if strings.Contains(spdxLicenseExpression, disallowed1) || strings.Contains(spdxLicenseExpression, disallowed2) {
akashsinghal marked this conversation as resolved.
Show resolved Hide resolved
return true
}

return false
}
43 changes: 43 additions & 0 deletions plugins/verifier/sbom/utils/spdxutils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,46 @@ func TestGetPackageLicenses(t *testing.T) {
t.Fatalf("unexpected packages count, expected 16")
}
}

func TestContainsLicense(t *testing.T) {
tests := []struct {
name string
spdxLicenseExpression string
disallowed string
expected bool
}{
{
name: "exact match",
spdxLicenseExpression: "MIT",
disallowed: "MIT",
expected: true,
},
{
name: "exact match with space",
spdxLicenseExpression: "MPL-2.0 AND LicenseRef-AND AND MIT",
disallowed: "MPL",
expected: false,
},
{
name: "exact match with space",
spdxLicenseExpression: "MPL-2.0 AND LicenseRef-AND AND MIT",
disallowed: "MPL-2.0",
expected: true,
},
{
name: "exact match with space",
spdxLicenseExpression: "MIT AND LicenseRef-AND AND MPL-2.0",
disallowed: "MPL-2.0",
expected: true,
},
}

for _, tt := range tests {
t.Run("test scenario", func(t *testing.T) {
result := ContainsLicense(tt.spdxLicenseExpression, tt.disallowed)
if result != tt.expected {
t.Fatalf("expected %t, got %t", tt.expected, result)
}
})
}
}
8 changes: 7 additions & 1 deletion test/bats/plugin-test.bats
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,15 @@ SLEEP_TIME=1
assert_success
sleep 5

run kubectl apply -f ./config/samples/config_v1beta1_verifier_sbom.yaml
run kubectl apply -f ./config/samples/config_v1beta1_verifier_sbom_deny.yaml
sleep 5
run kubectl run sbom --namespace default --image=registry:5000/sbom:v0
assert_failure

run kubectl apply -f ./config/samples/config_v1beta1_verifier_sbom.yaml
# wait for the httpserver cache to be invalidated
sleep 15
run kubectl run sbom --namespace default --image=registry:5000/sbom:v0
assert_success

run kubectl delete verifiers.config.ratify.deislabs.io/verifier-sbom
Expand Down
Loading