mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-05 20:40:16 -08:00
322 lines
8.7 KiB
Go
322 lines
8.7 KiB
Go
package image_test
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"testing"
|
|
|
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
|
fakei "github.com/google/go-containerregistry/pkg/v1/fake"
|
|
"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/internal/registrytest"
|
|
"github.com/aquasecurity/trivy/pkg/fanal/artifact"
|
|
image2 "github.com/aquasecurity/trivy/pkg/fanal/artifact/image"
|
|
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
|
"github.com/aquasecurity/trivy/pkg/log"
|
|
"github.com/aquasecurity/trivy/pkg/oci"
|
|
"github.com/aquasecurity/trivy/pkg/rekortest"
|
|
"github.com/aquasecurity/trivy/pkg/sbom"
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
log.InitLogger(false, true)
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
type fakeImage struct {
|
|
name string
|
|
repoDigests []string
|
|
v1.Image
|
|
ftypes.ImageExtension
|
|
}
|
|
|
|
func (f fakeImage) ID() (string, error) {
|
|
return "", nil
|
|
}
|
|
|
|
func (f fakeImage) Name() string {
|
|
return f.name
|
|
}
|
|
|
|
func (f fakeImage) RepoDigests() []string {
|
|
return f.repoDigests
|
|
}
|
|
|
|
func (f fakeImage) RepoTags() []string {
|
|
return nil
|
|
}
|
|
|
|
func TestArtifact_InspectRekorAttestation(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
imageName string
|
|
repoDigests []string
|
|
wantBlobs []cachetest.WantBlob
|
|
want artifact.Reference
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "happy path",
|
|
imageName: "test/image:10",
|
|
repoDigests: []string{
|
|
"test/image@sha256:782143e39f1e7a04e3f6da2d88b1c057e5657363c4f90679f3e8a071b7619e02",
|
|
},
|
|
wantBlobs: []cachetest.WantBlob{
|
|
{
|
|
ID: "sha256:066b9998617ffb7dfe0a3219ac5c3efc1008a6223606fcf474e7d5c965e4e8da",
|
|
BlobInfo: ftypes.BlobInfo{
|
|
SchemaVersion: ftypes.BlobJSONSchemaVersion,
|
|
OS: ftypes.OS{
|
|
Family: "alpine",
|
|
Name: "3.16.2",
|
|
},
|
|
PackageInfos: []ftypes.PackageInfo{
|
|
{
|
|
Packages: ftypes.Packages{
|
|
{
|
|
ID: "musl@1.2.3-r0",
|
|
Name: "musl",
|
|
Version: "1.2.3-r0",
|
|
Identifier: ftypes.PkgIdentifier{
|
|
PURL: &packageurl.PackageURL{
|
|
Type: packageurl.TypeApk,
|
|
Namespace: "alpine",
|
|
Name: "musl",
|
|
Version: "1.2.3-r0",
|
|
Qualifiers: packageurl.Qualifiers{
|
|
{
|
|
Key: "distro",
|
|
Value: "3.16.2",
|
|
},
|
|
},
|
|
},
|
|
BOMRef: "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.2",
|
|
},
|
|
SrcName: "musl",
|
|
SrcVersion: "1.2.3-r0",
|
|
Licenses: []string{"MIT"},
|
|
Layer: ftypes.Layer{
|
|
DiffID: "sha256:994393dc58e7931862558d06e46aa2bb17487044f670f310dffe1d24e4d1eec7",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: artifact.Reference{
|
|
Name: "test/image:10",
|
|
Type: ftypes.TypeCycloneDX,
|
|
ID: "sha256:066b9998617ffb7dfe0a3219ac5c3efc1008a6223606fcf474e7d5c965e4e8da",
|
|
BlobIDs: []string{
|
|
"sha256:066b9998617ffb7dfe0a3219ac5c3efc1008a6223606fcf474e7d5c965e4e8da",
|
|
},
|
|
ImageMetadata: artifact.ImageMetadata{
|
|
ID: "sha256:9c6f0724472873bb50a2ae67a9e7adcb57673a183cea8b06eb778dca859181b5",
|
|
DiffIDs: []string{
|
|
"sha256:994393dc58e7931862558d06e46aa2bb17487044f670f310dffe1d24e4d1eec7",
|
|
},
|
|
RepoTags: []string{
|
|
"alpine:3.16",
|
|
},
|
|
RepoDigests: []string{
|
|
"alpine@sha256:bc41182d7ef5ffc53a40b044e725193bc10142a1243f395ee852a8d9730fc2ad",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "attestation not found",
|
|
imageName: "test/image:10",
|
|
repoDigests: []string{
|
|
"test/image@sha256:123456e39f1e7a04e3f6da2d88b1c057e5657363c4f90679f3e8a071b7619e02",
|
|
},
|
|
wantErr: "remote SBOM fetching error",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ts := rekortest.NewServer(t)
|
|
defer ts.Close()
|
|
|
|
c := cachetest.NewCache(t, nil)
|
|
|
|
fi := &fakei.FakeImage{}
|
|
fi.ConfigFileReturns(&v1.ConfigFile{}, nil)
|
|
|
|
img := &fakeImage{
|
|
name: tt.imageName,
|
|
repoDigests: tt.repoDigests,
|
|
Image: fi,
|
|
}
|
|
a, err := image2.NewArtifact(img, c, artifact.Option{
|
|
SBOMSources: []string{"rekor"},
|
|
RekorURL: ts.URL(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
got, err := a.Inspect(t.Context())
|
|
if tt.wantErr != "" {
|
|
assert.ErrorContains(t, err, tt.wantErr)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
defer a.Clean(got)
|
|
|
|
got.BOM = nil
|
|
assert.Equal(t, tt.want, got)
|
|
cachetest.AssertBlobs(t, c, tt.wantBlobs)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Common test data for CycloneDX SBOM (used by OCI referrer tests)
|
|
var wantBlobsCycloneDX = []cachetest.WantBlob{
|
|
{
|
|
ID: "sha256:2171d8ccf798e94d09aca9c6abf15d28abd3236def1caa4a394b6f0a69c4266d",
|
|
BlobInfo: ftypes.BlobInfo{
|
|
SchemaVersion: ftypes.BlobJSONSchemaVersion,
|
|
Applications: []ftypes.Application{
|
|
{
|
|
Type: ftypes.GoBinary,
|
|
Packages: ftypes.Packages{
|
|
{
|
|
ID: "github.com/opencontainers/go-digest@v1.0.0",
|
|
Name: "github.com/opencontainers/go-digest",
|
|
Version: "v1.0.0",
|
|
Identifier: ftypes.PkgIdentifier{
|
|
PURL: &packageurl.PackageURL{
|
|
Type: packageurl.TypeGolang,
|
|
Namespace: "github.com/opencontainers",
|
|
Name: "go-digest",
|
|
Version: "v1.0.0",
|
|
},
|
|
BOMRef: "pkg:golang/github.com/opencontainers/go-digest@v1.0.0",
|
|
},
|
|
},
|
|
{
|
|
ID: "golang.org/x/sync@v0.1.0",
|
|
Name: "golang.org/x/sync",
|
|
Version: "v0.1.0",
|
|
Identifier: ftypes.PkgIdentifier{
|
|
PURL: &packageurl.PackageURL{
|
|
Type: packageurl.TypeGolang,
|
|
Namespace: "golang.org/x",
|
|
Name: "sync",
|
|
Version: "v0.1.0",
|
|
},
|
|
BOMRef: "pkg:golang/golang.org/x/sync@v0.1.0",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
func TestArtifact_InspectOCIReferrerSBOM(t *testing.T) {
|
|
// Start a test registry with referrers support
|
|
ts := registrytest.NewServer(t)
|
|
defer ts.Close()
|
|
|
|
u, err := url.Parse(ts.URL)
|
|
require.NoError(t, err)
|
|
registryHost := u.Host
|
|
|
|
tests := []struct {
|
|
name string
|
|
setup func(t *testing.T) (imageName string, repoDigests []string)
|
|
wantType ftypes.ArtifactType
|
|
wantID string
|
|
wantBlobs []cachetest.WantBlob
|
|
}{
|
|
{
|
|
name: "CycloneDX SBOM",
|
|
setup: func(t *testing.T) (string, []string) {
|
|
ref, subjectDesc := registrytest.PushRandomImage(t, registryHost, "test/cyclonedx", "latest")
|
|
|
|
sbomContent, err := os.ReadFile("testdata/cyclonedx.json")
|
|
require.NoError(t, err)
|
|
|
|
registrytest.PushReferrer(t, registryHost, "test/cyclonedx", subjectDesc, oci.CycloneDXArtifactType, sbomContent)
|
|
|
|
return ref.String(),
|
|
[]string{fmt.Sprintf("%s/test/cyclonedx@%s", registryHost, subjectDesc.Digest.String())}
|
|
},
|
|
wantType: ftypes.TypeCycloneDX,
|
|
wantID: "sha256:2171d8ccf798e94d09aca9c6abf15d28abd3236def1caa4a394b6f0a69c4266d",
|
|
wantBlobs: wantBlobsCycloneDX,
|
|
},
|
|
{
|
|
name: "Sigstore bundle",
|
|
setup: func(t *testing.T) (string, []string) {
|
|
ref, subjectDesc := registrytest.PushRandomImage(t, registryHost, "test/sigstore", "latest")
|
|
|
|
bundleContent, err := os.ReadFile("testdata/sigstore-bundle.json")
|
|
require.NoError(t, err)
|
|
|
|
registrytest.PushReferrer(t, registryHost, "test/sigstore", subjectDesc, sbom.SigstoreBundleMediaType, bundleContent)
|
|
|
|
return ref.String(),
|
|
[]string{fmt.Sprintf("%s/test/sigstore@%s", registryHost, subjectDesc.Digest.String())}
|
|
},
|
|
wantType: ftypes.TypeCycloneDX,
|
|
wantID: "sha256:2171d8ccf798e94d09aca9c6abf15d28abd3236def1caa4a394b6f0a69c4266d",
|
|
wantBlobs: wantBlobsCycloneDX,
|
|
},
|
|
{
|
|
name: "no referrers",
|
|
setup: func(t *testing.T) (string, []string) {
|
|
// Push image without any referrers
|
|
ref, subjectDesc := registrytest.PushRandomImage(t, registryHost, "test/no-referrers", "latest")
|
|
|
|
return ref.String(),
|
|
[]string{fmt.Sprintf("%s/test/no-referrers@%s", registryHost, subjectDesc.Digest.String())}
|
|
},
|
|
// Falls back to normal image scanning when no referrers found
|
|
wantType: ftypes.TypeContainerImage,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
imageName, repoDigests := tt.setup(t)
|
|
|
|
c := cachetest.NewCache(t, nil)
|
|
|
|
fi := &fakei.FakeImage{}
|
|
fi.ConfigFileReturns(&v1.ConfigFile{}, nil)
|
|
|
|
img := &fakeImage{
|
|
name: imageName,
|
|
repoDigests: repoDigests,
|
|
Image: fi,
|
|
}
|
|
a, err := image2.NewArtifact(img, c, artifact.Option{
|
|
SBOMSources: []string{"oci"},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
got, err := a.Inspect(t.Context())
|
|
require.NoError(t, err)
|
|
defer a.Clean(got)
|
|
|
|
assert.Equal(t, tt.wantType, got.Type)
|
|
if tt.wantID != "" {
|
|
assert.Equal(t, tt.wantID, got.ID)
|
|
}
|
|
if tt.wantBlobs != nil {
|
|
cachetest.AssertBlobs(t, c, tt.wantBlobs)
|
|
}
|
|
})
|
|
}
|
|
}
|