@docker-doctor/cli 0.4.3 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +2 -2
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +2 -2
- package/dist/cli.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +11 -4
- package/dist/index.d.mts +11 -4
- package/dist/index.mjs +1 -1
- package/dist/{src-TK9DRoRo.mjs → src-CvOv-hL3.mjs} +85 -69
- package/dist/src-CvOv-hL3.mjs.map +1 -0
- package/dist/{src-ILbJuJmE.cjs → src-DwuAaQcq.cjs} +89 -67
- package/dist/src-DwuAaQcq.cjs.map +1 -0
- package/package.json +1 -1
- package/dist/src-ILbJuJmE.cjs.map +0 -1
- package/dist/src-TK9DRoRo.mjs.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
3
|
-
const require_src = require('./src-
|
|
3
|
+
const require_src = require('./src-DwuAaQcq.cjs');
|
|
4
4
|
|
|
5
5
|
//#region ../core/src/config/define-config.ts
|
|
6
6
|
const defineConfig = (config) => config;
|
package/dist/index.d.cts
CHANGED
|
@@ -6,10 +6,11 @@ interface DockerfileInstruction {
|
|
|
6
6
|
line: number;
|
|
7
7
|
raw: string;
|
|
8
8
|
}
|
|
9
|
+
type DiagnosticSeverity = "error" | "warning" | "info";
|
|
9
10
|
interface Diagnostic {
|
|
10
11
|
file: string;
|
|
11
12
|
rule: string;
|
|
12
|
-
severity:
|
|
13
|
+
severity: DiagnosticSeverity;
|
|
13
14
|
message: string;
|
|
14
15
|
help: string;
|
|
15
16
|
line?: number;
|
|
@@ -21,14 +22,20 @@ interface ProjectInfo {
|
|
|
21
22
|
dockerignores?: string[];
|
|
22
23
|
}
|
|
23
24
|
type RuleCategory = "Security" | "Performance" | "Best Practices" | "Compose" | "Image Size";
|
|
24
|
-
type RuleSeverity =
|
|
25
|
+
type RuleSeverity = DiagnosticSeverity | "off";
|
|
25
26
|
interface RuleDefinition {
|
|
26
27
|
key: string;
|
|
27
28
|
category: RuleCategory;
|
|
28
|
-
defaultSeverity:
|
|
29
|
+
defaultSeverity: DiagnosticSeverity;
|
|
29
30
|
message: string;
|
|
30
31
|
help: string;
|
|
31
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Maps a path of YAML keys/indices (e.g. `["services", "web"]`) to the
|
|
35
|
+
* 1-based line where that key is defined, or `undefined` when the path does
|
|
36
|
+
* not resolve to a concrete node in the source document.
|
|
37
|
+
*/
|
|
38
|
+
type ComposeLocator = (path: readonly (string | number)[]) => number | undefined;
|
|
32
39
|
interface DockerDoctorConfig {
|
|
33
40
|
rules?: Record<string, RuleSeverity>;
|
|
34
41
|
categories?: Partial<Record<RuleCategory, RuleSeverity>>;
|
|
@@ -50,7 +57,7 @@ declare const parseCompose: (content: string, filepath: string) => unknown;
|
|
|
50
57
|
declare const runDockerfileRules: (instructions: DockerfileInstruction[], file: string, projectFiles: string[], rulesConfig?: Record<string, RuleSeverity>, categoriesConfig?: Record<string, RuleSeverity>) => Diagnostic[];
|
|
51
58
|
//#endregion
|
|
52
59
|
//#region ../core/src/runners/compose-runner.d.ts
|
|
53
|
-
declare const runComposeRules: (composeContent: unknown, file: string, rulesConfig?: Record<string, RuleSeverity>, categoriesConfig?: Record<string, RuleSeverity
|
|
60
|
+
declare const runComposeRules: (composeContent: unknown, file: string, rulesConfig?: Record<string, RuleSeverity>, categoriesConfig?: Record<string, RuleSeverity>, locate?: ComposeLocator) => Diagnostic[];
|
|
54
61
|
//#endregion
|
|
55
62
|
//#region ../core/src/rules/index.d.ts
|
|
56
63
|
declare const allRules: RuleDefinition[];
|
package/dist/index.d.mts
CHANGED
|
@@ -6,10 +6,11 @@ interface DockerfileInstruction {
|
|
|
6
6
|
line: number;
|
|
7
7
|
raw: string;
|
|
8
8
|
}
|
|
9
|
+
type DiagnosticSeverity = "error" | "warning" | "info";
|
|
9
10
|
interface Diagnostic {
|
|
10
11
|
file: string;
|
|
11
12
|
rule: string;
|
|
12
|
-
severity:
|
|
13
|
+
severity: DiagnosticSeverity;
|
|
13
14
|
message: string;
|
|
14
15
|
help: string;
|
|
15
16
|
line?: number;
|
|
@@ -21,14 +22,20 @@ interface ProjectInfo {
|
|
|
21
22
|
dockerignores?: string[];
|
|
22
23
|
}
|
|
23
24
|
type RuleCategory = "Security" | "Performance" | "Best Practices" | "Compose" | "Image Size";
|
|
24
|
-
type RuleSeverity =
|
|
25
|
+
type RuleSeverity = DiagnosticSeverity | "off";
|
|
25
26
|
interface RuleDefinition {
|
|
26
27
|
key: string;
|
|
27
28
|
category: RuleCategory;
|
|
28
|
-
defaultSeverity:
|
|
29
|
+
defaultSeverity: DiagnosticSeverity;
|
|
29
30
|
message: string;
|
|
30
31
|
help: string;
|
|
31
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Maps a path of YAML keys/indices (e.g. `["services", "web"]`) to the
|
|
35
|
+
* 1-based line where that key is defined, or `undefined` when the path does
|
|
36
|
+
* not resolve to a concrete node in the source document.
|
|
37
|
+
*/
|
|
38
|
+
type ComposeLocator = (path: readonly (string | number)[]) => number | undefined;
|
|
32
39
|
interface DockerDoctorConfig {
|
|
33
40
|
rules?: Record<string, RuleSeverity>;
|
|
34
41
|
categories?: Partial<Record<RuleCategory, RuleSeverity>>;
|
|
@@ -50,7 +57,7 @@ declare const parseCompose: (content: string, filepath: string) => unknown;
|
|
|
50
57
|
declare const runDockerfileRules: (instructions: DockerfileInstruction[], file: string, projectFiles: string[], rulesConfig?: Record<string, RuleSeverity>, categoriesConfig?: Record<string, RuleSeverity>) => Diagnostic[];
|
|
51
58
|
//#endregion
|
|
52
59
|
//#region ../core/src/runners/compose-runner.d.ts
|
|
53
|
-
declare const runComposeRules: (composeContent: unknown, file: string, rulesConfig?: Record<string, RuleSeverity>, categoriesConfig?: Record<string, RuleSeverity
|
|
60
|
+
declare const runComposeRules: (composeContent: unknown, file: string, rulesConfig?: Record<string, RuleSeverity>, categoriesConfig?: Record<string, RuleSeverity>, locate?: ComposeLocator) => Diagnostic[];
|
|
54
61
|
//#endregion
|
|
55
62
|
//#region ../core/src/rules/index.d.ts
|
|
56
63
|
declare const allRules: RuleDefinition[];
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as runDockerfileRules,
|
|
2
|
+
import { a as runDockerfileRules, d as discoverProject, f as version$1, i as runComposeRules, l as parseCompose, n as calculateScore, o as allRules, r as loadConfig, s as findRule, t as toJsonReport, u as parseDockerfile } from "./src-CvOv-hL3.mjs";
|
|
3
3
|
|
|
4
4
|
//#region ../core/src/config/define-config.ts
|
|
5
5
|
const defineConfig = (config) => config;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { parse } from "yaml";
|
|
4
|
+
import { LineCounter, isAlias, isMap, isScalar, isSeq, parse, parseDocument } from "yaml";
|
|
5
5
|
|
|
6
6
|
//#region package.json
|
|
7
|
-
var version = "0.4.
|
|
7
|
+
var version = "0.4.4";
|
|
8
8
|
|
|
9
9
|
//#endregion
|
|
10
10
|
//#region ../core/src/project-info/discover.ts
|
|
@@ -107,7 +107,6 @@ const processHeredocLine = (state, trimmed) => {
|
|
|
107
107
|
};
|
|
108
108
|
const processInstructionLine = (state, trimmed, lineNum) => {
|
|
109
109
|
let lineContent = trimmed;
|
|
110
|
-
if (lineContent.startsWith("#")) return;
|
|
111
110
|
const hasContinuation = lineContent.endsWith("\\");
|
|
112
111
|
if (hasContinuation) lineContent = lineContent.slice(0, -1).trim();
|
|
113
112
|
if (state.currentInstruction) state.currentArgs += (state.currentArgs ? " " : "") + lineContent;
|
|
@@ -130,7 +129,8 @@ const parseDockerfile = (content) => {
|
|
|
130
129
|
const trimmed = rawLine.trim();
|
|
131
130
|
const lineNum = i + 1;
|
|
132
131
|
const insideHeredoc = state.heredocQueue.length > 0;
|
|
133
|
-
if (!
|
|
132
|
+
if (!insideHeredoc && trimmed.startsWith("#")) continue;
|
|
133
|
+
if (!state.currentInstruction && !insideHeredoc && trimmed === "") continue;
|
|
134
134
|
state.rawAccumulator.push(rawLine);
|
|
135
135
|
if (insideHeredoc) processHeredocLine(state, trimmed);
|
|
136
136
|
else processInstructionLine(state, trimmed, lineNum);
|
|
@@ -173,6 +173,40 @@ const parseCompose = (content, filepath) => {
|
|
|
173
173
|
});
|
|
174
174
|
}
|
|
175
175
|
};
|
|
176
|
+
/**
|
|
177
|
+
* Builds a {@link ComposeLocator} over the same source text a compose object
|
|
178
|
+
* was parsed from, so rules can attach line numbers to their diagnostics.
|
|
179
|
+
*
|
|
180
|
+
* Keys pulled in via YAML merge keys (`<<: *anchor`) have no concrete node
|
|
181
|
+
* at the merge site, so paths through them resolve to `undefined` — callers
|
|
182
|
+
* fall back to an unnumbered diagnostic, which matches the old behavior.
|
|
183
|
+
*/
|
|
184
|
+
const createComposeLocator = (content) => {
|
|
185
|
+
const lineCounter = new LineCounter();
|
|
186
|
+
const doc = parseDocument(content, {
|
|
187
|
+
lineCounter,
|
|
188
|
+
merge: true
|
|
189
|
+
});
|
|
190
|
+
return (path) => {
|
|
191
|
+
let node = doc.contents;
|
|
192
|
+
let offset;
|
|
193
|
+
for (const segment of path) {
|
|
194
|
+
if (isAlias(node)) node = node.resolve(doc);
|
|
195
|
+
if (isMap(node)) {
|
|
196
|
+
const pair = node.items.find((item) => isScalar(item.key) && String(item.key.value) === String(segment));
|
|
197
|
+
if (!pair || !isScalar(pair.key)) return;
|
|
198
|
+
offset = pair.key.range?.[0];
|
|
199
|
+
node = pair.value;
|
|
200
|
+
} else if (isSeq(node) && typeof segment === "number") {
|
|
201
|
+
const item = node.items[segment];
|
|
202
|
+
if (item === void 0 || item === null) return;
|
|
203
|
+
offset = item.range?.[0];
|
|
204
|
+
node = item;
|
|
205
|
+
} else return;
|
|
206
|
+
}
|
|
207
|
+
return offset === void 0 ? void 0 : lineCounter.linePos(offset).line;
|
|
208
|
+
};
|
|
209
|
+
};
|
|
176
210
|
|
|
177
211
|
//#endregion
|
|
178
212
|
//#region ../core/src/parsers/exec-form.ts
|
|
@@ -248,8 +282,8 @@ const collectStageAliases = (instructions) => {
|
|
|
248
282
|
};
|
|
249
283
|
|
|
250
284
|
//#endregion
|
|
251
|
-
//#region ../core/src/rules/
|
|
252
|
-
const createDiagnostic
|
|
285
|
+
//#region ../core/src/rules/create-diagnostic.ts
|
|
286
|
+
const createDiagnostic = (file, ruleKey, severity, message, help, line) => ({
|
|
253
287
|
file,
|
|
254
288
|
help,
|
|
255
289
|
line,
|
|
@@ -257,12 +291,15 @@ const createDiagnostic$4 = (file, ruleKey, severity, message, help, line) => ({
|
|
|
257
291
|
rule: ruleKey,
|
|
258
292
|
severity
|
|
259
293
|
});
|
|
294
|
+
|
|
295
|
+
//#endregion
|
|
296
|
+
//#region ../core/src/rules/best-practices.ts
|
|
260
297
|
const requireHealthcheck = {
|
|
261
298
|
category: "Best Practices",
|
|
262
299
|
check(instructions, file) {
|
|
263
300
|
const hasHealthcheck = instructions.some((inst) => inst.instruction === "HEALTHCHECK");
|
|
264
301
|
const hasExposedPortsOrEntry = instructions.some((inst) => inst.instruction === "EXPOSE" || inst.instruction === "CMD" || inst.instruction === "ENTRYPOINT");
|
|
265
|
-
if (!hasHealthcheck && hasExposedPortsOrEntry) return [createDiagnostic
|
|
302
|
+
if (!hasHealthcheck && hasExposedPortsOrEntry) return [createDiagnostic(file, this.key, this.defaultSeverity, "No HEALTHCHECK instruction found. Containers running services should expose healthchecks to enable auto-healing.", this.help, 1)];
|
|
266
303
|
return [];
|
|
267
304
|
},
|
|
268
305
|
defaultSeverity: "info",
|
|
@@ -279,7 +316,7 @@ const preferCopyOverAdd = {
|
|
|
279
316
|
if (!src) continue;
|
|
280
317
|
const isRemote = src.startsWith("http://") || src.startsWith("https://");
|
|
281
318
|
const isArchive = src.endsWith(".tar") || src.endsWith(".tar.gz") || src.endsWith(".tgz") || src.endsWith(".zip");
|
|
282
|
-
if (!isRemote && !isArchive) diagnostics.push(createDiagnostic
|
|
319
|
+
if (!isRemote && !isArchive) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `ADD instruction used for regular files: '${inst.args}'. COPY is simpler and less prone to magic side effects.`, this.help, inst.line));
|
|
283
320
|
}
|
|
284
321
|
return diagnostics;
|
|
285
322
|
},
|
|
@@ -292,7 +329,7 @@ const useExecForm = {
|
|
|
292
329
|
category: "Best Practices",
|
|
293
330
|
check(instructions, file) {
|
|
294
331
|
const diagnostics = [];
|
|
295
|
-
for (const inst of instructions) if ((inst.instruction === "CMD" || inst.instruction === "ENTRYPOINT") && parseExecForm(inst.args) === null) diagnostics.push(createDiagnostic
|
|
332
|
+
for (const inst of instructions) if ((inst.instruction === "CMD" || inst.instruction === "ENTRYPOINT") && parseExecForm(inst.args) === null) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `${inst.instruction} instruction uses shell form instead of exec form. In shell form, the command runs under '/bin/sh -c', which does not pass signals to child processes.`, this.help, inst.line));
|
|
296
333
|
return diagnostics;
|
|
297
334
|
},
|
|
298
335
|
defaultSeverity: "warning",
|
|
@@ -303,7 +340,7 @@ const useExecForm = {
|
|
|
303
340
|
const requireLabels = {
|
|
304
341
|
category: "Best Practices",
|
|
305
342
|
check(instructions, file) {
|
|
306
|
-
if (!instructions.some((inst) => inst.instruction === "LABEL")) return [createDiagnostic
|
|
343
|
+
if (!instructions.some((inst) => inst.instruction === "LABEL")) return [createDiagnostic(file, this.key, this.defaultSeverity, "No LABEL metadata was found in this Dockerfile. Adding labels helps identify build information, maintainers, and descriptions.", this.help, 1)];
|
|
307
344
|
return [];
|
|
308
345
|
},
|
|
309
346
|
defaultSeverity: "info",
|
|
@@ -318,8 +355,8 @@ const combineAptUpdateInstall = {
|
|
|
318
355
|
for (const inst of instructions) if (inst.instruction === "RUN") {
|
|
319
356
|
const hasUpdate = inst.args.includes("apt-get update");
|
|
320
357
|
const hasInstall = inst.args.includes("apt-get install");
|
|
321
|
-
if (hasUpdate && !hasInstall) diagnostics.push(createDiagnostic
|
|
322
|
-
else if (hasInstall && !hasUpdate) diagnostics.push(createDiagnostic
|
|
358
|
+
if (hasUpdate && !hasInstall) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, "RUN apt-get update used without apt-get install in the same instruction. This can cause caching issues and build failures.", this.help, inst.line));
|
|
359
|
+
else if (hasInstall && !hasUpdate) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, "RUN apt-get install used without apt-get update in the same instruction. Always combine them to ensure up-to-date package installation.", this.help, inst.line));
|
|
323
360
|
}
|
|
324
361
|
return diagnostics;
|
|
325
362
|
},
|
|
@@ -359,7 +396,7 @@ const usePipefail = {
|
|
|
359
396
|
const { args } = inst;
|
|
360
397
|
if (!HAS_PIPE_RE.test(args)) continue;
|
|
361
398
|
const execArgv = parseExecForm(args);
|
|
362
|
-
if (!(execArgv === null ? shellHasPipefail || PIPEFAIL_SETTING_RE.test(args) : PIPEFAIL_SETTING_RE.test(execArgv.join(" ")))) diagnostics.push(createDiagnostic
|
|
399
|
+
if (!(execArgv === null ? shellHasPipefail || PIPEFAIL_SETTING_RE.test(args) : PIPEFAIL_SETTING_RE.test(execArgv.join(" ")))) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, "RUN instruction uses a pipe (|) but does not configure 'pipefail'. If a command in the pipe fails, the step may still succeed silently.", this.help, inst.line));
|
|
363
400
|
}
|
|
364
401
|
return diagnostics;
|
|
365
402
|
},
|
|
@@ -374,7 +411,7 @@ const absoluteWorkdir = {
|
|
|
374
411
|
const diagnostics = [];
|
|
375
412
|
for (const inst of instructions) if (inst.instruction === "WORKDIR") {
|
|
376
413
|
const path = inst.args.trim();
|
|
377
|
-
if (!/^(?:\/|\\|\$|[a-zA-Z]:)/u.test(path)) diagnostics.push(createDiagnostic
|
|
414
|
+
if (!/^(?:\/|\\|\$|[a-zA-Z]:)/u.test(path)) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `WORKDIR specifies a relative path '${path}'. For clarity and reliability, always use absolute paths.`, this.help, inst.line));
|
|
378
415
|
}
|
|
379
416
|
return diagnostics;
|
|
380
417
|
},
|
|
@@ -387,7 +424,7 @@ const avoidRunCd = {
|
|
|
387
424
|
category: "Best Practices",
|
|
388
425
|
check(instructions, file) {
|
|
389
426
|
const diagnostics = [];
|
|
390
|
-
for (const inst of instructions) if (inst.instruction === "RUN" && /\bcd\b/u.test(inst.args)) diagnostics.push(createDiagnostic
|
|
427
|
+
for (const inst of instructions) if (inst.instruction === "RUN" && /\bcd\b/u.test(inst.args)) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, "Avoid using 'cd' in RUN instructions. Use WORKDIR instead to change the working directory stably across layers.", this.help, inst.line));
|
|
391
428
|
return diagnostics;
|
|
392
429
|
},
|
|
393
430
|
defaultSeverity: "info",
|
|
@@ -407,7 +444,7 @@ const sortMultilineArgs = {
|
|
|
407
444
|
const packages = raw.split(/\r?\n/u).slice(1).map((line) => line.trim()).filter((line) => line !== "" && !line.startsWith("&&") && !line.startsWith("-") && !line.includes("rm -rf")).map((line) => line.endsWith("\\") ? line.slice(0, -1).trim() : line).filter(Boolean);
|
|
408
445
|
if (packages.length > 1) {
|
|
409
446
|
const sorted = packages.toSorted((a, b) => a.localeCompare(b));
|
|
410
|
-
if (!packages.every((val, idx) => val === sorted[idx])) diagnostics.push(createDiagnostic
|
|
447
|
+
if (!packages.every((val, idx) => val === sorted[idx])) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, "Multi-line package arguments are not sorted alphanumerically. Keeping them sorted makes maintenance easier and prevents duplicates.", this.help, inst.line));
|
|
411
448
|
}
|
|
412
449
|
}
|
|
413
450
|
}
|
|
@@ -422,7 +459,7 @@ const useraddNoLogInit = {
|
|
|
422
459
|
category: "Best Practices",
|
|
423
460
|
check(instructions, file) {
|
|
424
461
|
const diagnostics = [];
|
|
425
|
-
for (const inst of instructions) if (inst.instruction === "RUN" && /\buseradd\b/u.test(inst.args) && !inst.args.includes("--no-log-init")) diagnostics.push(createDiagnostic
|
|
462
|
+
for (const inst of instructions) if (inst.instruction === "RUN" && /\buseradd\b/u.test(inst.args) && !inst.args.includes("--no-log-init")) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, "RUN instruction runs 'useradd' without '--no-log-init'. This can cause excessive disk space usage / exhaustion under Go's sparse tar archive bug when large UIDs are used.", this.help, inst.line));
|
|
426
463
|
return diagnostics;
|
|
427
464
|
},
|
|
428
465
|
defaultSeverity: "warning",
|
|
@@ -445,17 +482,10 @@ const bestPracticesRules = [
|
|
|
445
482
|
|
|
446
483
|
//#endregion
|
|
447
484
|
//#region ../core/src/rules/compose.ts
|
|
448
|
-
const createDiagnostic$3 = (file, ruleKey, severity, message, help) => ({
|
|
449
|
-
file,
|
|
450
|
-
help,
|
|
451
|
-
message,
|
|
452
|
-
rule: ruleKey,
|
|
453
|
-
severity
|
|
454
|
-
});
|
|
455
485
|
const noVersionKey = {
|
|
456
486
|
category: "Compose",
|
|
457
|
-
check(composeContent, file) {
|
|
458
|
-
if (composeContent && typeof composeContent === "object" && "version" in composeContent) return [createDiagnostic
|
|
487
|
+
check(composeContent, file, context) {
|
|
488
|
+
if (composeContent && typeof composeContent === "object" && "version" in composeContent) return [createDiagnostic(file, this.key, this.defaultSeverity, "The 'version' property is deprecated. Remove it to use standard Compose spec behavior.", this.help, context?.locate?.(["version"]))];
|
|
459
489
|
return [];
|
|
460
490
|
},
|
|
461
491
|
defaultSeverity: "warning",
|
|
@@ -465,14 +495,14 @@ const noVersionKey = {
|
|
|
465
495
|
};
|
|
466
496
|
const requireResourceLimits = {
|
|
467
497
|
category: "Compose",
|
|
468
|
-
check(composeContent, file) {
|
|
498
|
+
check(composeContent, file, context) {
|
|
469
499
|
const diagnostics = [];
|
|
470
500
|
if (composeContent && typeof composeContent === "object" && "services" in composeContent) {
|
|
471
501
|
const { services } = composeContent;
|
|
472
502
|
if (services && typeof services === "object") {
|
|
473
503
|
for (const [name, config] of Object.entries(services)) if (config && typeof config === "object") {
|
|
474
504
|
const limits = (config.deploy?.resources)?.limits;
|
|
475
|
-
if (!limits || !limits.cpus && !limits.memory) diagnostics.push(createDiagnostic
|
|
505
|
+
if (!limits || !limits.cpus && !limits.memory) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Service '${name}' does not have CPU or memory limits defined. A resource leak in this service could crash the host.`, this.help, context?.locate?.(["services", name])));
|
|
476
506
|
}
|
|
477
507
|
}
|
|
478
508
|
}
|
|
@@ -485,7 +515,7 @@ const requireResourceLimits = {
|
|
|
485
515
|
};
|
|
486
516
|
const requireRestartPolicy = {
|
|
487
517
|
category: "Compose",
|
|
488
|
-
check(composeContent, file) {
|
|
518
|
+
check(composeContent, file, context) {
|
|
489
519
|
const diagnostics = [];
|
|
490
520
|
if (composeContent && typeof composeContent === "object" && "services" in composeContent) {
|
|
491
521
|
const { services } = composeContent;
|
|
@@ -493,7 +523,7 @@ const requireRestartPolicy = {
|
|
|
493
523
|
for (const [name, config] of Object.entries(services)) if (config && typeof config === "object") {
|
|
494
524
|
const hasRestart = "restart" in config;
|
|
495
525
|
const hasDeployRestart = config.deploy?.restart_policy !== void 0;
|
|
496
|
-
if (!hasRestart && !hasDeployRestart) diagnostics.push(createDiagnostic
|
|
526
|
+
if (!hasRestart && !hasDeployRestart) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Service '${name}' has no restart policy configured. It will not restart if it crashes or if the host reboots.`, this.help, context?.locate?.(["services", name])));
|
|
497
527
|
}
|
|
498
528
|
}
|
|
499
529
|
}
|
|
@@ -506,14 +536,18 @@ const requireRestartPolicy = {
|
|
|
506
536
|
};
|
|
507
537
|
const useDependsOnCondition = {
|
|
508
538
|
category: "Compose",
|
|
509
|
-
check(composeContent, file) {
|
|
539
|
+
check(composeContent, file, context) {
|
|
510
540
|
const diagnostics = [];
|
|
511
541
|
if (composeContent && typeof composeContent === "object" && "services" in composeContent) {
|
|
512
542
|
const { services } = composeContent;
|
|
513
543
|
if (services && typeof services === "object") {
|
|
514
544
|
for (const [name, config] of Object.entries(services)) if (config && typeof config === "object") {
|
|
515
545
|
const dependsOn = config.depends_on;
|
|
516
|
-
if (dependsOn && Array.isArray(dependsOn)) diagnostics.push(createDiagnostic
|
|
546
|
+
if (dependsOn && Array.isArray(dependsOn)) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Service '${name}' uses shorthand depends_on list. This only checks if containers are started, not if they are ready/healthy.`, this.help, context?.locate?.([
|
|
547
|
+
"services",
|
|
548
|
+
name,
|
|
549
|
+
"depends_on"
|
|
550
|
+
]) ?? context?.locate?.(["services", name])));
|
|
517
551
|
}
|
|
518
552
|
}
|
|
519
553
|
}
|
|
@@ -533,14 +567,6 @@ const composeRules = [
|
|
|
533
567
|
|
|
534
568
|
//#endregion
|
|
535
569
|
//#region ../core/src/rules/image-size.ts
|
|
536
|
-
const createDiagnostic$2 = (file, ruleKey, severity, message, help, line) => ({
|
|
537
|
-
file,
|
|
538
|
-
help,
|
|
539
|
-
line,
|
|
540
|
-
message,
|
|
541
|
-
rule: ruleKey,
|
|
542
|
-
severity
|
|
543
|
-
});
|
|
544
570
|
const preferSlimBase = {
|
|
545
571
|
category: "Image Size",
|
|
546
572
|
check(instructions, file) {
|
|
@@ -554,7 +580,7 @@ const preferSlimBase = {
|
|
|
554
580
|
if (ref.digest) continue;
|
|
555
581
|
if (!ref.tag) continue;
|
|
556
582
|
const haystack = `${ref.name} ${ref.tag}`.toLowerCase();
|
|
557
|
-
if (!(haystack.includes("alpine") || haystack.includes("slim") || haystack.includes("distroless") || haystack.includes("busybox"))) diagnostics.push(createDiagnostic
|
|
583
|
+
if (!(haystack.includes("alpine") || haystack.includes("slim") || haystack.includes("distroless") || haystack.includes("busybox"))) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Base image '${imagePart}' may be a full-OS distribution. Consider using a slim or alpine alternative.`, this.help, inst.line));
|
|
558
584
|
}
|
|
559
585
|
return diagnostics;
|
|
560
586
|
},
|
|
@@ -563,14 +589,20 @@ const preferSlimBase = {
|
|
|
563
589
|
key: "docker-doctor/prefer-slim-base",
|
|
564
590
|
message: "Prefer slim, alpine, or distroless base images"
|
|
565
591
|
};
|
|
592
|
+
const BUILDKIT_MOUNT_FLAG_RE = /--mount=(?<spec>\S+)/gu;
|
|
593
|
+
const CACHE_TARGET_KEY_RE = /^(?:target|dst|destination)=/u;
|
|
594
|
+
const cacheMountTargets = (args) => [...args.matchAll(BUILDKIT_MOUNT_FLAG_RE)].map((match) => (match.groups?.spec ?? "").split(",")).filter((options) => options.includes("type=cache")).flatMap((options) => options.filter((option) => CACHE_TARGET_KEY_RE.test(option)).map((option) => option.slice(option.indexOf("=") + 1)));
|
|
595
|
+
const APT_CACHE_DIRS = ["/var/lib/apt", "/var/cache/apt"];
|
|
596
|
+
const APK_CACHE_DIRS = ["/var/cache/apk", "/etc/apk/cache"];
|
|
597
|
+
const hasCacheMountFor = (args, cacheDirs) => cacheMountTargets(args).some((target) => cacheDirs.some((dir) => target === dir || target.startsWith(`${dir}/`)));
|
|
566
598
|
const cleanPackageCache = {
|
|
567
599
|
category: "Image Size",
|
|
568
600
|
check(instructions, file) {
|
|
569
601
|
const diagnostics = [];
|
|
570
602
|
for (const inst of instructions) if (inst.instruction === "RUN") {
|
|
571
603
|
const { args } = inst;
|
|
572
|
-
if (args.includes("apt-get install") && !args.includes("rm -rf /var/lib/apt/lists")) diagnostics.push(createDiagnostic
|
|
573
|
-
if (args.includes("apk add") && !args.includes("--no-cache") && !args.includes("rm -rf /var/cache/apk")) diagnostics.push(createDiagnostic
|
|
604
|
+
if (args.includes("apt-get install") && !args.includes("rm -rf /var/lib/apt/lists") && !hasCacheMountFor(args, APT_CACHE_DIRS)) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Running 'apt-get install' without removing package lists afterwards. This keeps metadata caches inside the image layer.`, this.help, inst.line));
|
|
605
|
+
if (args.includes("apk add") && !args.includes("--no-cache") && !args.includes("rm -rf /var/cache/apk") && !hasCacheMountFor(args, APK_CACHE_DIRS)) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Running 'apk add' without '--no-cache' or cleaning the apk cache. This increases layer size.`, this.help, inst.line));
|
|
574
606
|
}
|
|
575
607
|
return diagnostics;
|
|
576
608
|
},
|
|
@@ -607,7 +639,7 @@ const avoidDevDependencies = {
|
|
|
607
639
|
for (const inst of stage.runs) {
|
|
608
640
|
if (!installsDevDependencies(inst.args)) continue;
|
|
609
641
|
const where = stageIndex === finalIndex ? "in the final stage" : `in stage '${stage.name}', whose layers the final stage inherits,`;
|
|
610
|
-
diagnostics.push(createDiagnostic
|
|
642
|
+
diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Running package install '${inst.args}' ${where} without omitting devDependencies.`, this.help, inst.line));
|
|
611
643
|
}
|
|
612
644
|
}
|
|
613
645
|
return diagnostics;
|
|
@@ -625,19 +657,11 @@ const imageSizeRules = [
|
|
|
625
657
|
|
|
626
658
|
//#endregion
|
|
627
659
|
//#region ../core/src/rules/performance.ts
|
|
628
|
-
const createDiagnostic$1 = (file, ruleKey, severity, message, help, line) => ({
|
|
629
|
-
file,
|
|
630
|
-
help,
|
|
631
|
-
line,
|
|
632
|
-
message,
|
|
633
|
-
rule: ruleKey,
|
|
634
|
-
severity
|
|
635
|
-
});
|
|
636
660
|
const useMultiStage = {
|
|
637
661
|
category: "Performance",
|
|
638
662
|
check(instructions, file) {
|
|
639
663
|
if (instructions.filter((inst) => inst.instruction === "FROM").length === 1) {
|
|
640
|
-
if (instructions.some((inst) => inst.instruction === "RUN" && (inst.args.includes("npm run build") || inst.args.includes("yarn build") || inst.args.includes("bun run build") || inst.args.includes("cargo build") || inst.args.includes("make")))) return [createDiagnostic
|
|
664
|
+
if (instructions.some((inst) => inst.instruction === "RUN" && (inst.args.includes("npm run build") || inst.args.includes("yarn build") || inst.args.includes("bun run build") || inst.args.includes("cargo build") || inst.args.includes("make")))) return [createDiagnostic(file, this.key, this.defaultSeverity, "Only one build stage (FROM) was detected, but build instructions were found. Multi-stage builds can significantly reduce final image size.", this.help, instructions.find((inst) => inst.instruction === "FROM")?.line || 1)];
|
|
641
665
|
}
|
|
642
666
|
return [];
|
|
643
667
|
},
|
|
@@ -661,7 +685,7 @@ const orderLayers = {
|
|
|
661
685
|
}
|
|
662
686
|
if (inst.instruction === "RUN" && copyAllLine !== -1) {
|
|
663
687
|
const args = inst.args.toLowerCase();
|
|
664
|
-
if (args.includes("npm install") || args.includes("npm ci") || args.includes("yarn install") || args.includes("bun install") || args.includes("pip install") || args.includes("cargo fetch")) diagnostics.push(createDiagnostic
|
|
688
|
+
if (args.includes("npm install") || args.includes("npm ci") || args.includes("yarn install") || args.includes("bun install") || args.includes("pip install") || args.includes("cargo fetch")) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Running package installation command '${inst.args}' after copying application files (at line ${copyAllLine}). This invalidates the cache on any code changes.`, this.help, inst.line));
|
|
665
689
|
}
|
|
666
690
|
}
|
|
667
691
|
return diagnostics;
|
|
@@ -681,10 +705,10 @@ const minimizeLayers = {
|
|
|
681
705
|
if (consecutiveRunCount === 0) firstRunLine = inst.line;
|
|
682
706
|
consecutiveRunCount += 1;
|
|
683
707
|
} else {
|
|
684
|
-
if (consecutiveRunCount > 2) diagnostics.push(createDiagnostic
|
|
708
|
+
if (consecutiveRunCount > 2) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Found ${consecutiveRunCount} consecutive RUN instructions starting at line ${firstRunLine}. Consider combining them into a single RUN layer.`, this.help, firstRunLine));
|
|
685
709
|
consecutiveRunCount = 0;
|
|
686
710
|
}
|
|
687
|
-
if (consecutiveRunCount > 2) diagnostics.push(createDiagnostic
|
|
711
|
+
if (consecutiveRunCount > 2) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Found ${consecutiveRunCount} consecutive RUN instructions starting at line ${firstRunLine}. Consider combining them into a single RUN layer.`, this.help, firstRunLine));
|
|
688
712
|
return diagnostics;
|
|
689
713
|
},
|
|
690
714
|
defaultSeverity: "info",
|
|
@@ -709,7 +733,7 @@ const useDockerignore = {
|
|
|
709
733
|
return src === "." || src === "./" || src === "*";
|
|
710
734
|
}
|
|
711
735
|
return false;
|
|
712
|
-
}) && context?.projectFiles && !hasDockerignoreFor(file, context.projectFiles)) return [createDiagnostic
|
|
736
|
+
}) && context?.projectFiles && !hasDockerignoreFor(file, context.projectFiles)) return [createDiagnostic(file, this.key, this.defaultSeverity, "Using COPY/ADD with a wildcard or directory, but no .dockerignore file was found next to the Dockerfile or at the project root. This can copy local build folders and secrets.", this.help, 1)];
|
|
713
737
|
return [];
|
|
714
738
|
},
|
|
715
739
|
defaultSeverity: "warning",
|
|
@@ -726,14 +750,6 @@ const performanceRules = [
|
|
|
726
750
|
|
|
727
751
|
//#endregion
|
|
728
752
|
//#region ../core/src/rules/security.ts
|
|
729
|
-
const createDiagnostic = (file, ruleKey, severity, message, help, line) => ({
|
|
730
|
-
file,
|
|
731
|
-
help,
|
|
732
|
-
line,
|
|
733
|
-
message,
|
|
734
|
-
rule: ruleKey,
|
|
735
|
-
severity
|
|
736
|
-
});
|
|
737
753
|
const isRootUser = (value) => {
|
|
738
754
|
const [user] = value.split(":");
|
|
739
755
|
return user === "root" || user === "0";
|
|
@@ -880,12 +896,12 @@ const runDockerfileRules = (instructions, file, projectFiles, rulesConfig, categ
|
|
|
880
896
|
|
|
881
897
|
//#endregion
|
|
882
898
|
//#region ../core/src/runners/compose-runner.ts
|
|
883
|
-
const runComposeRules = (composeContent, file, rulesConfig, categoriesConfig) => {
|
|
899
|
+
const runComposeRules = (composeContent, file, rulesConfig, categoriesConfig, locate) => {
|
|
884
900
|
const diagnostics = [];
|
|
885
901
|
for (const rule of allComposeRules) {
|
|
886
902
|
const severity = resolveSeverity(rule, rulesConfig, categoriesConfig);
|
|
887
903
|
if (severity === "off") continue;
|
|
888
|
-
const ruleDiagnostics = rule.check(composeContent, file);
|
|
904
|
+
const ruleDiagnostics = rule.check(composeContent, file, { locate });
|
|
889
905
|
if (severity !== rule.defaultSeverity) for (const diag of ruleDiagnostics) diag.severity = severity;
|
|
890
906
|
diagnostics.push(...ruleDiagnostics);
|
|
891
907
|
}
|
|
@@ -1116,5 +1132,5 @@ const toJsonReport = (diagnostics, score, label, project) => ({
|
|
|
1116
1132
|
});
|
|
1117
1133
|
|
|
1118
1134
|
//#endregion
|
|
1119
|
-
export { runDockerfileRules as a,
|
|
1120
|
-
//# sourceMappingURL=src-
|
|
1135
|
+
export { runDockerfileRules as a, createComposeLocator as c, discoverProject as d, version as f, runComposeRules as i, parseCompose as l, calculateScore as n, allRules as o, loadConfig as r, findRule as s, toJsonReport as t, parseDockerfile as u };
|
|
1136
|
+
//# sourceMappingURL=src-CvOv-hL3.mjs.map
|