Skip to content
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
2 changes: 2 additions & 0 deletions integration_tests/instance_groups.lua
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Test.gql("Application with instance groups", function(t)
image {
name
tag
digest
}
readyInstances
desiredInstances
Expand Down Expand Up @@ -45,6 +46,7 @@ Test.gql("Application with instance groups", function(t)
image = {
name = "ghcr.io/navikt/myapp",
tag = "v1.2.3",
digest = Null,
},
readyInstances = 2,
desiredInstances = 2,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
apiVersion: nais.io/v1alpha1
kind: Application
metadata:
name: app-with-vulnerabilities
spec:
image: europe-north1-docker.pkg.dev/nais/navikt/app-name:latest@sha256:deadbeef
---
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
apiVersion: nais.io/v1alpha1
kind: Application
metadata:
name: app-with-digest-only
spec:
image: europe-north1-docker.pkg.dev/nais/navikt/app-name@sha256:cafebabe
4 changes: 3 additions & 1 deletion integration_tests/vulnerabilities.lua
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Test.gql("List vulnerability summaries for team", function(t)
image{
name
tag
digest
hasSBOM
sbom {
status
Expand Down Expand Up @@ -84,7 +85,8 @@ Test.gql("List vulnerability summaries for team", function(t)
{
image = {
name = "europe-north1-docker.pkg.dev/nais/navikt/app-name",
tag = "latest@sha256:deadbeef",
tag = "latest",
digest = "sha256:deadbeef",
hasSBOM = true,
sbom = {
status = "READY",
Expand Down
39 changes: 39 additions & 0 deletions integration_tests/vulnerability_digest_only.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
Helper.readK8sResources("k8s_resources/vulnerability_digest_only")

local team = Team.new("slug-1", "purpose", "#channel")
local user = User.new("authenticated", "authenticated@example.com", "some-id")

Test.gql("Digest-only image exposes empty tag and populated digest", function(t)
t.addHeader("x-user-email", user:email())
t.query(string.format([[
{
team(slug: "%s") {
environment(name: "%s") {
workload(name: "%s") {
image {
name
tag
digest
}
}
}
}
}
]], team:slug(), "dev", "app-with-digest-only"))

t.check {
data = {
team = {
environment = {
workload = {
image = {
name = "europe-north1-docker.pkg.dev/nais/navikt/app-name",
tag = "",
digest = "sha256:cafebabe",
},
},
},
},
},
}
end)
18 changes: 17 additions & 1 deletion internal/graph/gengql/root_.generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions internal/graph/gengql/workloads.generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion internal/graph/schema/workloads.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,16 @@ type ContainerImage implements Node & ActivityLogger {
name: String!

"""
Tag of the container image.
Tag of the container image. Empty when the image reference has no explicit tag,
for example when it is referenced by digest only.
"""
tag: String!

"""
Digest of the container image, if present.
"""
digest: String

"""
Activity log associated with the container image.
"""
Expand Down
4 changes: 1 addition & 3 deletions internal/vulnerability/fake/fakedata.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,7 @@ func createFakeData(ctx context.Context) *fakeData {
}

func createWorkloadSummary(env, team, workloadType, name, image string, vulnFactor int32) *vulnerabilities.WorkloadSummary {
parsedImage := workload.ParseContainerImage(image)
imageName := parsedImage.Name
imageTag := parsedImage.Tag
imageName, imageTag := workload.FormatImageReferenceForLookup(image)
summary := &vulnerabilities.Summary{
Critical: vulnFactor,
High: vulnFactor * 2,
Expand Down
31 changes: 7 additions & 24 deletions internal/vulnerability/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -886,36 +886,19 @@ func normalizeWorkloadTypeForVulnerabilityIdent(workloadType string) string {
// splitImage splits an image reference into name, tag, and sha components.
// Tag defaults to "latest" if not specified. SHA is optional and will be empty if not present.
func splitImage(image string) (name, tag, sha string, hasExplicitTag bool) {
before, after, ok := strings.Cut(image, "@")
if ok {
image = before
sha = after
}
// Only look for a tag colon in the final path segment to avoid splitting
// on a registry port (e.g. "myregistry.io:5000/myimage").
lastSlash := strings.LastIndex(image, "/")
segment := image[lastSlash+1:]
if before, after, ok := strings.Cut(segment, ":"); ok {
name = image[:lastSlash+1] + before
tag = after
hasExplicitTag = true
} else {
name = image
parsed := workload.ParseImageReference(image)
name = parsed.Name
tag = parsed.Tag
sha = parsed.Digest
hasExplicitTag = parsed.HasExplicitTag
if tag == "" {
tag = "latest"
}
return name, tag, sha, hasExplicitTag
}

func splitImageRefForLookup(image string) (name, tag string) {
name, tag, sha, hasExplicitTag := splitImage(image)
if sha != "" {
if hasExplicitTag && tag != "" {
return name, tag + "@" + sha
}
return name, sha
}

return name, tag
return workload.FormatImageReferenceForLookup(image)
}

func GetSbomStatus(ctx context.Context, ref string) (SBOMStatus, error) {
Expand Down
44 changes: 44 additions & 0 deletions internal/vulnerability/queries_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ func TestSplitImage(t *testing.T) {
wantTag: "v2.0.0",
wantSha: "sha256:deadbeef",
},
{
name: "image with nested path sha and no tag",
image: "europe-north1-docker.pkg.dev/my-project/my-repo/my-app@sha256:deadbeef",
wantName: "europe-north1-docker.pkg.dev/my-project/my-repo/my-app",
wantTag: "latest",
wantSha: "sha256:deadbeef",
},
{
name: "simple image name only",
image: "nginx",
Expand Down Expand Up @@ -122,6 +129,13 @@ func TestSplitImage(t *testing.T) {
wantTag: "latest",
wantSha: "sha256:abc123",
},
{
name: "registry with port nested path tag and sha",
image: "myregistry.io:5000/org/repo/myimage:v2.0.0@sha256:abc123",
wantName: "myregistry.io:5000/org/repo/myimage",
wantTag: "v2.0.0",
wantSha: "sha256:abc123",
},
}

for _, tt := range tests {
Expand Down Expand Up @@ -183,6 +197,24 @@ func TestSplitImageRefForLookup(t *testing.T) {
wantName: "myregistry.io/myimage",
wantTag: "v1.2.3@sha256:abc123",
},
{
name: "image with nested path and no tag defaults to latest",
image: "europe-north1-docker.pkg.dev/my-project/my-repo/my-app",
wantName: "europe-north1-docker.pkg.dev/my-project/my-repo/my-app",
wantTag: "latest",
},
{
name: "image with nested path sha and no tag",
image: "europe-north1-docker.pkg.dev/my-project/my-repo/my-app@sha256:deadbeef",
wantName: "europe-north1-docker.pkg.dev/my-project/my-repo/my-app",
wantTag: "sha256:deadbeef",
},
{
name: "image with nested path tag and sha",
image: "europe-north1-docker.pkg.dev/my-project/my-repo/my-app:v2.0.0@sha256:deadbeef",
wantName: "europe-north1-docker.pkg.dev/my-project/my-repo/my-app",
wantTag: "v2.0.0@sha256:deadbeef",
},
{
name: "registry with port and tag",
image: "myregistry.io:5000/myimage:v1",
Expand All @@ -201,6 +233,18 @@ func TestSplitImageRefForLookup(t *testing.T) {
wantName: "myregistry.io:5000/myimage",
wantTag: "v1@sha256:abc123",
},
{
name: "registry with port nested path and tag",
image: "myregistry.io:5000/org/repo/myimage:v2.0.0",
wantName: "myregistry.io:5000/org/repo/myimage",
wantTag: "v2.0.0",
},
{
name: "registry with port nested path tag and sha",
image: "myregistry.io:5000/org/repo/myimage:v2.0.0@sha256:abc123",
wantName: "myregistry.io:5000/org/repo/myimage",
wantTag: "v2.0.0@sha256:abc123",
},
}

for _, tt := range tests {
Expand Down
Loading
Loading