Skip to content

Commit 9a328ec

Browse files
committed
test: add unit and system tests for OCI base image labels fallback
1 parent b814413 commit 9a328ec

File tree

5 files changed

+194
-0
lines changed

5 files changed

+194
-0
lines changed

lib/static.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,22 @@ export async function analyzeStatically(
5454
packageFormat: parsedAnalysisResult.packageFormat,
5555
};
5656

57+
// If no Dockerfile was provided (or it couldn't detect the base image),
58+
// try to detect the base image from OCI standard labels.
59+
// Many modern images (Chainguard, Bitnami, official images) include
60+
// org.opencontainers.image.base.name in their labels.
61+
if (
62+
(!dockerfileAnalysis || !dockerfileAnalysis.baseImage) &&
63+
staticAnalysis.imageLabels
64+
) {
65+
const baseImageLabel =
66+
staticAnalysis.imageLabels["org.opencontainers.image.base.name"] ||
67+
staticAnalysis.imageLabels["org.opencontainers.image.base.digest"];
68+
if (baseImageLabel && dockerfileAnalysis) {
69+
dockerfileAnalysis.baseImage = baseImageLabel;
70+
}
71+
}
72+
5773
const excludeBaseImageVulns = isTrue(options["exclude-base-image-vulns"]);
5874

5975
const names = getImageNames(options, imageName);
9 KB
Binary file not shown.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# An empty Dockerfile

