feat: include registry and repository in artifact ID calculation (#9689)

Co-authored-by: knqyf263 <knqyf263@users.noreply.github.com>
This commit is contained in:
Teppei Fukuda
2025-10-28 13:44:32 +04:00
committed by GitHub
parent eff52eb2e6
commit 758f271040
49 changed files with 804 additions and 109 deletions

View File

@@ -7,7 +7,6 @@ import (
"strings"
"testing"
"github.com/samber/lo"
"github.com/stretchr/testify/require"
"github.com/aquasecurity/trivy/internal/testutil"
@@ -227,29 +226,17 @@ func TestDockerEngine(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
imageName := tt.input
if !tt.invalidImage {
testfile, err := os.Open(tt.input)
require.NoError(t, err, tt.name)
defer testfile.Close()
// Ensure image doesn't already exist
cli.ImageRemove(t, ctx, tt.input)
// Load image into docker engine
loadedImage := cli.ImageLoad(t, ctx, tt.input)
// Tag our image to something unique
err = cli.ImageTag(ctx, loadedImage, tt.input)
require.NoError(t, err, tt.name)
// Cleanup
t.Cleanup(func() { cli.ImageRemove(t, ctx, tt.input) })
// Removes any existing images with conflicting RepoTags and loading images
imageName = cli.ImageCleanLoad(t, ctx, tt.input)
}
osArgs := []string{
"image",
imageName,
"--cache-dir",
cacheDir,
"image",
"--quiet",
"--skip-db-update",
"--format=json",
@@ -291,26 +278,22 @@ func TestDockerEngine(t *testing.T) {
}...)
}
osArgs = append(osArgs, tt.input)
// Run Trivy
runTest(t, osArgs, tt.golden, types.FormatJSON, runOptions{
wantErr: tt.wantErr,
fakeUUID: "3ff14136-e09f-4df9-80ea-%012d",
// Image config fields were removed
override: overrideFuncs(overrideUID, overrideDockerRemovedFields, overrideDockerEngineRepoTags),
override: overrideFuncs(overrideUID, overrideDockerRemovedFields, func(t *testing.T, want, got *types.Report) {
// Override ArtifactName to match the archive file path
got.ArtifactName = tt.input
// Override Result.Target for each result to match golden file expectations
require.Len(t, got.Results, len(want.Results))
for i := range got.Results {
got.Results[i].Target = want.Results[i].Target
}
}),
})
})
}
}
// overrideDockerEngineRepoTags removes test-specific RepoTags that are added during the test.
// In TestDockerEngine, we tag the loaded image with the archive file path (e.g., "testdata/fixtures/images/alpine-39.tar.gz")
// for test purposes. This function filters out these test tags from the actual results to match the expected
// RepoTags from the archive's manifest.json.
func overrideDockerEngineRepoTags(_ *testing.T, _, got *types.Report) {
// Keep only tags that don't start with "testdata/fixtures/images/"
got.Metadata.RepoTags = lo.Filter(got.Metadata.RepoTags, func(tag string, _ int) bool {
return !strings.HasPrefix(tag, "testdata/fixtures/images/")
})
}

View File

