@docker-doctor/cli 0.6.0 → 0.6.1
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 +66 -20
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +66 -20
- package/dist/cli.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{src-BpAv-H0Z.cjs → src-C7XK6Kp-.cjs} +123 -52
- package/dist/src-C7XK6Kp-.cjs.map +1 -0
- package/dist/{src-BLeXr_hR.mjs → src-DiEWLJ6g.mjs} +123 -52
- package/dist/src-DiEWLJ6g.mjs.map +1 -0
- package/package.json +1 -1
- package/skill/docker-doctor/SKILL.md +7 -5
- package/skill/docker-doctor/references/explain.md +2 -2
- package/dist/src-BLeXr_hR.mjs.map +0 -1
- package/dist/src-BpAv-H0Z.cjs.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-C7XK6Kp-.cjs');
|
|
4
4
|
|
|
5
5
|
//#region ../core/src/config/define-config.ts
|
|
6
6
|
const defineConfig = (config) => config;
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
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-
|
|
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-DiEWLJ6g.mjs";
|
|
3
3
|
|
|
4
4
|
//#region ../core/src/config/define-config.ts
|
|
5
5
|
const defineConfig = (config) => config;
|
|
@@ -31,9 +31,10 @@ node_fs_promises = __toESM(node_fs_promises, 1);
|
|
|
31
31
|
let node_path = require("node:path");
|
|
32
32
|
node_path = __toESM(node_path, 1);
|
|
33
33
|
let yaml = require("yaml");
|
|
34
|
+
let node_url = require("node:url");
|
|
34
35
|
|
|
35
36
|
//#region package.json
|
|
36
|
-
var version = "0.6.
|
|
37
|
+
var version = "0.6.1";
|
|
37
38
|
|
|
38
39
|
//#endregion
|
|
39
40
|
//#region ../core/src/project-info/ignore.ts
|
|
@@ -73,6 +74,17 @@ const globToRegExp = (pattern) => {
|
|
|
73
74
|
}
|
|
74
75
|
return new RegExp(`${source}$`, "u");
|
|
75
76
|
};
|
|
77
|
+
const LEADING_DOT_SLASH_RE = /^(?:\.\/)+/u;
|
|
78
|
+
/**
|
|
79
|
+
* Accepts two spellings people bring from `.gitignore`. A leading `./` is
|
|
80
|
+
* dropped because patterns are already root-relative, and a trailing `/`
|
|
81
|
+
* means the whole directory. `vendor/` and `./vendor/**` both become
|
|
82
|
+
* `vendor/**`.
|
|
83
|
+
*/
|
|
84
|
+
const normalizeIgnorePattern = (pattern) => {
|
|
85
|
+
const rootRelative = pattern.replace(LEADING_DOT_SLASH_RE, "");
|
|
86
|
+
return rootRelative.endsWith("/") ? `${rootRelative}**` : rootRelative;
|
|
87
|
+
};
|
|
76
88
|
/**
|
|
77
89
|
* Builds a predicate over root-relative paths from `ignore.files`
|
|
78
90
|
* patterns. Windows separators in the tested path are normalized to `/`
|
|
@@ -80,7 +92,7 @@ const globToRegExp = (pattern) => {
|
|
|
80
92
|
*/
|
|
81
93
|
const createIgnoreMatcher = (patterns) => {
|
|
82
94
|
if (!patterns || patterns.length === 0) return () => false;
|
|
83
|
-
const regexps = patterns.map(globToRegExp);
|
|
95
|
+
const regexps = patterns.map(normalizeIgnorePattern).map(globToRegExp);
|
|
84
96
|
return (relativePath) => {
|
|
85
97
|
const normalized = relativePath.replaceAll("\\", "/");
|
|
86
98
|
return regexps.some((regexp) => regexp.test(normalized));
|
|
@@ -117,7 +129,7 @@ const PRUNED_DIRECTORIES = [
|
|
|
117
129
|
const PRUNED = new Set(PRUNED_DIRECTORIES);
|
|
118
130
|
const toPosixRelative = (rootDir, file) => node_path.default.relative(rootDir, file).split(node_path.default.sep).join("/");
|
|
119
131
|
const SUBTREE_SUFFIX = "/**";
|
|
120
|
-
const createPrunedDirectoryMatcher = (patterns = []) => createIgnoreMatcher(patterns.filter((pattern) => pattern.endsWith(SUBTREE_SUFFIX)).map((pattern) => pattern.slice(0, -3)));
|
|
132
|
+
const createPrunedDirectoryMatcher = (patterns = []) => createIgnoreMatcher(patterns.map(normalizeIgnorePattern).filter((pattern) => pattern.endsWith(SUBTREE_SUFFIX)).map((pattern) => pattern.slice(0, -3)));
|
|
121
133
|
const isPermissionError = (error) => error instanceof Error && "code" in error && (error.code === "EACCES" || error.code === "EPERM");
|
|
122
134
|
const walk = async (dir, options, fileList = []) => {
|
|
123
135
|
let files;
|
|
@@ -265,7 +277,7 @@ const HEREDOC_INSTRUCTIONS = /* @__PURE__ */ new Set([
|
|
|
265
277
|
"RUN"
|
|
266
278
|
]);
|
|
267
279
|
const INSTRUCTION_LINE_RE = /^(?<inst>[A-Za-z]+)\s+(?<args>.*)$/u;
|
|
268
|
-
const HEREDOC_OPENER_RE = /(?<=^|\s)<<-?(?<quote>['"]?)(?<delim
|
|
280
|
+
const HEREDOC_OPENER_RE = /(?<=^|\s)<<-?(?<quote>['"]?)(?<delim>[^\s'"<>;&|()]+)\k<quote>/gu;
|
|
269
281
|
const createParserState = () => ({
|
|
270
282
|
currentArgs: "",
|
|
271
283
|
currentInstruction: "",
|
|
@@ -412,7 +424,7 @@ const parseExecForm = (args) => {
|
|
|
412
424
|
//#endregion
|
|
413
425
|
//#region ../core/src/parsers/image-ref.ts
|
|
414
426
|
const parseImageRef = (ref) => {
|
|
415
|
-
if (ref.includes("$
|
|
427
|
+
if (ref.includes("$")) return {
|
|
416
428
|
isVariable: true,
|
|
417
429
|
name: ref
|
|
418
430
|
};
|
|
@@ -464,6 +476,15 @@ const isHardenedRuntimeImage = (imagePart) => {
|
|
|
464
476
|
const { tag } = parseImageRef(imagePart);
|
|
465
477
|
return !(tag === "dev" || tag?.endsWith("-dev"));
|
|
466
478
|
};
|
|
479
|
+
const DISTROLESS_NONROOT_TAG_RE = /(?:^|-)nonroot(?:-|$)/u;
|
|
480
|
+
/**
|
|
481
|
+
* Distroless images tagged `nonroot` (also `debug-nonroot` and the
|
|
482
|
+
* per-architecture `nonroot-amd64` forms) run as uid 65532.
|
|
483
|
+
*/
|
|
484
|
+
const isDistrolessNonroot = (imagePart) => {
|
|
485
|
+
const { name, registry, tag } = parseImageRef(imagePart);
|
|
486
|
+
return registry === "gcr.io" && name.startsWith("distroless/") && tag !== void 0 && DISTROLESS_NONROOT_TAG_RE.test(tag);
|
|
487
|
+
};
|
|
467
488
|
/**
|
|
468
489
|
* Why a reference would resolve differently over time: no tag at all, or
|
|
469
490
|
* the mutable `latest` tag without a digest. `undefined` means the ref is
|
|
@@ -493,6 +514,11 @@ const collectStageAliases = (instructions) => {
|
|
|
493
514
|
return aliases;
|
|
494
515
|
};
|
|
495
516
|
|
|
517
|
+
//#endregion
|
|
518
|
+
//#region ../core/src/parsers/run-flags.ts
|
|
519
|
+
const LEADING_RUN_FLAGS_RE = /^(?:--[a-z][a-z-]*(?:=\S*)?\s+)+/u;
|
|
520
|
+
const stripRunFlags = (args) => args.replace(LEADING_RUN_FLAGS_RE, "");
|
|
521
|
+
|
|
496
522
|
//#endregion
|
|
497
523
|
//#region ../core/src/rules/create-diagnostic.ts
|
|
498
524
|
const createDiagnostic = (file, ruleKey, severity, message, help, line) => ({
|
|
@@ -636,12 +662,14 @@ const usePipefail = {
|
|
|
636
662
|
key: "docker-doctor/use-pipefail",
|
|
637
663
|
message: "Use pipefail to catch pipeline command failures"
|
|
638
664
|
};
|
|
665
|
+
const SURROUNDING_QUOTES_RE$1 = /^(?<quote>["'])(?<inner>.*)\k<quote>$/u;
|
|
639
666
|
const absoluteWorkdir = {
|
|
640
667
|
category: "Best Practices",
|
|
641
668
|
check(instructions, file) {
|
|
642
669
|
const diagnostics = [];
|
|
643
670
|
for (const inst of instructions) if (inst.instruction === "WORKDIR") {
|
|
644
|
-
const
|
|
671
|
+
const trimmed = inst.args.trim();
|
|
672
|
+
const path = trimmed.match(SURROUNDING_QUOTES_RE$1)?.groups?.inner ?? trimmed;
|
|
645
673
|
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));
|
|
646
674
|
}
|
|
647
675
|
return diagnostics;
|
|
@@ -655,7 +683,7 @@ const avoidRunCd = {
|
|
|
655
683
|
category: "Best Practices",
|
|
656
684
|
check(instructions, file) {
|
|
657
685
|
const diagnostics = [];
|
|
658
|
-
for (const inst of instructions) if (inst.instruction === "RUN" && CD_COMMAND_RE.test(maskQuotedText(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));
|
|
686
|
+
for (const inst of instructions) if (inst.instruction === "RUN" && CD_COMMAND_RE.test(maskQuotedText(stripRunFlags(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));
|
|
659
687
|
return diagnostics;
|
|
660
688
|
},
|
|
661
689
|
defaultSeverity: "info",
|
|
@@ -666,11 +694,13 @@ const avoidRunCd = {
|
|
|
666
694
|
const PACKAGE_LIST_START_RE = /\b(?:apt-get(?:\s+-\S+)*\s+install|apk\s+add|yum\s+install|dnf\s+install)\b/u;
|
|
667
695
|
const SHELL_SEPARATOR_RE$1 = /[;|]|&&/u;
|
|
668
696
|
const WHITESPACE_RE$1 = /\s+/u;
|
|
697
|
+
const UNCONTINUED_NEWLINE_RE = /(?<!\\[ \t]*)\r?\n/u;
|
|
669
698
|
const collectPackageList = (raw) => {
|
|
670
699
|
const start = PACKAGE_LIST_START_RE.exec(raw);
|
|
671
700
|
if (!start) return [];
|
|
672
701
|
const packages = [];
|
|
673
|
-
const
|
|
702
|
+
const [command] = raw.slice(start.index + start[0].length).split(UNCONTINUED_NEWLINE_RE);
|
|
703
|
+
const tokens = command.split(WHITESPACE_RE$1);
|
|
674
704
|
for (const token of tokens) {
|
|
675
705
|
if (SHELL_SEPARATOR_RE$1.test(token)) break;
|
|
676
706
|
if (token !== "" && token !== "\\" && !token.startsWith("-")) packages.push(token);
|
|
@@ -700,15 +730,18 @@ const sortMultilineArgs = {
|
|
|
700
730
|
key: "docker-doctor/sort-multiline-args",
|
|
701
731
|
message: "Sort multi-line arguments alphanumerically"
|
|
702
732
|
};
|
|
733
|
+
const USERADD_COMMAND_RE = /\buseradd\b/u;
|
|
734
|
+
const NO_LOG_INIT_FLAG_RE = /(?:^|\s)(?:--no-log-init|-[A-Za-z]*l[A-Za-z]*)(?=\s|$)/u;
|
|
735
|
+
const useraddWritesLoginLogs = (args) => args.split(SHELL_SEPARATOR_RE$1).some((command) => USERADD_COMMAND_RE.test(command) && !NO_LOG_INIT_FLAG_RE.test(command));
|
|
703
736
|
const useraddNoLogInit = {
|
|
704
737
|
category: "Best Practices",
|
|
705
738
|
check(instructions, file) {
|
|
706
739
|
const diagnostics = [];
|
|
707
|
-
for (const inst of instructions) if (inst.instruction === "RUN" &&
|
|
740
|
+
for (const inst of instructions) if (inst.instruction === "RUN" && useraddWritesLoginLogs(inst.args)) 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));
|
|
708
741
|
return diagnostics;
|
|
709
742
|
},
|
|
710
743
|
defaultSeverity: "warning",
|
|
711
|
-
help: "Pass `--no-log-init`
|
|
744
|
+
help: "Pass `--no-log-init` or its short form `-l` to useradd (e.g., `RUN useradd --no-log-init -r -g mygroup myuser`).",
|
|
712
745
|
key: "docker-doctor/useradd-no-log-init",
|
|
713
746
|
message: "Use --no-log-init with useradd"
|
|
714
747
|
};
|
|
@@ -727,6 +760,13 @@ const bestPracticesRules = [
|
|
|
727
760
|
|
|
728
761
|
//#endregion
|
|
729
762
|
//#region ../core/src/rules/compose-services.ts
|
|
763
|
+
const TRUE_SPELLINGS = /* @__PURE__ */ new Set([
|
|
764
|
+
"true",
|
|
765
|
+
"y",
|
|
766
|
+
"yes",
|
|
767
|
+
"on"
|
|
768
|
+
]);
|
|
769
|
+
const isComposeTrue = (value) => value === true || typeof value === "string" && TRUE_SPELLINGS.has(value.trim().toLowerCase());
|
|
730
770
|
/**
|
|
731
771
|
* Narrows an unknown compose document to its service entries. A service
|
|
732
772
|
* with a null body (`web:` with nothing under it) is returned as an empty
|
|
@@ -993,7 +1033,7 @@ const volumeMount = (volume) => {
|
|
|
993
1033
|
if (volume && typeof volume === "object") {
|
|
994
1034
|
const { read_only: readOnly, source, target, type } = volume;
|
|
995
1035
|
return {
|
|
996
|
-
readOnly: readOnly
|
|
1036
|
+
readOnly: isComposeTrue(readOnly),
|
|
997
1037
|
source: typeof source === "string" ? source : void 0,
|
|
998
1038
|
target: typeof target === "string" ? target : void 0,
|
|
999
1039
|
type: typeof type === "string" ? type : void 0
|
|
@@ -1078,12 +1118,14 @@ const SECRET_KEY_PATTERNS = [
|
|
|
1078
1118
|
/(?:^|[_-])auth(?:[_-]|$)/iu,
|
|
1079
1119
|
/(?:^|[_-])pat(?:[_-]|$)/iu
|
|
1080
1120
|
];
|
|
1081
|
-
const
|
|
1121
|
+
const SECRET_LOCATION_SUFFIX = /[_-](?:file|path)$/iu;
|
|
1122
|
+
const isSecretKey = (key) => !SECRET_LOCATION_SUFFIX.test(key) && SECRET_KEY_PATTERNS.some((regex) => regex.test(key));
|
|
1082
1123
|
const CREDENTIAL_FREE_URL = /^[a-z][a-z0-9+.-]*:\/\/[^@/\s]*(?:\/|$)/iu;
|
|
1124
|
+
const SWITCH_VALUE = /^(?:true|false|yes|no|on|off)$/iu;
|
|
1083
1125
|
const SURROUNDING_QUOTES_RE = /^(?<quote>["'])(?<inner>.*)\k<quote>$/su;
|
|
1084
1126
|
const isLiteralSecretValue = (value) => {
|
|
1085
1127
|
const unquoted = value.match(SURROUNDING_QUOTES_RE)?.groups?.inner ?? value;
|
|
1086
|
-
return unquoted.length > 0 && !unquoted.startsWith("$") && !CREDENTIAL_FREE_URL.test(unquoted);
|
|
1128
|
+
return unquoted.length > 0 && !unquoted.startsWith("$") && !SWITCH_VALUE.test(unquoted) && !CREDENTIAL_FREE_URL.test(unquoted);
|
|
1087
1129
|
};
|
|
1088
1130
|
|
|
1089
1131
|
//#endregion
|
|
@@ -1092,7 +1134,7 @@ const noPrivilegedService = {
|
|
|
1092
1134
|
category: "Compose",
|
|
1093
1135
|
check(composeContent, file, context) {
|
|
1094
1136
|
const diagnostics = [];
|
|
1095
|
-
for (const [name, config] of composeServices(composeContent)) if (config.privileged
|
|
1137
|
+
for (const [name, config] of composeServices(composeContent)) if (isComposeTrue(config.privileged)) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Service '${name}' runs in privileged mode. A privileged container has full access to the host's devices and kernel, so compromising this service compromises the host.`, this.help, context?.locate?.([
|
|
1096
1138
|
"services",
|
|
1097
1139
|
name,
|
|
1098
1140
|
"privileged"
|
|
@@ -1244,13 +1286,56 @@ const composeSecurityRules = [
|
|
|
1244
1286
|
|
|
1245
1287
|
//#endregion
|
|
1246
1288
|
//#region ../core/src/rules/image-size.ts
|
|
1289
|
+
const NODE_ENV_ASSIGNMENT_RE = /(?:^|\s)NODE_ENV(?:=|\s+)["']?(?<value>[^\s"']*)/u;
|
|
1290
|
+
const collectStages = (instructions) => {
|
|
1291
|
+
const stages = [];
|
|
1292
|
+
const productionEnv = [];
|
|
1293
|
+
for (const inst of instructions) if (inst.instruction === "FROM") {
|
|
1294
|
+
const { base, stage } = parseFromArgs(inst.args);
|
|
1295
|
+
const baseKey = base?.toLowerCase() ?? "";
|
|
1296
|
+
const parent = stages.findIndex((s) => s.name !== null && s.name === baseKey);
|
|
1297
|
+
productionEnv.push(parent !== -1 && productionEnv[parent]);
|
|
1298
|
+
stages.push({
|
|
1299
|
+
base: baseKey,
|
|
1300
|
+
from: inst,
|
|
1301
|
+
name: stage?.toLowerCase() ?? null,
|
|
1302
|
+
runs: []
|
|
1303
|
+
});
|
|
1304
|
+
} else if (inst.instruction === "ENV" && stages.length > 0) {
|
|
1305
|
+
const value = NODE_ENV_ASSIGNMENT_RE.exec(inst.args)?.groups?.value;
|
|
1306
|
+
if (value !== void 0) productionEnv[stages.length - 1] = value === "production";
|
|
1307
|
+
} else if (inst.instruction === "RUN" && stages.length > 0 && !productionEnv[stages.length - 1]) stages.at(-1)?.runs.push(inst);
|
|
1308
|
+
return stages;
|
|
1309
|
+
};
|
|
1310
|
+
const shippedStages = (stages) => {
|
|
1311
|
+
const shipped = [];
|
|
1312
|
+
let index = stages.length - 1;
|
|
1313
|
+
while (index >= 0) {
|
|
1314
|
+
shipped.unshift(stages[index]);
|
|
1315
|
+
const { base } = stages[index];
|
|
1316
|
+
index = stages.slice(0, index).findIndex((s) => s.name !== null && s.name === base);
|
|
1317
|
+
}
|
|
1318
|
+
return shipped;
|
|
1319
|
+
};
|
|
1320
|
+
const MINIMAL_BASE_MARKERS = [
|
|
1321
|
+
"alpine",
|
|
1322
|
+
"slim",
|
|
1323
|
+
"distroless",
|
|
1324
|
+
"busybox",
|
|
1325
|
+
"chainguard",
|
|
1326
|
+
"wolfi",
|
|
1327
|
+
"chiseled",
|
|
1328
|
+
"minideb",
|
|
1329
|
+
"nanoserver"
|
|
1330
|
+
];
|
|
1331
|
+
const MINIMAL_BASE_SEGMENT_RE = /(?:^|[/_. -])(?:minimal|micro)(?:$|[/_. -])/u;
|
|
1247
1332
|
const preferSlimBase = {
|
|
1248
1333
|
category: "Image Size",
|
|
1249
1334
|
check(instructions, file) {
|
|
1250
1335
|
const diagnostics = [];
|
|
1251
1336
|
const stageAliases = collectStageAliases(instructions);
|
|
1252
|
-
for (const
|
|
1253
|
-
const imagePart = parseFromArgs(
|
|
1337
|
+
for (const { from } of shippedStages(collectStages(instructions))) {
|
|
1338
|
+
const imagePart = parseFromArgs(from.args).base;
|
|
1254
1339
|
if (!imagePart || isScratch(imagePart)) continue;
|
|
1255
1340
|
const ref = parseImageRef(imagePart);
|
|
1256
1341
|
if (ref.isVariable || stageAliases.has(imagePart.toLowerCase())) continue;
|
|
@@ -1258,7 +1343,7 @@ const preferSlimBase = {
|
|
|
1258
1343
|
if (ref.digest) continue;
|
|
1259
1344
|
if (!ref.tag) continue;
|
|
1260
1345
|
const haystack = `${ref.name} ${ref.tag}`.toLowerCase();
|
|
1261
|
-
if (!(
|
|
1346
|
+
if (!(MINIMAL_BASE_MARKERS.some((marker) => haystack.includes(marker)) || MINIMAL_BASE_SEGMENT_RE.test(haystack))) 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, from.line));
|
|
1262
1347
|
}
|
|
1263
1348
|
return diagnostics;
|
|
1264
1349
|
},
|
|
@@ -1270,6 +1355,8 @@ const preferSlimBase = {
|
|
|
1270
1355
|
const BUILDKIT_MOUNT_FLAG_RE = /--mount=(?<spec>\S+)/gu;
|
|
1271
1356
|
const CACHE_TARGET_KEY_RE = /^(?:target|dst|destination)=/u;
|
|
1272
1357
|
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)));
|
|
1358
|
+
const RECURSIVE_RM_RE = /\brm\s+(?:-[A-Za-z]+\s+)*-[A-Za-z]*[rR][A-Za-z]*\s(?<operands>[^;&|]*)/gu;
|
|
1359
|
+
const removesRecursively = (args, dir) => [...args.matchAll(RECURSIVE_RM_RE)].some((match) => (match.groups?.operands ?? "").includes(dir));
|
|
1273
1360
|
const APT_CACHE_DIRS = ["/var/lib/apt", "/var/cache/apt"];
|
|
1274
1361
|
const APK_CACHE_DIRS = ["/var/cache/apk", "/etc/apk/cache"];
|
|
1275
1362
|
const hasCacheMountFor = (args, cacheDirs) => cacheMountTargets(args).some((target) => cacheDirs.some((dir) => target === dir || target.startsWith(`${dir}/`)));
|
|
@@ -1279,8 +1366,8 @@ const cleanPackageCache = {
|
|
|
1279
1366
|
const diagnostics = [];
|
|
1280
1367
|
for (const inst of instructions) if (inst.instruction === "RUN") {
|
|
1281
1368
|
const { args } = inst;
|
|
1282
|
-
if (hasAptGetInstall(args) && !args
|
|
1283
|
-
if (args.includes("apk add") && !args.includes("--no-cache") && !args
|
|
1369
|
+
if (hasAptGetInstall(args) && !removesRecursively(args, "/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));
|
|
1370
|
+
if (args.includes("apk add") && !args.includes("--no-cache") && !removesRecursively(args, "/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));
|
|
1284
1371
|
}
|
|
1285
1372
|
return diagnostics;
|
|
1286
1373
|
},
|
|
@@ -1307,6 +1394,7 @@ const NON_DEV_INSTALL_FLAGS = /* @__PURE__ */ new Set([
|
|
|
1307
1394
|
"-g",
|
|
1308
1395
|
"--global",
|
|
1309
1396
|
"--production",
|
|
1397
|
+
"--production=true",
|
|
1310
1398
|
"--omit=dev",
|
|
1311
1399
|
"--only=prod",
|
|
1312
1400
|
"--only=production",
|
|
@@ -1322,36 +1410,17 @@ const segmentInstallsDevDependencies = (segment) => {
|
|
|
1322
1410
|
if (!(NODE_PACKAGE_MANAGERS.has(manager) && INSTALL_VERBS.has(verb))) return false;
|
|
1323
1411
|
return !tokens.slice(0, commandStart).includes("NODE_ENV=production") && options.every((option) => option.startsWith("-") && !NON_DEV_INSTALL_FLAGS.has(option));
|
|
1324
1412
|
};
|
|
1325
|
-
const installsDevDependencies = (args) => !args.includes("prune") && maskQuotedText(args).split(SHELL_SEPARATOR_RE).some(segmentInstallsDevDependencies);
|
|
1413
|
+
const installsDevDependencies = (args) => !args.includes("prune") && maskQuotedText(stripRunFlags(args)).split(SHELL_SEPARATOR_RE).some(segmentInstallsDevDependencies);
|
|
1326
1414
|
const avoidDevDependencies = {
|
|
1327
1415
|
category: "Image Size",
|
|
1328
1416
|
check(instructions, file) {
|
|
1329
1417
|
const diagnostics = [];
|
|
1330
|
-
const stages =
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
runs: []
|
|
1337
|
-
});
|
|
1338
|
-
} else if (inst.instruction === "RUN" && stages.length > 0) stages.at(-1)?.runs.push(inst);
|
|
1339
|
-
if (stages.length === 0) return diagnostics;
|
|
1340
|
-
const auditedIndices = [];
|
|
1341
|
-
let index = stages.length - 1;
|
|
1342
|
-
while (index >= 0) {
|
|
1343
|
-
auditedIndices.push(index);
|
|
1344
|
-
const { base } = stages[index];
|
|
1345
|
-
index = stages.slice(0, index).findIndex((s) => s.name !== null && s.name === base);
|
|
1346
|
-
}
|
|
1347
|
-
const finalIndex = stages.length - 1;
|
|
1348
|
-
for (const stageIndex of auditedIndices.toReversed()) {
|
|
1349
|
-
const stage = stages[stageIndex];
|
|
1350
|
-
for (const inst of stage.runs) {
|
|
1351
|
-
if (!installsDevDependencies(inst.args)) continue;
|
|
1352
|
-
const where = stageIndex === finalIndex ? "in the final stage" : `in stage '${stage.name}', whose layers the final stage inherits,`;
|
|
1353
|
-
diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Running package install '${inst.args}' ${where} without omitting devDependencies.`, this.help, inst.line));
|
|
1354
|
-
}
|
|
1418
|
+
const stages = collectStages(instructions);
|
|
1419
|
+
const finalStage = stages.at(-1);
|
|
1420
|
+
for (const stage of shippedStages(stages)) for (const inst of stage.runs) {
|
|
1421
|
+
if (!installsDevDependencies(inst.args)) continue;
|
|
1422
|
+
const where = stage === finalStage ? "in the final stage" : `in stage '${stage.name}', whose layers the final stage inherits,`;
|
|
1423
|
+
diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Running package install '${inst.args}' ${where} without omitting devDependencies.`, this.help, inst.line));
|
|
1355
1424
|
}
|
|
1356
1425
|
return diagnostics;
|
|
1357
1426
|
},
|
|
@@ -1373,7 +1442,7 @@ const useMultiStage = {
|
|
|
1373
1442
|
category: "Performance",
|
|
1374
1443
|
check(instructions, file) {
|
|
1375
1444
|
if (instructions.filter((inst) => inst.instruction === "FROM").length === 1) {
|
|
1376
|
-
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") || BUILD_TOOL_COMMAND_RE.test(maskQuotedText(inst.args))))) 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)];
|
|
1445
|
+
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") || BUILD_TOOL_COMMAND_RE.test(maskQuotedText(stripRunFlags(inst.args)))))) 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)];
|
|
1377
1446
|
}
|
|
1378
1447
|
return [];
|
|
1379
1448
|
},
|
|
@@ -1476,7 +1545,7 @@ const noRootUser = {
|
|
|
1476
1545
|
let lastUserLine = 1;
|
|
1477
1546
|
for (const inst of instructions) if (inst.instruction === "FROM") {
|
|
1478
1547
|
const { base, stage } = parseFromArgs(inst.args);
|
|
1479
|
-
const baseDefaultUser = base && isHardenedRuntimeImage(base) ? "nonroot" : "root";
|
|
1548
|
+
const baseDefaultUser = base && (isHardenedRuntimeImage(base) || isDistrolessNonroot(base)) ? "nonroot" : "root";
|
|
1480
1549
|
lastUser = stageUser.get(base?.toLowerCase() ?? "") ?? baseDefaultUser;
|
|
1481
1550
|
lastUserLine = inst.line;
|
|
1482
1551
|
currentStage = stage?.toLowerCase() ?? null;
|
|
@@ -1555,14 +1624,16 @@ const noAddRemote = {
|
|
|
1555
1624
|
check(instructions, file) {
|
|
1556
1625
|
const diagnostics = [];
|
|
1557
1626
|
for (const inst of instructions) if (inst.instruction === "ADD") {
|
|
1558
|
-
const
|
|
1559
|
-
|
|
1627
|
+
const parts = inst.args.split(/\s+/u);
|
|
1628
|
+
const src = parts.find((p) => !p.startsWith("--"));
|
|
1629
|
+
const isVerified = parts.some((p) => p.startsWith("--checksum="));
|
|
1630
|
+
if (!src || isVerified) continue;
|
|
1560
1631
|
if (src.startsWith("http://") || src.startsWith("https://")) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `ADD instruction uses a remote URL '${src}'. Remote files added via ADD cannot be cleaned up in later layers, increasing image size.`, this.help, inst.line));
|
|
1561
1632
|
}
|
|
1562
1633
|
return diagnostics;
|
|
1563
1634
|
},
|
|
1564
1635
|
defaultSeverity: "warning",
|
|
1565
|
-
help: "
|
|
1636
|
+
help: "Add `--checksum=sha256:<digest>` so BuildKit verifies the download, or use `RUN curl` or `RUN wget` and delete the downloaded archive in the same layer to minimize size.",
|
|
1566
1637
|
key: "docker-doctor/no-add-remote",
|
|
1567
1638
|
message: "Avoid using ADD with remote URLs"
|
|
1568
1639
|
};
|
|
@@ -1746,7 +1817,7 @@ const importConfig = async (filePath) => {
|
|
|
1746
1817
|
if (filePath.endsWith(".json")) return parseConfigFile(filePath, "JSON", JSON.parse);
|
|
1747
1818
|
if (filePath.endsWith(".yaml") || filePath.endsWith(".yml")) return parseConfigFile(filePath, "YAML", yaml.parse);
|
|
1748
1819
|
try {
|
|
1749
|
-
const configModule = await import(filePath);
|
|
1820
|
+
const configModule = await import((0, node_url.pathToFileURL)(filePath).href);
|
|
1750
1821
|
return configModule.default || configModule;
|
|
1751
1822
|
} catch (error) {
|
|
1752
1823
|
const reason = typeStrippingFailure(filePath, error);
|
|
@@ -1952,4 +2023,4 @@ Object.defineProperty(exports, 'version', {
|
|
|
1952
2023
|
return version;
|
|
1953
2024
|
}
|
|
1954
2025
|
});
|
|
1955
|
-
//# sourceMappingURL=src-
|
|
2026
|
+
//# sourceMappingURL=src-C7XK6Kp-.cjs.map
|