Skip to content

Commit b7aac22

Browse files
committed
fix: update append latest logic
1 parent c26f858 commit b7aac22

File tree

3 files changed

+88
-86
lines changed

3 files changed

+88
-86
lines changed

lib/extractor/oci-distribution-metadata.ts

Lines changed: 8 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,14 @@ export function constructOCIDisributionMetadata({
4343
imageTag: parsed.tag,
4444
};
4545

46-
if (!ociDistributionMetadataIsValid(metadata)) {
46+
// 255 byte limit is enforced by RFC 1035.
47+
if (Buffer.byteLength(metadata.registryHost) > 255) {
48+
return;
49+
}
50+
51+
// 2048 byte limit is enforced by Snyk for platform stability.
52+
// Longer strings may be valid, but nothing close to this limit has been observed by Snyk at time of writing.
53+
if (Buffer.byteLength(metadata.repository) > 2048) {
4754
return;
4855
}
4956

@@ -52,51 +59,3 @@ export function constructOCIDisributionMetadata({
5259
return;
5360
}
5461
}
55-
56-
function ociDistributionMetadataIsValid(
57-
data: OCIDistributionMetadata,
58-
): boolean {
59-
// 255 byte limit is enforced by RFC 1035.
60-
if (Buffer.byteLength(data.registryHost) > 255) {
61-
return false;
62-
}
63-
64-
// 2048 byte limit is enforced by Snyk for platform stability.
65-
// Longer strings may be valid, but nothing close to this limit has been observed by Snyk at time of writing.
66-
if (
67-
Buffer.byteLength(data.repository) > 2048 ||
68-
!repositoryNameIsValid(data.repository)
69-
) {
70-
return false;
71-
}
72-
73-
if (!digestIsValid(data.manifestDigest)) {
74-
return false;
75-
}
76-
77-
if (data.indexDigest && !digestIsValid(data.indexDigest)) {
78-
return false;
79-
}
80-
81-
if (data.imageTag && !tagIsValid(data.imageTag)) {
82-
return false;
83-
}
84-
85-
return true;
86-
}
87-
88-
// Regular Expression Source: OCI Distribution Spec V1
89-
// https://github.com/opencontainers/distribution-spec/blob/570d0262abe8ec5e59d8e3fbbd7be4bd784b200e/spec.md?plain=1#L141
90-
const repositoryNameIsValid = (name: string) =>
91-
/^[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*$/.test(
92-
name,
93-
);
94-
95-
// Regular Expression Source: OCI Image Spec V1
96-
// https://github.com/opencontainers/image-spec/blob/d60099175f88c47cd379c4738d158884749ed235/descriptor.md?plain=1#L143
97-
const digestIsValid = (digest: string) => /^sha256:[a-f0-9]{64}$/.test(digest);
98-
99-
// Regular Expression Source: OCI Image Spec V1
100-
// https://github.com/opencontainers/distribution-spec/blob/3940529fe6c0a068290b27fb3cd797cf0528bed6/spec.md?plain=1#L160
101-
const tagIsValid = (tag: string) =>
102-
/^[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}$/.test(tag);

lib/scan.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { getArchivePath, getImageType } from "./image-type";
1212
import { isNumber, isTrue } from "./option-utils";
1313
import * as staticModule from "./static";
1414
import { ImageType, PluginOptions, PluginResponse } from "./types";
15-
import { isValidImageReference } from "./image-reference";
15+
import { isValidImageReference, ParsedImageReference, parseImageReference } from "./image-reference";
1616

1717
// Registry credentials may also be provided by env vars. When both are set, flags take precedence.
1818
export function mergeEnvVarsIntoCredentials(
@@ -214,13 +214,16 @@ async function imageIdentifierAnalysis(
214214
}
215215

216216
export function appendLatestTagIfMissing(targetImage: string): string {
217-
if (
218-
getImageType(targetImage) === ImageType.Identifier &&
219-
!targetImage.includes(":")
220-
) {
221-
return `${targetImage}:latest`;
217+
if (getImageType(targetImage) !== ImageType.Identifier) {
218+
return targetImage;
222219
}
223-
return targetImage;
220+
try {
221+
const parsed = parseImageReference(targetImage);
222+
if (parsed.tag !== undefined || parsed.digest !== undefined) {
223+
return parsed.toString();
224+
}
225+
return parsed.toString() + ':latest';
226+
} catch { return targetImage; }
224227
}
225228

226229
export async function extractContent(

test/lib/scan.spec.ts

Lines changed: 70 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -30,22 +30,22 @@ describe("mergeEnvVarsIntoCredentials", () => {
3030
${FLAG_USER} | ${ENV_VAR_USER} | ${FLAG_USER}
3131
3232
`("should set username to $expectedUsername when flag is $usernameFlag and envvar is $usernameEnvVar",
33-
({
34-
usernameFlag,
35-
usernameEnvVar,
36-
expectedUsername,
37-
}) => {
38-
if (usernameEnvVar) {
39-
process.env.SNYK_REGISTRY_USERNAME = usernameEnvVar;
40-
}
41-
const options = {
42-
username: usernameFlag,
43-
};
44-
45-
mergeEnvVarsIntoCredentials(options);
46-
47-
expect(options.username).toEqual(expectedUsername);
48-
});
33+
({
34+
usernameFlag,
35+
usernameEnvVar,
36+
expectedUsername,
37+
}) => {
38+
if (usernameEnvVar) {
39+
process.env.SNYK_REGISTRY_USERNAME = usernameEnvVar;
40+
}
41+
const options = {
42+
username: usernameFlag,
43+
};
44+
45+
mergeEnvVarsIntoCredentials(options);
46+
47+
expect(options.username).toEqual(expectedUsername);
48+
});
4949

5050
// prettier-ignore
5151
it.each`
@@ -59,10 +59,10 @@ describe("mergeEnvVarsIntoCredentials", () => {
5959
6060
`("should set password to $expectedPassword when flag is $passwordFlag and envvar is $passwordEnvVar",
6161
({
62-
passwordFlag,
63-
passwordEnvVar,
64-
expectedPassword,
65-
}) => {
62+
passwordFlag,
63+
passwordEnvVar,
64+
expectedPassword,
65+
}) => {
6666
if (passwordEnvVar) {
6767
process.env.SNYK_REGISTRY_PASSWORD = passwordEnvVar;
6868
}
@@ -84,26 +84,66 @@ describe("appendLatestTagIfMissing", () => {
8484
);
8585
});
8686

87-
it("does not append latest to docker archive path", () => {
87+
it("does not append latest to oci archive path", () => {
8888
const ociArchivePath = "oci-archive:some/path/image.tar";
8989
expect(appendLatestTagIfMissing(ociArchivePath)).toEqual(ociArchivePath);
9090
});
9191

92+
it("does not append latest to kaniko-archive path", () => {
93+
const path = "kaniko-archive:some/path/image.tar";
94+
expect(appendLatestTagIfMissing(path)).toEqual(path);
95+
});
96+
97+
it("does not append latest to unspecified archive path (.tar)", () => {
98+
const path = "/tmp/nginx.tar";
99+
expect(appendLatestTagIfMissing(path)).toEqual(path);
100+
});
101+
92102
it("does not append latest if tag exists", () => {
93103
const imageWithTag = "image:sometag";
94104
expect(appendLatestTagIfMissing(imageWithTag)).toEqual(imageWithTag);
95105
});
96106

97-
it("does not modify targetImage with sha", () => {
98-
const imageWithSha =
99-
"snyk container test nginx@sha256:56ea7092e72db3e9f84d58d583370d59b842de02ea9e1f836c3f3afc7ce408c1";
100-
expect(appendLatestTagIfMissing(imageWithSha)).toEqual(imageWithSha);
107+
it("does not append latest if digest exists (digest-only reference)", () => {
108+
const imageWithDigest =
109+
"nginx@sha256:56ea7092e72db3e9f84d58d583370d59b842de02ea9e1f836c3f3afc7ce408c1";
110+
expect(appendLatestTagIfMissing(imageWithDigest)).toEqual(
111+
imageWithDigest,
112+
);
101113
});
102114

103-
it("appends latest if no tag exists", () => {
104-
const imageWithoutTag = "image";
105-
expect(appendLatestTagIfMissing(imageWithoutTag)).toEqual(
106-
`${imageWithoutTag}:latest`,
115+
it("does not append latest if both tag and digest exist", () => {
116+
const imageWithTagAndDigest =
117+
"nginx:1.23@sha256:56ea7092e72db3e9f84d58d583370d59b842de02ea9e1f836c3f3afc7ce408c1";
118+
expect(appendLatestTagIfMissing(imageWithTagAndDigest)).toEqual(
119+
imageWithTagAndDigest,
107120
);
108121
});
109-
});
122+
123+
it("appends latest for repository-only reference", () => {
124+
expect(appendLatestTagIfMissing("image")).toEqual("image:latest");
125+
});
126+
127+
it("appends latest for repository with namespace", () => {
128+
expect(appendLatestTagIfMissing("library/nginx")).toEqual(
129+
"library/nginx:latest",
130+
);
131+
});
132+
133+
it("appends latest for registry with port and no tag", () => {
134+
expect(appendLatestTagIfMissing("localhost:5000/foo/bar")).toEqual(
135+
"localhost:5000/foo/bar:latest",
136+
);
137+
});
138+
139+
it("appends latest for custom registry without tag", () => {
140+
expect(appendLatestTagIfMissing("gcr.io/project/nginx")).toEqual(
141+
"gcr.io/project/nginx:latest",
142+
);
143+
});
144+
145+
it("returns unchanged for invalid image reference", () => {
146+
const invalid = "/test:unknown";
147+
expect(appendLatestTagIfMissing(invalid)).toEqual(invalid);
148+
});
149+
});

0 commit comments

Comments
 (0)