Files
trivy/pkg/fanal/artifact/sbom/sbom_test.go
2025-09-20 14:26:53 +00:00

502 lines
15 KiB
Go

package sbom_test
import (
"path/filepath"
"strings"
"testing"
"github.com/package-url/packageurl-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/aquasecurity/trivy/internal/cachetest"
"github.com/aquasecurity/trivy/pkg/cache"
"github.com/aquasecurity/trivy/pkg/fanal/artifact"
"github.com/aquasecurity/trivy/pkg/fanal/artifact/sbom"
"github.com/aquasecurity/trivy/pkg/fanal/types"
)
func TestArtifact_Inspect(t *testing.T) {
tests := []struct {
name string
filePath string
setUpCache func(t *testing.T) cache.Cache
wantBlobs []cachetest.WantBlob
want artifact.Reference
wantErr []string
}{
{
name: "happy path",
filePath: filepath.Join("testdata", "bom.json"),
wantBlobs: []cachetest.WantBlob{
{
ID: "sha256:76bc49ae239d24c6a122e730bafb9d5295d0af380492aeb92a3bf34bea3a14ca",
BlobInfo: types.BlobInfo{
SchemaVersion: types.BlobJSONSchemaVersion,
OS: types.OS{
Family: "alpine",
Name: "3.16.0",
},
PackageInfos: []types.PackageInfo{
{
Packages: types.Packages{
{
ID: "musl@1.2.3-r0",
Name: "musl",
Version: "1.2.3-r0",
SrcName: "musl",
SrcVersion: "1.2.3-r0",
Licenses: []string{"MIT"},
Layer: types.Layer{
DiffID: "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3",
},
Identifier: types.PkgIdentifier{
PURL: &packageurl.PackageURL{
Type: packageurl.TypeApk,
Namespace: "alpine",
Name: "musl",
Version: "1.2.3-r0",
Qualifiers: packageurl.Qualifiers{
{
Key: "distro",
Value: "3.16.0",
},
},
},
BOMRef: "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0",
},
},
},
},
},
Applications: []types.Application{
{
Type: "composer",
FilePath: "app/composer/composer.lock",
Packages: types.Packages{
{
ID: "pear/log@1.13.1",
Name: "pear/log",
Version: "1.13.1",
Layer: types.Layer{
DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
},
Identifier: types.PkgIdentifier{
PURL: &packageurl.PackageURL{
Type: packageurl.TypeComposer,
Namespace: "pear",
Name: "log",
Version: "1.13.1",
},
BOMRef: "pkg:composer/pear/log@1.13.1",
},
},
{
ID: "pear/pear_exception@v1.0.0",
Name: "pear/pear_exception",
Version: "v1.0.0",
Layer: types.Layer{
DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
},
Identifier: types.PkgIdentifier{
PURL: &packageurl.PackageURL{
Type: packageurl.TypeComposer,
Namespace: "pear",
Name: "pear_exception",
Version: "v1.0.0",
},
BOMRef: "pkg:composer/pear/pear_exception@v1.0.0",
},
},
},
},
{
Type: "gobinary",
FilePath: "app/gobinary/gobinary",
Packages: types.Packages{
{
ID: "github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a",
Name: "github.com/package-url/packageurl-go",
Version: "v0.1.1-0.20220203205134-d70459300c8a",
Layer: types.Layer{
DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
},
Identifier: types.PkgIdentifier{
PURL: &packageurl.PackageURL{
Type: packageurl.TypeGolang,
Namespace: "github.com/package-url",
Name: "packageurl-go",
Version: "v0.1.1-0.20220203205134-d70459300c8a",
},
BOMRef: "pkg:golang/github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a",
},
},
},
},
{
Type: "jar",
FilePath: "",
Packages: types.Packages{
{
ID: "org.codehaus.mojo:child-project:1.0",
Name: "org.codehaus.mojo:child-project",
Version: "1.0",
Layer: types.Layer{
DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
},
FilePath: "app/maven/target/child-project-1.0.jar",
Identifier: types.PkgIdentifier{
PURL: &packageurl.PackageURL{
Type: packageurl.TypeMaven,
Namespace: "org.codehaus.mojo",
Name: "child-project",
Version: "1.0",
},
// Keep the original value
BOMRef: "pkg:maven/org.codehaus.mojo/child-project@1.0?file_path=app%2Fmaven%2Ftarget%2Fchild-project-1.0.jar",
},
},
},
},
{
Type: "node-pkg",
FilePath: "",
Packages: types.Packages{
{
ID: "bootstrap@5.0.2",
Name: "bootstrap",
Version: "5.0.2",
Licenses: []string{"MIT"},
Layer: types.Layer{
DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
},
FilePath: "app/app/package.json",
Identifier: types.PkgIdentifier{
PURL: &packageurl.PackageURL{
Type: packageurl.TypeNPM,
Name: "bootstrap",
Version: "5.0.2",
},
// Keep the original value
BOMRef: "pkg:npm/bootstrap@5.0.2?file_path=app%2Fapp%2Fpackage.json",
},
},
},
},
},
},
},
},
want: artifact.Reference{
Name: filepath.Join("testdata", "bom.json"),
Type: types.TypeCycloneDX,
ID: "sha256:76bc49ae239d24c6a122e730bafb9d5295d0af380492aeb92a3bf34bea3a14ca",
BlobIDs: []string{
"sha256:76bc49ae239d24c6a122e730bafb9d5295d0af380492aeb92a3bf34bea3a14ca",
},
},
},
{
name: "happy path for sbom attestation",
filePath: filepath.Join("testdata", "sbom.cdx.intoto.jsonl"),
wantBlobs: []cachetest.WantBlob{
{
ID: "sha256:76bc49ae239d24c6a122e730bafb9d5295d0af380492aeb92a3bf34bea3a14ca",
BlobInfo: types.BlobInfo{
SchemaVersion: types.BlobJSONSchemaVersion,
OS: types.OS{
Family: "alpine",
Name: "3.16.0",
},
PackageInfos: []types.PackageInfo{
{
Packages: types.Packages{
{
ID: "musl@1.2.3-r0",
Name: "musl",
Version: "1.2.3-r0",
SrcName: "musl",
SrcVersion: "1.2.3-r0",
Licenses: []string{"MIT"},
Identifier: types.PkgIdentifier{
PURL: &packageurl.PackageURL{
Type: packageurl.TypeApk,
Namespace: "alpine",
Name: "musl",
Version: "1.2.3-r0",
Qualifiers: packageurl.Qualifiers{
{
Key: "distro",
Value: "3.16.0",
},
},
},
BOMRef: "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0",
},
Layer: types.Layer{
DiffID: "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3",
},
},
},
},
},
Applications: []types.Application{
{
Type: "composer",
FilePath: "app/composer/composer.lock",
Packages: types.Packages{
{
ID: "pear/log@1.13.1",
Name: "pear/log",
Version: "1.13.1",
Identifier: types.PkgIdentifier{
PURL: &packageurl.PackageURL{
Type: packageurl.TypeComposer,
Namespace: "pear",
Name: "log",
Version: "1.13.1",
},
BOMRef: "pkg:composer/pear/log@1.13.1",
},
Layer: types.Layer{
DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
},
},
{
ID: "pear/pear_exception@v1.0.0",
Name: "pear/pear_exception",
Version: "v1.0.0",
Identifier: types.PkgIdentifier{
PURL: &packageurl.PackageURL{
Type: packageurl.TypeComposer,
Namespace: "pear",
Name: "pear_exception",
Version: "v1.0.0",
},
BOMRef: "pkg:composer/pear/pear_exception@v1.0.0",
},
Layer: types.Layer{
DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
},
},
},
},
{
Type: "gobinary",
FilePath: "app/gobinary/gobinary",
Packages: types.Packages{
{
ID: "github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a",
Name: "github.com/package-url/packageurl-go",
Version: "v0.1.1-0.20220203205134-d70459300c8a",
Identifier: types.PkgIdentifier{
PURL: &packageurl.PackageURL{
Type: packageurl.TypeGolang,
Namespace: "github.com/package-url",
Name: "packageurl-go",
Version: "v0.1.1-0.20220203205134-d70459300c8a",
},
BOMRef: "pkg:golang/github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a",
},
Layer: types.Layer{
DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
},
},
},
},
{
Type: "jar",
FilePath: "",
Packages: types.Packages{
{
ID: "org.codehaus.mojo:child-project:1.0",
Name: "org.codehaus.mojo:child-project",
Version: "1.0",
Identifier: types.PkgIdentifier{
PURL: &packageurl.PackageURL{
Type: packageurl.TypeMaven,
Namespace: "org.codehaus.mojo",
Name: "child-project",
Version: "1.0",
},
// Keep the original value
BOMRef: "pkg:maven/org.codehaus.mojo/child-project@1.0?file_path=app%2Fmaven%2Ftarget%2Fchild-project-1.0.jar",
},
Layer: types.Layer{
DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
},
FilePath: "app/maven/target/child-project-1.0.jar",
},
},
},
{
Type: "node-pkg",
FilePath: "",
Packages: types.Packages{
{
ID: "bootstrap@5.0.2",
Name: "bootstrap",
Version: "5.0.2",
Identifier: types.PkgIdentifier{
PURL: &packageurl.PackageURL{
Type: packageurl.TypeNPM,
Name: "bootstrap",
Version: "5.0.2",
},
// Keep the original value
BOMRef: "pkg:npm/bootstrap@5.0.2?file_path=app%2Fapp%2Fpackage.json",
},
Licenses: []string{"MIT"},
Layer: types.Layer{
DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
},
FilePath: "app/app/package.json",
},
},
},
},
},
},
},
want: artifact.Reference{
Name: filepath.Join("testdata", "sbom.cdx.intoto.jsonl"),
Type: types.TypeCycloneDX,
ID: "sha256:76bc49ae239d24c6a122e730bafb9d5295d0af380492aeb92a3bf34bea3a14ca",
BlobIDs: []string{
"sha256:76bc49ae239d24c6a122e730bafb9d5295d0af380492aeb92a3bf34bea3a14ca",
},
},
},
{
name: "components with missing BOM-REF",
filePath: filepath.Join("testdata", "bom-missing-refs.json"),
wantBlobs: []cachetest.WantBlob{
{
ID: "sha256:512b9e999c9d7b4880c63ce55c2c74ea5c22b05cdbcb486097a16ec692c746a0",
BlobInfo: types.BlobInfo{
SchemaVersion: types.BlobJSONSchemaVersion,
OS: types.OS{
Family: "alpine",
Name: "3.16.0",
},
PackageInfos: []types.PackageInfo{
{
Packages: types.Packages{
{
ID: "musl@1.2.3-r0",
Name: "musl",
Version: "1.2.3-r0",
SrcName: "musl",
SrcVersion: "1.2.3-r0",
Licenses: []string{"MIT"},
Layer: types.Layer{
DiffID: "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3",
},
Identifier: types.PkgIdentifier{
PURL: &packageurl.PackageURL{
Type: packageurl.TypeApk,
Namespace: "alpine",
Name: "musl",
Version: "1.2.3-r0",
Qualifiers: packageurl.Qualifiers{
{
Key: "distro",
Value: "3.16.0",
},
},
},
// BOM-Ref should be auto-generated from PURL
BOMRef: "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0",
},
},
},
},
},
Applications: []types.Application{
{
Type: "composer",
FilePath: "",
Packages: types.Packages{
{
ID: "pear/log@1.13.1",
Name: "pear/log",
Version: "1.13.1",
Layer: types.Layer{
DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
},
Identifier: types.PkgIdentifier{
PURL: &packageurl.PackageURL{
Type: packageurl.TypeComposer,
Namespace: "pear",
Name: "log",
Version: "1.13.1",
},
// BOM-Ref should be auto-generated from PURL
BOMRef: "pkg:composer/pear/log@1.13.1",
},
},
},
},
},
},
},
},
want: artifact.Reference{
Name: filepath.Join("testdata", "bom-missing-refs.json"),
Type: types.TypeCycloneDX,
ID: "sha256:512b9e999c9d7b4880c63ce55c2c74ea5c22b05cdbcb486097a16ec692c746a0",
BlobIDs: []string{
"sha256:512b9e999c9d7b4880c63ce55c2c74ea5c22b05cdbcb486097a16ec692c746a0",
},
},
},
{
name: "sad path with no such directory",
filePath: filepath.Join("testdata", "unknown.json"),
wantErr: []string{
"no such file or directory",
"The system cannot find the file specified",
},
},
{
name: "sad path PutBlob returns an error",
filePath: filepath.Join("testdata", "os-only-bom.json"),
setUpCache: func(_ *testing.T) cache.Cache {
return cachetest.NewErrorCache(cachetest.ErrorCacheOptions{
PutBlob: true,
})
},
wantErr: []string{"failed to store blob"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := cachetest.NewCache(t, tt.setUpCache)
a, err := sbom.NewArtifact(tt.filePath, c, artifact.Option{})
require.NoError(t, err)
got, err := a.Inspect(t.Context())
if len(tt.wantErr) > 0 {
require.Error(t, err)
found := false
for _, wantErr := range tt.wantErr {
if strings.Contains(err.Error(), wantErr) {
found = true
break
}
}
assert.True(t, found)
return
}
// Not compare the original CycloneDX report
got.BOM = nil
require.NoError(t, err)
assert.Equal(t, tt.want, got)
cachetest.AssertBlobs(t, c, tt.wantBlobs)
})
}
}