test/lib/static.spec.ts

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import * as analyzer from "../../lib/analyzer";
2+
import * as depTree from "../../lib/dependency-tree";
3+
import { DockerFileAnalysis } from "../../lib/dockerfile/types";
4+
import * as parser from "../../lib/parser";
5+
import * as responseBuilder from "../../lib/response-builder";
6+
import { analyzeStatically } from "../../lib/static";
7+
8+
jest.mock("../../lib/analyzer");
9+
jest.mock("../../lib/parser");
10+
jest.mock("../../lib/dependency-tree");
11+
jest.mock("../../lib/response-builder");
12+
13+
describe("analyzeStatically", () => {
14+
beforeEach(() => {
15+
jest.clearAllMocks();
16+
17+
(parser.parseAnalysisResults as jest.Mock).mockReturnValue({
18+
imageId: "test-id",
19+
imageLayers: [],
20+
packageFormat: "test-format",
21+
depInfosList: [],
22+
targetOS: { name: "test", version: "1" },
23+
});
24+
25+
(depTree.buildTree as jest.Mock).mockReturnValue({
26+
dependencies: {},
27+
});
28+
29+
(responseBuilder.buildResponse as jest.Mock).mockReturnValue({
30+
scanResults: [],
31+
});
32+
});
33+
34+
it("updates baseImage from org.opencontainers.image.base.name label when baseImage is missing", async () => {
35+
const mockDockerFileAnalysis: DockerFileAnalysis = {
36+
dockerfilePackages: {},
37+
dockerfileLayers: {},
38+
baseImage: undefined,
39+
};
40+
41+
(analyzer.analyzeStatically as jest.Mock).mockResolvedValue({
42+
osRelease: { name: "test", version: "1" },
43+
imageLabels: {
44+
"org.opencontainers.image.base.name": "alpine:latest",
45+
},
46+
});
47+
48+
await analyzeStatically(
49+
"test-image",
50+
mockDockerFileAnalysis,
51+
"docker-archive",
52+
"test-path",
53+
{ include: [], exclude: [] },
54+
{},
55+
);
56+
57+
expect(mockDockerFileAnalysis.baseImage).toEqual("alpine:latest");
58+
});
59+
60+
it("updates baseImage from org.opencontainers.image.base.digest label when name is missing", async () => {
61+
const mockDockerFileAnalysis: DockerFileAnalysis = {
62+
dockerfilePackages: {},
63+
dockerfileLayers: {},
64+
baseImage: undefined,
65+
};
66+
67+
(analyzer.analyzeStatically as jest.Mock).mockResolvedValue({
68+
osRelease: { name: "test", version: "1" },
69+
imageLabels: {
70+
"org.opencontainers.image.base.digest": "sha256:1234567890abcdef",
71+
},
72+
});
73+
74+
await analyzeStatically(
75+
"test-image",
76+
mockDockerFileAnalysis,
77+
"docker-archive",
78+
"test-path",
79+
{ include: [], exclude: [] },
80+
{},
81+
);
82+
83+
expect(mockDockerFileAnalysis.baseImage).toEqual("sha256:1234567890abcdef");
84+
});
85+
86+
it("does not update baseImage if it is already present in dockerfileAnalysis", async () => {
87+
const mockDockerFileAnalysis: DockerFileAnalysis = {
88+
dockerfilePackages: {},
89+
dockerfileLayers: {},
90+
baseImage: "ubuntu:latest",
91+
};
92+
93+
(analyzer.analyzeStatically as jest.Mock).mockResolvedValue({
94+
osRelease: { name: "test", version: "1" },
95+
imageLabels: {
96+
"org.opencontainers.image.base.name": "alpine:latest",
97+
},
98+
});
99+
100+
await analyzeStatically(
101+
"test-image",
102+
mockDockerFileAnalysis,
103+
"docker-archive",
104+
"test-path",
105+
{ include: [], exclude: [] },
106+
{},
107+
);
108+
109+
expect(mockDockerFileAnalysis.baseImage).toEqual("ubuntu:latest");
110+
});
111+
112+
it("handles cases where imageLabels are undefined", async () => {
113+
const mockDockerFileAnalysis: DockerFileAnalysis = {
114+
dockerfilePackages: {},
115+
dockerfileLayers: {},
116+
baseImage: undefined,
117+
};
118+
119+
(analyzer.analyzeStatically as jest.Mock).mockResolvedValue({
120+
osRelease: { name: "test", version: "1" },
121+
imageLabels: undefined,
122+
});
123+
124+
await analyzeStatically(
125+
"test-image",
126+
mockDockerFileAnalysis,
127+
"docker-archive",
128+
"test-path",
129+
{ include: [], exclude: [] },
130+
{},
131+
);
132+
133+
expect(mockDockerFileAnalysis.baseImage).toBeUndefined();
134+
});
135+
136+
it("handles cases where dockerfileAnalysis is undefined", async () => {
137+
(analyzer.analyzeStatically as jest.Mock).mockResolvedValue({
138+
osRelease: { name: "test", version: "1" },
139+
imageLabels: {
140+
"org.opencontainers.image.base.name": "alpine:latest",
141+
},
142+
});
143+
144+
// Should not crash
145+
await expect(
146+
analyzeStatically(
147+
"test-image",
148+
undefined,
149+
"docker-archive",
150+
"test-path",
151+
{ include: [], exclude: [] },
152+
{},
153+
),
154+
).resolves.toBeDefined();
155+
});
156+
});

test/system/static.spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,4 +273,25 @@ describe("static", () => {
273273
// expected number of direct deps
274274
expect(depGraph.getDepPkgs()).toHaveLength(125);
275275
});
276+
277+
test("static scanning an image sets baseImage from OCI standard labels", async () => {
278+
const dockerfilePath = path.join(
279+
__dirname,
280+
"../fixtures/dockerfiles/empty-dockerfile",
281+
);
282+
const fixturePath = getFixture("docker-save/oci-labels.tar");
283+
const imagePath = `docker-archive:${fixturePath}`;
284+
285+
const pluginResultStatic = await plugin.scan({
286+
path: imagePath,
287+
file: dockerfilePath,
288+
});
289+
290+
const dockerfileAnalysis: DockerFileAnalysis =
291+
pluginResultStatic.scanResults[0].facts.find(
292+
(fact) => fact.type === "dockerfileAnalysis",
293+
)!.data;
294+
295+
expect(dockerfileAnalysis.baseImage).toEqual("alpine:latest");
296+
});
276297
});

0 commit comments

Comments
 (0)