@@ -249,7 +249,13 @@ func TestRegistry(t *testing.T) {
runTest(t, osArgs, tt.golden, types.FormatJSON, runOptions{
wantErr: tt.wantErr,
fakeUUID: "3ff14136-e09f-4df9-80ea-%012d",
override: overrideFuncs(overrideUID, func(_ *testing.T, want, _ *types.Report) {
override: overrideFuncs(overrideUID, func(_ *testing.T, want, got *types.Report) {
// Exclude ArtifactID from comparison because registry tests use random ports
// (e.g., localhost:54321/alpine:3.10), which causes RepoTags and the calculated
// Artifact ID to vary on each test run.
got.ArtifactID = ""
want.ArtifactID = ""
want.ArtifactName = s
want.Metadata.RepoTags = []string{s}
for i := range want.Results {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:4ca63ce1d8a90da2ed4f2d5e93e8e9db2f32d0fabf0718a2edebbe0e70826622",
"ArtifactID": "sha256:fb75459277a4cbcf98182b48c789cfbd4b34414e05898e1231ae8b2ca099f4e7",
"ArtifactName": "testdata/fixtures/images/almalinux-8.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4",
"ArtifactID": "sha256:39549bf49d696f172a6513103cdc8f53717024ad1fbce62d680a8e7ddde1a612",
"ArtifactName": "testdata/fixtures/images/alpine-310.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:055936d3920576da37aa9bc460d70c5f212028bda1c08c0879aedf03d7a66ea1",
"ArtifactID": "sha256:988a8e3eb049d90c20fafb183d0e792c99b8ba28433be1d1e4447a8b5a1adbdf",
"ArtifactName": "testdata/fixtures/images/alpine-39.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:055936d3920576da37aa9bc460d70c5f212028bda1c08c0879aedf03d7a66ea1",
"ArtifactID": "sha256:988a8e3eb049d90c20fafb183d0e792c99b8ba28433be1d1e4447a8b5a1adbdf",
"ArtifactName": "testdata/fixtures/images/alpine-39.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:055936d3920576da37aa9bc460d70c5f212028bda1c08c0879aedf03d7a66ea1",
"ArtifactID": "sha256:988a8e3eb049d90c20fafb183d0e792c99b8ba28433be1d1e4447a8b5a1adbdf",
"ArtifactName": "testdata/fixtures/images/alpine-39.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:055936d3920576da37aa9bc460d70c5f212028bda1c08c0879aedf03d7a66ea1",
"ArtifactID": "sha256:988a8e3eb049d90c20fafb183d0e792c99b8ba28433be1d1e4447a8b5a1adbdf",
"ArtifactName": "testdata/fixtures/images/alpine-39.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:22848737c0d272ad5d7c7369d8ca830a62929e63e38edcb22085139a6ae0688d",
"ArtifactID": "sha256:0edd1906378dca3abc435f47f2e4b91059e9950e55cd82c76089d60b9ca68f90",
"ArtifactName": "testdata/fixtures/images/alpine-distroless.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:961c4ee06269351d858969ea0426878675ed708d3a140246eabbc0bfc352bffa",
"ArtifactID": "sha256:5a0fd7bb415c9b52d1bb909e40b9f498a89a5572724bd107d26ead4a25f203e1",
"ArtifactName": "testdata/fixtures/images/amazon-1.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:b94321659aca6a89cb7650a5b864bc8ec4bf62c620b8f1a01530c2e90a88c391",
"ArtifactID": "sha256:87e7ebcf8b5c0a26985fd80875e09e11850fa4828e1156da190f85b17dcecb71",
"ArtifactName": "testdata/fixtures/images/amazon-2.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:88702f6b6133bf06cc46af48437d0c0fc661239155548757c65916504a0e5eee",
"ArtifactID": "sha256:4f807ebe9dfe3b25af3d4354d6cff8288e9a8c63477dabcde5e63998f0e68188",
"ArtifactName": "testdata/fixtures/images/busybox-with-lockfile.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:5bf9684f472089d6d5cb636041d3d6dc748dbde39f1aefc374bbd367bd2aabbf",
"ArtifactID": "sha256:6fd360ff01eb63800aaafdb5e58af85fab9d7849344b577f5f6077cceb0399bb",
"ArtifactName": "testdata/fixtures/images/centos-6.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:f1cb7c7d58b73eac859c395882eec49d50651244e342cd6c68a5c7809785f427",
"ArtifactID": "sha256:8af996fb4a61e515887a173fdea3d5111c90c76e9f8247b3f668b17ab8215946",
"ArtifactName": "testdata/fixtures/images/centos-7.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:f1cb7c7d58b73eac859c395882eec49d50651244e342cd6c68a5c7809785f427",
"ArtifactID": "sha256:8af996fb4a61e515887a173fdea3d5111c90c76e9f8247b3f668b17ab8215946",
"ArtifactName": "testdata/fixtures/images/centos-7.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:f1cb7c7d58b73eac859c395882eec49d50651244e342cd6c68a5c7809785f427",
"ArtifactID": "sha256:8af996fb4a61e515887a173fdea3d5111c90c76e9f8247b3f668b17ab8215946",
"ArtifactName": "testdata/fixtures/images/centos-7.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:c2c03a296d2329a4f3ab72a7bf38b78a8a80108204d326b0139d6af700e152d1",
"ArtifactID": "sha256:b75d2c78a42eae6eaa44e99638ba5fa36900538fa0b7a4feba19d18dd588552d",
"ArtifactName": "testdata/fixtures/images/debian-buster.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:c2c03a296d2329a4f3ab72a7bf38b78a8a80108204d326b0139d6af700e152d1",
"ArtifactID": "sha256:b75d2c78a42eae6eaa44e99638ba5fa36900538fa0b7a4feba19d18dd588552d",
"ArtifactName": "testdata/fixtures/images/debian-buster.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:f26939cc87ef44a6fc554eedd0a976ab30b5bc2769d65d2e986b6c5f1fd4053d",
"ArtifactID": "sha256:e7f0b65012f754f3e69bfa9e94999ba080f12cd7e6ac2742d5cb908252f9609f",
"ArtifactName": "testdata/fixtures/images/debian-stretch.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:7f04a8d247173b1f2546d22913af637bbab4e7411e00ae6207da8d94c445750d",
"ArtifactID": "sha256:6c3688715cb42ea1466b96eb45b39d8afc9f8cdcf723df8464fb26391711a7db",
"ArtifactName": "testdata/fixtures/images/distroless-base.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:6fcac2cc8a710f21577b5bbd534e0bfc841c0cca569b57182ba19054696cddda",
"ArtifactID": "sha256:b9ec0b7f93064fddace988e9a901386ccd55a6c16a34c00d5c45b06f62ec20ca",
"ArtifactName": "testdata/fixtures/images/distroless-python27.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:5a992077baba51b97f27591a10d54d2f2723dc9c81a3fe419e261023f2554933",
"ArtifactID": "sha256:7a550fb73ac2bf3e1fe50c96a8a5ba699be62cbb09ba9bd982557e574c904b3d",
"ArtifactName": "testdata/fixtures/images/fluentd-multiple-lockfiles.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:8cdcbf18341ed8afa5322e7b0077f8ef3f46896882c921df5f97c51b369f6767",
"ArtifactID": "sha256:357e30db7fb673e279ababa1128b33c8acc2fce50826727ec57ef24f5213fe0d",
"ArtifactName": "testdata/fixtures/images/mariner-1.0.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:fef5ad254f6378f08071cfa2daaf05a1ce9857141c944b67a40742e63e65cecc",
"ArtifactID": "sha256:b9f31768f6c3908af7de80cff1a9e53c62f46d1bc54aaf3b97b2c922ed9cf1fd",
"ArtifactName": "testdata/fixtures/images/opensuse-leap-151.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:580e73f5c823232e6587136e9f5428a89afdf77a123bb8575d08208e0cc34b12",
"ArtifactID": "sha256:1ab435ff0da0a8c95292bfd8a3b270b457cfca575a4a68731dd2fc142e2c13ef",
"ArtifactName": "testdata/fixtures/images/opensuse-tumbleweed.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:8988c7081e1f7b6c2928cbc4832b8a05968bb589d45d444ca1e3027c68f97f56",
"ArtifactID": "sha256:2982b2b5fd16f59d6b9ccdef6292e710791eb7fa12895a593959c5bceb7780e6",
"ArtifactName": "testdata/fixtures/images/oraclelinux-8.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:5ccb5186b75cd13ff0d028f5b5b2bdf7ef7ca2b3d56eb2c6eb6c136077a6991a",
"ArtifactID": "sha256:1d4bc53b38b27a97aca270bea3abf3e9ea14964d02c71d2c93cd7bc53b74d660",
"ArtifactName": "testdata/fixtures/images/photon-30.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:210996f98b856d7cd00496ddbe9412e73f1c714c95de09661e07b4e43648f9ab",
"ArtifactID": "sha256:ed11998b28b0bcd0488c4d2fda300f80d71ac058ce7eb12a43a4deb312ce429c",
"ArtifactName": "testdata/fixtures/images/rockylinux-8.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:c45ec974938acac29c893b5d273d73e4ebdd7e6a97b6fa861dfbd8dd430b9016",
"ArtifactID": "sha256:f3d716e4652bf4a60bf4289eace7bb2b46878fa9461c86b12b4c059126563aee",
"ArtifactName": "testdata/fixtures/images/sle-micro-rancher-5.4_ndb.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:ed8f0747d483b60657982f0ef1ba74482aed08795cf0eb774b00bc53022a8351",
"ArtifactID": "sha256:d5fbfb11da0ffb72f8bdeae29420a8101a04534c281854fe5ace22ec13d133bb",
"ArtifactName": "testdata/fixtures/images/spring4shell-jre11.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:b88bc3d2f0b5aacf1d36efa498f427d923b01c854dac090acf5368c55ac04fda",
"ArtifactID": "sha256:8ac64b751688d0e2577befd47be60baea54f7294bee6deeb0288b580daf6ca52",
"ArtifactName": "testdata/fixtures/images/spring4shell-jre8.tar.gz",
"ArtifactType": "container_image",
"Metadata": {
@@ -308,15 +308,6 @@
"Target": "",
"Class": "custom",
"CustomResources": [
{
"Type": "spring4shell/tomcat-version",
"FilePath": "/usr/local/tomcat/RELEASE-NOTES",
"Layer": {
"Digest": "sha256:59c0978ccb117247fd40d936973c40df89195f60466118c5acc6a55f8ba29f06",
"DiffID": "sha256:85595543df2b1115a18284a8ef62d0b235c4bc29e3d33b55f89b54ee1eadf4c6"
},
"Data": "8.5.77"
},
{
"Type": "spring4shell/java-major-version",
"FilePath": "/usr/local/openjdk-8/release",
@@ -325,6 +316,15 @@
"DiffID": "sha256:ba40706eccba610401e4942e29f50bdf36807f8638942ce20805b359ae3ac1c1"
},
"Data": "1.8.0_322"
},
{
"Type": "spring4shell/tomcat-version",
"FilePath": "/usr/local/tomcat/RELEASE-NOTES",
"Layer": {
"Digest": "sha256:59c0978ccb117247fd40d936973c40df89195f60466118c5acc6a55f8ba29f06",
"DiffID": "sha256:85595543df2b1115a18284a8ef62d0b235c4bc29e3d33b55f89b54ee1eadf4c6"
},
"Data": "8.5.77"
}
]
}

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:6fecccc91c83e11ae4fede6793e9410841221d4779520c2b9e9fb7f7b3830264",
"ArtifactID": "sha256:b4b4762f4769903a61d4605927079c4e60d007d9b12f65c39714e5311fa3d0ca",
"ArtifactName": "testdata/fixtures/images/ubi-7.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:6fecccc91c83e11ae4fede6793e9410841221d4779520c2b9e9fb7f7b3830264",
"ArtifactID": "sha256:b4b4762f4769903a61d4605927079c4e60d007d9b12f65c39714e5311fa3d0ca",
"ArtifactName": "testdata/fixtures/images/ubi-7.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:a2a15febcdf362f6115e801d37b5e60d6faaeedcb9896155e5fe9d754025be12",
"ArtifactID": "sha256:61d0ff5073e754270b3b59487ac8049c50e12bca1cf792d84ec9a62dad36a8a1",
"ArtifactName": "testdata/fixtures/images/ubuntu-1804.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -2,7 +2,7 @@
"SchemaVersion": 2,
"ReportID": "3ff14136-e09f-4df9-80ea-000000000001",
"CreatedAt": "2021-08-25T12:20:30.000000005Z",
"ArtifactID": "sha256:a2a15febcdf362f6115e801d37b5e60d6faaeedcb9896155e5fe9d754025be12",
"ArtifactID": "sha256:61d0ff5073e754270b3b59487ac8049c50e12bca1cf792d84ec9a62dad36a8a1",
"ArtifactName": "testdata/fixtures/images/ubuntu-1804.tar.gz",
"ArtifactType": "container_image",
"Metadata": {

View File

@@ -3,13 +3,17 @@ package testutil
import (
"context"
"encoding/json"
"io"
"os"
"strings"
"testing"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/client"
"github.com/google/go-containerregistry/pkg/v1/tarball"
"github.com/stretchr/testify/require"
gzutil "github.com/aquasecurity/trivy/pkg/fanal/utils/gzip"
)
type DockerClient struct {
@@ -22,6 +26,8 @@ func NewDockerClient(t *testing.T) *DockerClient {
return &DockerClient{Client: cli}
}
// ImageLoad loads a Docker image from a tar archive file into the Docker engine.
// It automatically registers cleanup via t.Cleanup() to remove the loaded image after the test.
func (c *DockerClient) ImageLoad(t *testing.T, ctx context.Context, imageFile string) string {
t.Helper()
testfile, err := os.Open(imageFile)
@@ -43,6 +49,7 @@ func (c *DockerClient) ImageLoad(t *testing.T, ctx context.Context, imageFile st
loadedImage = strings.TrimSpace(loadedImage)
require.NotEmpty(t, loadedImage, data.Stream)
// Register cleanup to remove the loaded image after the test
t.Cleanup(func() { c.ImageRemove(t, ctx, loadedImage) })
return loadedImage
@@ -55,3 +62,29 @@ func (c *DockerClient) ImageRemove(t *testing.T, ctx context.Context, imageID st
PruneChildren: true,
})
}
// ImageCleanLoad performs a clean load of a Docker image from a tar archive.
// It removes any existing images with conflicting RepoTags before loading,
// ensuring the loaded image has the correct RepoTags from the archive.
// It automatically registers cleanup via t.Cleanup() to remove the loaded image after the test.
func (c *DockerClient) ImageCleanLoad(t *testing.T, ctx context.Context, archivePath string) string {
t.Helper()
// Extract RepoTags from archive
opener := func() (io.ReadCloser, error) {
return gzutil.OpenFile(archivePath)
}
manifest, err := tarball.LoadManifest(opener)
require.NoError(t, err, "failed to load manifest from archive")
// Remove existing images with the same RepoTags to avoid conflicts
for _, m := range manifest {
for _, tag := range m.RepoTags {
c.ImageRemove(t, ctx, tag)
}
}
// Load image
return c.ImageLoad(t, ctx, archivePath)
}

View File

@@ -7,6 +7,11 @@ import (
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/aquasecurity/trivy/pkg/fanal/image"
)
var (
@@ -65,3 +70,11 @@ func imageName(img, subpath, tag, digest string) string {
}
return img
}
// MustParseReference parses a string into a Reference and fails the test if there's an error
func MustParseReference(t *testing.T, s string) image.Reference {
t.Helper()
ref, err := image.ParseReference(s)
require.NoError(t, err)
return ref
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/google/go-containerregistry/pkg/v1"
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
"github.com/aquasecurity/trivy/pkg/fanal/image"
"github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/fanal/walker"
"github.com/aquasecurity/trivy/pkg/misconf"
@@ -103,6 +104,7 @@ type ImageMetadata struct {
DiffIDs []string // uncompressed layer IDs
RepoTags []string
RepoDigests []string
Reference image.Reference // image reference matching the artifact name
ConfigFile v1.ConfigFile
}

View File

@@ -1,6 +1,7 @@
package image
import (
"cmp"
"context"
"errors"
"fmt"
@@ -12,6 +13,7 @@ import (
"strings"
"github.com/docker/go-units"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/samber/lo"
"golang.org/x/sync/errgroup"
@@ -146,6 +148,10 @@ func (a Artifact) Inspect(ctx context.Context) (ref artifact.Reference, err erro
return artifact.Reference{}, xerrors.Errorf("analyze error: %w", err)
}
repoTags := a.image.RepoTags()
repoDigests := a.image.RepoDigests()
imgRef := a.findMatchingReference(a.image.Name(), repoTags, repoDigests)
return artifact.Reference{
Name: a.image.Name(),
Type: types.TypeContainerImage,
@@ -154,8 +160,9 @@ func (a Artifact) Inspect(ctx context.Context) (ref artifact.Reference, err erro
ImageMetadata: artifact.ImageMetadata{
ID: imageID,
DiffIDs: diffIDs,
RepoTags: a.image.RepoTags(),
RepoDigests: a.image.RepoDigests(),
RepoTags: repoTags,
RepoDigests: repoDigests,
Reference: imgRef,
ConfigFile: *configFile,
},
}, nil
@@ -165,6 +172,101 @@ func (a Artifact) Clean(_ artifact.Reference) error {
return nil
}
// findMatchingReference finds a RepoTag or RepoDigest that matches the artifact name
func (a Artifact) findMatchingReference(artifactName string, repoTags, repoDigests []string) image.Reference {
// Convert strings to typed references
parsedTags := a.parseRepoTags(repoTags)
parsedDigests := a.parseRepoDigests(repoDigests)
ref := a.findMatchingRepoReference(artifactName, parsedTags, parsedDigests)
return image.NewReference(ref)
}
// parseRepoTags parses repo tags into name.Tag
func (a Artifact) parseRepoTags(repoTags []string) []name.Tag {
return lo.FilterMap(repoTags, func(tagStr string, _ int) (name.Tag, bool) {
tag, err := name.NewTag(tagStr)
if err != nil {
a.logger.Debug("Failed to parse repo tag", log.String("tag", tagStr), log.Err(err))
return name.Tag{}, false
}
return tag, true
})
}
// parseRepoDigests parses repo digests into name.Digest
func (a Artifact) parseRepoDigests(repoDigests []string) []name.Digest {
return lo.FilterMap(repoDigests, func(digestStr string, _ int) (name.Digest, bool) {
digest, err := name.NewDigest(digestStr)
if err != nil {
a.logger.Debug("Failed to parse repo digest", log.String("digest", digestStr), log.Err(err))
return name.Digest{}, false
}
return digest, true
})
}
// findMatchingRepoReference finds a RepoTag or RepoDigest that matches the artifact name
func (a Artifact) findMatchingRepoReference(artifactName string, repoTags []name.Tag, repoDigests []name.Digest) name.Reference {
// If there are no RepoTags or RepoDigests, return nil
if len(repoTags) == 0 && len(repoDigests) == 0 {
return nil
}
// Select the first available reference as fallback
fallback := cmp.Or[name.Reference](lo.FirstOrEmpty(repoTags), lo.FirstOrEmpty(repoDigests))
// TODO(knqyf263): refactor to use a more robust method instead of suffix-based detection
// Check if artifact name looks like a file path (tar archive)
archiveExts := []string{
".tar",
".gz",
".gzip",
".tgz",
}
ext := strings.ToLower(filepath.Ext(artifactName))
if slices.Contains(archiveExts, ext) {
// For file paths, use the first RepoTag or RepoDigest
return fallback
}
// Try to parse the artifact name as an image reference
artifactRef, err := name.ParseReference(artifactName)
if err != nil {
// If parsing fails, use the first RepoTag or RepoDigest
a.logger.Debug("Failed to parse artifact name as image reference, using first RepoTag",
log.String("name", artifactName), log.Err(err))
return fallback
}
artifactRefName := artifactRef.Name()
switch artifactRef.(type) {
case name.Digest:
// Try to find a matching digest from RepoDigests
if digest, ok := lo.Find(repoDigests, func(d name.Digest) bool {
return artifactRefName == d.Name()
}); ok {
return digest
}
case name.Tag:
// Try to find a matching tag from RepoTags
if tag, ok := lo.Find(repoTags, func(t name.Tag) bool {
return artifactRefName == t.Name()
}); ok {
return tag
}
}
// If no matching tag/digest found, use the first RepoTag or RepoDigest as fallback
// This also handles the case when the artifact is specified by image ID (e.g., `trivy image sha256:abc123`)
a.logger.Debug("No matching repo tag/digest found for artifact, using first one",
log.String("name", artifactName),
log.String("ref", artifactRefName),
log.String("fallback", fallback.String()))
return fallback
}
func (a Artifact) calcCacheKeys(imageID string, diffIDs []string) (string, []string, error) {
// Pass an empty config scanner option so that the cache key can be the same, even when policies are updated.
imageKey, err := cache.CalcKey(imageID, artifactVersion, a.configAnalyzer.AnalyzerVersions(), nil, artifact.Option{})

View File

@@ -12,6 +12,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/aquasecurity/trivy/internal/cachetest"
"github.com/aquasecurity/trivy/internal/testutil"
"github.com/aquasecurity/trivy/pkg/cache"
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
"github.com/aquasecurity/trivy/pkg/fanal/artifact"
@@ -444,6 +445,7 @@ func TestArtifact_Inspect(t *testing.T) {
},
RepoTags: []string{"alpine:3.11"},
RepoDigests: nil,
Reference: testutil.MustParseReference(t, "alpine:3.11"),
ConfigFile: v1.ConfigFile{
Architecture: "amd64",
Author: "",
@@ -1776,6 +1778,7 @@ func TestArtifact_Inspect(t *testing.T) {
},
RepoTags: []string{"vuln-image:latest"},
RepoDigests: nil,
Reference: testutil.MustParseReference(t, "vuln-image:latest"),
ConfigFile: v1.ConfigFile{
Architecture: "amd64",
Author: "",
@@ -1933,6 +1936,7 @@ func TestArtifact_Inspect(t *testing.T) {
},
RepoTags: []string{"vuln-image:latest"},
RepoDigests: nil,
Reference: testutil.MustParseReference(t, "vuln-image:latest"),
ConfigFile: v1.ConfigFile{
Architecture: "amd64",
Author: "",

View File

@@ -1,17 +1,14 @@
package image
import (
"bufio"
"compress/gzip"
"io"
"os"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/tarball"
"github.com/samber/lo"
"golang.org/x/xerrors"
"github.com/aquasecurity/trivy/pkg/fanal/utils"
gzutil "github.com/aquasecurity/trivy/pkg/fanal/utils/gzip"
)
type dockerArchive struct {
@@ -42,22 +39,6 @@ func tryDockerArchive(fileName string) (v1.Image, error) {
func fileOpener(fileName string) func() (io.ReadCloser, error) {
return func() (io.ReadCloser, error) {
f, err := os.Open(fileName)
if err != nil {
return nil, xerrors.Errorf("unable to open the file: %w", err)
}
var r io.Reader
br := bufio.NewReader(f)
r = br
if utils.IsGzip(br) {
r, err = gzip.NewReader(br)
if err != nil {
_ = f.Close()
return nil, xerrors.Errorf("failed to open gzip: %w", err)
}
}
return io.NopCloser(r), nil
return gzutil.OpenFile(fileName)
}
}

View File

@@ -0,0 +1,62 @@
package image
import (
"encoding/json"
"github.com/google/go-containerregistry/pkg/name"
"github.com/samber/lo"
"golang.org/x/xerrors"
)
// Reference wraps name.Reference to support JSON marshaling/unmarshaling
type Reference struct {
name.Reference
}
// NewReference creates a new Reference from name.Reference
func NewReference(ref name.Reference) Reference {
return Reference{Reference: ref}
}
// ParseReference parses a string into a Reference
func ParseReference(s string) (Reference, error) {
if s == "" {
return Reference{}, nil
}
ref, err := name.ParseReference(s)
if err != nil {
return Reference{}, xerrors.Errorf("failed to parse reference: %w", err)
}
return Reference{Reference: ref}, nil
}
// MarshalJSON implements json.Marshaler
func (r Reference) MarshalJSON() ([]byte, error) {
if lo.IsNil(r.Reference) {
return json.Marshal("")
}
return json.Marshal(r.Reference.String())
}
// UnmarshalJSON implements json.Unmarshaler
func (r *Reference) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return xerrors.Errorf("failed to unmarshal reference: %w", err)
}
if s == "" {
r.Reference = nil
return nil
}
ref, err := name.ParseReference(s)
if err != nil {
return xerrors.Errorf("failed to parse reference: %w", err)
}
r.Reference = ref
return nil
}
// IsEmpty returns true if the reference is empty
func (r Reference) IsEmpty() bool {
return lo.IsNil(r.Reference)
}

View File

@@ -0,0 +1,198 @@
package image_test
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/aquasecurity/trivy/internal/testutil"
"github.com/aquasecurity/trivy/pkg/fanal/image"
)
func TestReference_MarshalJSON(t *testing.T) {
tests := []struct {
name string
ref image.Reference
want string
}{
{
name: "valid reference with tag",
ref: testutil.MustParseReference(t, "ghcr.io/aquasecurity/trivy:latest"),
want: `"ghcr.io/aquasecurity/trivy:latest"`,
},
{
name: "valid reference with digest",
ref: testutil.MustParseReference(t, "ghcr.io/aquasecurity/trivy@sha256:0000000000000000000000000000000000000000000000000000000000000000"),
want: `"ghcr.io/aquasecurity/trivy@sha256:0000000000000000000000000000000000000000000000000000000000000000"`,
},
{
name: "empty reference",
ref: image.Reference{},
want: `""`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := json.Marshal(tt.ref)
require.NoError(t, err)
assert.Equal(t, tt.want, string(got))
})
}
}
func TestReference_UnmarshalJSON(t *testing.T) {
tests := []struct {
name string
json string
want string
wantIsEmpty bool
wantErr assert.ErrorAssertionFunc
}{
{
name: "valid reference with tag",
json: `"ghcr.io/aquasecurity/trivy:latest"`,
want: "ghcr.io/aquasecurity/trivy:latest",
wantIsEmpty: false,
wantErr: assert.NoError,
},
{
name: "valid reference with digest",
json: `"ghcr.io/aquasecurity/trivy@sha256:0000000000000000000000000000000000000000000000000000000000000000"`,
want: "ghcr.io/aquasecurity/trivy@sha256:0000000000000000000000000000000000000000000000000000000000000000",
wantIsEmpty: false,
wantErr: assert.NoError,
},
{
name: "empty reference",
json: `""`,
want: "",
wantIsEmpty: true,
wantErr: assert.NoError,
},
{
name: "invalid reference",
json: `"not a valid reference!"`,
wantErr: assert.Error,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var r image.Reference
err := json.Unmarshal([]byte(tt.json), &r)
tt.wantErr(t, err)
if err != nil {
return
}
assert.Equal(t, tt.wantIsEmpty, r.IsEmpty())
if !r.IsEmpty() {
assert.Equal(t, tt.want, r.String())
}
})
}
}
func TestReference_String(t *testing.T) {
tests := []struct {
name string
ref image.Reference
want string
}{
{
name: "ghcr.io with tag",
ref: testutil.MustParseReference(t, "ghcr.io/aquasecurity/trivy:latest"),
want: "ghcr.io/aquasecurity/trivy:latest",
},
{
name: "ghcr.io with digest",
ref: testutil.MustParseReference(t, "ghcr.io/aquasecurity/trivy@sha256:0000000000000000000000000000000000000000000000000000000000000000"),
want: "ghcr.io/aquasecurity/trivy@sha256:0000000000000000000000000000000000000000000000000000000000000000",
},
{
name: "docker.io implicit",
ref: testutil.MustParseReference(t, "aquasecurity/trivy:latest"),
want: "aquasecurity/trivy:latest",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, tt.ref.String())
})
}
}
func TestReference_Context(t *testing.T) {
tests := []struct {
name string
ref image.Reference
want string
}{
{
name: "ghcr.io with tag",
ref: testutil.MustParseReference(t, "ghcr.io/aquasecurity/trivy:latest"),
want: "ghcr.io/aquasecurity/trivy",
},
{
name: "ghcr.io with digest",
ref: testutil.MustParseReference(t, "ghcr.io/aquasecurity/trivy@sha256:0000000000000000000000000000000000000000000000000000000000000000"),
want: "ghcr.io/aquasecurity/trivy",
},
{
name: "docker.io implicit",
ref: testutil.MustParseReference(t, "aquasecurity/trivy:latest"),
want: "index.docker.io/aquasecurity/trivy",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, tt.ref.Context().String())
})
}
}
func TestReference_IsEmpty(t *testing.T) {
tests := []struct {
name string
ref image.Reference
want bool
}{
{
name: "non-empty reference",
ref: testutil.MustParseReference(t, "ghcr.io/aquasecurity/trivy:latest"),
want: false,
},
{
name: "empty reference",
ref: image.Reference{},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, tt.ref.IsEmpty())
})
}
}
func TestReference_JSONRoundTrip(t *testing.T) {
ref := testutil.MustParseReference(t, "ghcr.io/aquasecurity/trivy:v0.65.0")
// Marshal to JSON
data, err := json.Marshal(ref)
require.NoError(t, err)
// Unmarshal from JSON
var decoded image.Reference
err = json.Unmarshal(data, &decoded)
require.NoError(t, err)
// Verify the decoded reference matches the original
assert.Equal(t, ref.String(), decoded.String())
assert.Equal(t, ref.Context().String(), decoded.Context().String())
}

View File

@@ -310,6 +310,7 @@ func localImageTestWithNamespace(t *testing.T, namespace string) {
},
RepoTags: []string{testutil.ImageName("", "alpine-310", "")},
RepoDigests: []string{testutil.ImageName("", "", "sha256:f12582b2f2190f350e3904462c1c23aaf366b4f76705e97b199f9bbded1d816a")},
Reference: testutil.MustParseReference(t, testutil.ImageName("", "alpine-310", "")),
ConfigFile: v1.ConfigFile{
Architecture: "amd64",
Created: v1.Time{
@@ -354,6 +355,7 @@ func localImageTestWithNamespace(t *testing.T, namespace string) {
tarArchive: "../../../../integration/testdata/fixtures/images/vulnimage.tar.gz",
wantMetadata: artifact.ImageMetadata{
ID: "sha256:c17083664da903e13e9092fa3a3a1aeee2431aa2728298e3dbcec72f26369c41",
Reference: testutil.MustParseReference(t, testutil.ImageName("", "vulnimage", "")),
DiffIDs: []string{
"sha256:ebf12965380b39889c99a9c02e82ba465f887b45975b6e389d42e9e6a3857888",
"sha256:0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33",
@@ -765,6 +767,7 @@ func TestContainerd_PullImage(t *testing.T) {
},
RepoTags: []string{testutil.ImageName("", "alpine-310", "")},
RepoDigests: []string{testutil.ImageName("", "", "sha256:72c42ed48c3a2db31b7dafe17d275b634664a708d901ec9fd57b1529280f01fb")},
Reference: testutil.MustParseReference(t, testutil.ImageName("", "alpine-310", "")),
ConfigFile: v1.ConfigFile{
Architecture: "amd64",
Created: v1.Time{

View File

@@ -0,0 +1,54 @@
package gzip
import (
"bufio"
"compress/gzip"
"io"
"os"
"golang.org/x/xerrors"
"github.com/aquasecurity/trivy/pkg/fanal/utils"
)
// multiCloser wraps a reader and manages multiple closers for proper cleanup
type multiCloser struct {
io.Reader
closers []io.Closer
}
func (mc *multiCloser) Close() error {
for _, c := range mc.closers {
if err := c.Close(); err != nil {
return err
}
}
return nil
}
// OpenFile opens a file (optionally gzipped) by file path
func OpenFile(fileName string) (io.ReadCloser, error) {
f, err := os.Open(fileName)
if err != nil {
return nil, xerrors.Errorf("unable to open the file: %w", err)
}
mc := &multiCloser{
closers: []io.Closer{f},
}
br := bufio.NewReader(f)
mc.Reader = br
if utils.IsGzip(br) {
gzr, err := gzip.NewReader(br)
if err != nil {
_ = f.Close()
return nil, xerrors.Errorf("failed to open gzip: %w", err)
}
mc.Reader = gzr
mc.closers = append(mc.closers, gzr)
}
return mc, nil
}

View File

@@ -114,8 +114,32 @@ func (s Service) ScanArtifact(ctx context.Context, options types.ScanOptions) (t
func (s Service) generateArtifactID(artifactInfo artifact.Reference) string {
switch artifactInfo.Type {
case ftypes.TypeContainerImage:
// Use image ID directly
return artifactInfo.ImageMetadata.ID
// For container images, calculate hash(ImageID + Registry + Repository)
// to ensure same images in different repos/registries have different IDs.
// Note: The artifact ID does NOT include the tag or digest, only registry/repository,
// so the same image with different tags will have the same artifact ID.
imageID := artifactInfo.ImageMetadata.ID
if imageID == "" {
return ""
}
// Use the Reference field if available
ref := artifactInfo.ImageMetadata.Reference
if ref.IsEmpty() {
// Reference is empty when RepoTags and RepoDigests are both empty.
// This happens in the following cases:
// 1. Images built without tags (e.g., "docker build ." without -t flag)
// 2. Images saved by ID (e.g., "docker save <image-id>" or "docker save sha256:xxx")
// In these cases, fall back to using the image ID directly.
log.Debug("No image reference available for artifact ID calculation, using image ID directly",
log.String("image", artifactInfo.Name))
return imageID
}
// ref.Context() returns registry/repository (e.g., "index.docker.io/library/alpine")
data := fmt.Sprintf("%s:%s", imageID, ref.Context())
hash := sha256.Sum256([]byte(data))
return fmt.Sprintf("sha256:%x", hash)
case ftypes.TypeRepository:
// Generate ID from repository URL and commit hash combination

View File

@@ -0,0 +1,223 @@
package scan
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/aquasecurity/trivy/internal/testutil"
"github.com/aquasecurity/trivy/pkg/fanal/artifact"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
)
func TestService_generateArtifactID(t *testing.T) {
tests := []struct {
name string
artifactInfo artifact.Reference
want string
}{
{
name: "container image with valid reference",
artifactInfo: artifact.Reference{
Name: "ghcr.io/aquasecurity/trivy:latest",
Type: ftypes.TypeContainerImage,
ImageMetadata: artifact.ImageMetadata{
ID: "sha256:abc123",
Reference: testutil.MustParseReference(t, "ghcr.io/aquasecurity/trivy:latest"),
},
},
want: "sha256:58a3381def2cec86309c94be4fbeaca4b6c0231743ed1df9b0bea883a33cdebb",
},
{
name: "same image with different tag should have same artifact ID",
artifactInfo: artifact.Reference{
Name: "ghcr.io/aquasecurity/trivy:v0.65.0",
Type: ftypes.TypeContainerImage,
ImageMetadata: artifact.ImageMetadata{
ID: "sha256:abc123",
Reference: testutil.MustParseReference(t, "ghcr.io/aquasecurity/trivy:v0.65.0"),
},
},
want: "sha256:58a3381def2cec86309c94be4fbeaca4b6c0231743ed1df9b0bea883a33cdebb",
},
{
name: "different repository should have different artifact ID",
artifactInfo: artifact.Reference{
Name: "ghcr.io/aqua-sec/trivy:v0.65.0",
Type: ftypes.TypeContainerImage,
ImageMetadata: artifact.ImageMetadata{
ID: "sha256:abc123",
Reference: testutil.MustParseReference(t, "ghcr.io/aqua-sec/trivy:v0.65.0"),
},
},
want: "sha256:bf73a838ae6a9d9c3018fbc7b628741f3be920b75c011a49c0b192736eb789b1",
},
{
name: "different registry should have different artifact ID",
artifactInfo: artifact.Reference{
Name: "docker.io/aquasecurity/trivy:v0.65.0",
Type: ftypes.TypeContainerImage,
ImageMetadata: artifact.ImageMetadata{
ID: "sha256:abc123",
Reference: testutil.MustParseReference(t, "docker.io/aquasecurity/trivy:v0.65.0"),
},
},
want: "sha256:dcba426e1fbd6e7fda125be3b9a2507ce3da2c7954c2edbf0e06e34d7f0ca22f",
},
{
name: "docker.io implicit (no registry)",
artifactInfo: artifact.Reference{
Name: "aquasecurity/trivy:latest",
Type: ftypes.TypeContainerImage,
ImageMetadata: artifact.ImageMetadata{
ID: "sha256:abc123",
Reference: testutil.MustParseReference(t, "aquasecurity/trivy:latest"),
},
},
want: "sha256:dcba426e1fbd6e7fda125be3b9a2507ce3da2c7954c2edbf0e06e34d7f0ca22f",
},
{
name: "docker.io official image",
artifactInfo: artifact.Reference{
Name: "alpine:3.10",
Type: ftypes.TypeContainerImage,
ImageMetadata: artifact.ImageMetadata{
ID: "sha256:alpine123",
Reference: testutil.MustParseReference(t, "alpine:3.10"),
},
},
want: "sha256:56de33d7ec6a1f832c9a7b2a26b1870efe79198e1c13ac645d43798c90954bb5",
},
{
name: "localhost with port",
artifactInfo: artifact.Reference{
Name: "localhost:5000/myapp:latest",
Type: ftypes.TypeContainerImage,
ImageMetadata: artifact.ImageMetadata{
ID: "sha256:local123",
Reference: testutil.MustParseReference(t, "localhost:5000/myapp:latest"),
},
},
want: "sha256:7cbf1bbde2285bac7c810fb76da5b0476d284f320f50b913987d6fc9226dc3e3",
},
{
name: "multi-level repository",
artifactInfo: artifact.Reference{
Name: "gcr.io/my-org/my-team/my-app:v1.0.0",
Type: ftypes.TypeContainerImage,
ImageMetadata: artifact.ImageMetadata{
ID: "sha256:gcr123",
Reference: testutil.MustParseReference(t, "gcr.io/my-org/my-team/my-app:v1.0.0"),
},
},
want: "sha256:edb01f579a800df17687439f1115bf4ced7bb977aa6afd468675ec56145a530c",
},
{
name: "same image with different digest should have same artifact ID",
artifactInfo: artifact.Reference{
Name: "ghcr.io/aquasecurity/trivy@sha256:0000000000000000000000000000000000000000000000000000000000000000",
Type: ftypes.TypeContainerImage,
ImageMetadata: artifact.ImageMetadata{
ID: "sha256:abc123",
Reference: testutil.MustParseReference(t, "ghcr.io/aquasecurity/trivy@sha256:0000000000000000000000000000000000000000000000000000000000000000"),
},
},
want: "sha256:58a3381def2cec86309c94be4fbeaca4b6c0231743ed1df9b0bea883a33cdebb",
},
{
name: "image with digest (no reference)",
artifactInfo: artifact.Reference{
Name: "ghcr.io/aquasecurity/trivy@sha256:abc123",
Type: ftypes.TypeContainerImage,
ImageMetadata: artifact.ImageMetadata{
ID: "sha256:abc123",
// No reference for digest case (empty)
},
},
want: "sha256:abc123",
},
{
name: "container image with no image ID",
artifactInfo: artifact.Reference{
Name: "ghcr.io/aquasecurity/trivy:latest",
Type: ftypes.TypeContainerImage,
ImageMetadata: artifact.ImageMetadata{
ID: "",
// No reference
},
},
want: "",
},
{
name: "container image with tar archive (uses RepoTag)",
artifactInfo: artifact.Reference{
Name: "../fanal/test/testdata/alpine-311.tar.gz",
Type: ftypes.TypeContainerImage,
ImageMetadata: artifact.ImageMetadata{
ID: "sha256:fallback123",
Reference: testutil.MustParseReference(t, "alpine:3.11"),
},
},
want: "sha256:a840c3e6bbadd213fee8cf6e4c32082f06541b8792a929fd373a57e5af0e8fa5",
},
{
name: "repository with URL and commit",
artifactInfo: artifact.Reference{
Name: "myrepo",
Type: ftypes.TypeRepository,
RepoMetadata: artifact.RepoMetadata{
RepoURL: "https://github.com/aquasecurity/trivy",
Commit: "abc123def456",
},
},
want: "sha256:e23a8c4bae6c00f26ebf52d59e70ddfbbf5b2916d089239c3224f7f06371af98",
},
{
name: "repository with only commit",
artifactInfo: artifact.Reference{
Name: "/path/to/local/repo",
Type: ftypes.TypeRepository,
RepoMetadata: artifact.RepoMetadata{
Commit: "abc123def456",
},
},
want: "sha256:9183de2823d60a525ed7aeabdb2cda775cba82dd5da0e94bb2fbba779ad399a7",
},
{
name: "repository without commit",
artifactInfo: artifact.Reference{
Name: "myrepo",
Type: ftypes.TypeRepository,
RepoMetadata: artifact.RepoMetadata{
RepoURL: "https://github.com/aquasecurity/trivy",
},
},
want: "",
},
{
name: "filesystem scan",
artifactInfo: artifact.Reference{
Name: "/some/path",
Type: ftypes.TypeFilesystem,
},
want: "",
},
{
name: "unknown type",
artifactInfo: artifact.Reference{
Name: "something",
Type: "unknown",
},
want: "",
},
}
s := Service{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := s.generateArtifactID(tt.artifactInfo)
assert.Equal(t, tt.want, got)
})
}
}

View File

@@ -54,7 +54,7 @@ func TestScanner_ScanArtifact(t *testing.T) {
want: tTypes.Report{
SchemaVersion: 2,
CreatedAt: time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC),
ArtifactID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72",
ArtifactID: "sha256:574abdaf07824449b1277ec1e7e67659cc869bbf97fd95447812b55644350a21", // hash(ImageID:index.docker.io/library/alpine) from RepoTag alpine:3.11
ArtifactName: "../fanal/test/testdata/alpine-311.tar.gz",
ArtifactType: ftypes.TypeContainerImage,
ReportID: "3ff14136-e09f-4df9-80ea-000000000001",

View File

@@ -11,14 +11,21 @@ import (
// Report represents a scan result
type Report struct {
SchemaVersion int `json:",omitempty"`
ReportID string `json:",omitempty"` // Unique identifier for this scan report
CreatedAt time.Time `json:",omitzero"`
ArtifactID string `json:",omitempty"` // Unique identifier for the artifact (e.g., image config hash)
ArtifactName string `json:",omitempty"`
ArtifactType ftypes.ArtifactType `json:",omitempty"`
Metadata Metadata `json:",omitzero"`
Results Results `json:",omitempty"`
SchemaVersion int `json:",omitempty"`
ReportID string `json:",omitempty"` // Unique identifier for this scan report
CreatedAt time.Time `json:",omitzero"`
// ArtifactID uniquely identifies the scanned artifact.
// For container images: hash(ImageID + Registry + Repository) - ensures same image in different repos have different IDs
// For repositories: hash(RepoURL + Commit) or hash(Path + Commit) for local repos
// For filesystems: empty string
// For other artifact types: empty string
ArtifactID string `json:",omitempty"`
ArtifactName string `json:",omitempty"`
ArtifactType ftypes.ArtifactType `json:",omitempty"`
Metadata Metadata `json:",omitzero"`
Results Results `json:",omitempty"`
// parsed SBOM
BOM *core.BOM `json:"-"` // Just for internal usage, not exported in JSON