
























@@ -33,43 +33,67 @@ type DependabotConfig = {
3333updates?: DependabotUpdate[];
3434};
353536-function resolveFirstFromReference(dockerfile: string): string | undefined {
36+function resolveArgDefaults(dockerfile: string): Map<string, string> {
3737const argDefaults = new Map<string, string>();
38-3938for (const line of dockerfile.split(/\r?\n/)) {
4039const trimmed = line.trim();
41-if (!trimmed) {
42-continue;
43-}
44-if (trimmed.startsWith("FROM ")) {
45-break;
46-}
4740const argMatch = trimmed.match(/^ARG\s+([A-Z0-9_]+)=(.+)$/);
4841if (!argMatch) {
4942continue;
5043}
5144const [, name, rawValue] = argMatch;
52-const value = rawValue.replace(/^["']|["']$/g, "");
53-argDefaults.set(name, value);
54-}
55-56-const fromLine = dockerfile.split(/\r?\n/).find((line) => line.trimStart().startsWith("FROM "));
57-if (!fromLine) {
58-return undefined;
45+argDefaults.set(name, rawValue.replace(/^["']|["']$/g, ""));
5946}
47+return argDefaults;
48+}
604950+function resolveFromImageRef(fromLine: string, argDefaults: Map<string, string>): string {
6151const fromMatch = fromLine.trim().match(/^FROM\s+(\S+?)(?:\s+AS\s+\S+)?$/);
6252if (!fromMatch) {
63-return undefined;
53+return fromLine;
6454}
6555const imageRef = fromMatch[1];
6656const argName =
6757imageRef.match(/^\$\{([A-Z0-9_]+)\}$/)?.[1] ?? imageRef.match(/^\$([A-Z0-9_]+)$/)?.[1];
68-6958if (!argName) {
7059return imageRef;
7160}
72-return argDefaults.get(argName);
61+return argDefaults.get(argName) ?? imageRef;
62+}
63+64+function resolveAllArgBackedFromReferences(
65+dockerfile: string,
66+): { stage: string; imageRef: string }[] {
67+const argDefaults = resolveArgDefaults(dockerfile);
68+const results: { stage: string; imageRef: string }[] = [];
69+let stageIndex = 0;
70+for (const line of dockerfile.split(/\r?\n/)) {
71+const trimmed = line.trim();
72+if (!trimmed.startsWith("FROM ")) {
73+continue;
74+}
75+const imageRef = resolveFromImageRef(trimmed, argDefaults);
76+// Only check FROM lines that use an ARG — literal `FROM scratch` etc. are intentionally unpinned.
77+const usesArg =
78+trimmed.match(/FROM\s+\$\{[A-Z0-9_]+\}/) !== null ||
79+trimmed.match(/FROM\s+\$[A-Z0-9_]+/) !== null;
80+if (usesArg) {
81+const stageMatch = trimmed.match(/AS\s+(\S+)/i);
82+const stageName = stageMatch ? stageMatch[1] : `stage-${stageIndex}`;
83+results.push({ stage: stageName, imageRef });
84+}
85+stageIndex += 1;
86+}
87+return results;
88+}
89+90+function resolveFirstFromReference(dockerfile: string): string | undefined {
91+const argDefaults = resolveArgDefaults(dockerfile);
92+const fromLine = dockerfile.split(/\r?\n/).find((line) => line.trimStart().startsWith("FROM "));
93+if (!fromLine) {
94+return undefined;
95+}
96+return resolveFromImageRef(fromLine, argDefaults);
7397}
74987599describe("docker base image pinning", () => {
@@ -84,6 +108,18 @@ describe("docker base image pinning", () => {
84108}
85109});
86110111+it("pins all ARG-backed FROM stages in selected Dockerfiles to sha256 digests", async () => {
112+for (const dockerfilePath of DIGEST_PINNED_DOCKERFILES) {
113+const dockerfile = await readFile(resolve(repoRoot, dockerfilePath), "utf8");
114+const stages = resolveAllArgBackedFromReferences(dockerfile);
115+for (const { stage, imageRef } of stages) {
116+expect(imageRef, `${dockerfilePath} stage "${stage}" must be digest-pinned`).toMatch(
117+/^\S+@sha256:[a-f0-9]{64}$/,
118+);
119+}
120+}
121+});
122+87123it("keeps Dependabot Docker updates enabled for root Dockerfiles", async () => {
88124const raw = await readFile(resolve(repoRoot, ".github/dependabot.yml"), "utf8");
89125const config = parse(raw) as DependabotConfig;
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。