@docker-doctor/cli 0.5.3 → 0.6.0
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/README.md +1 -1
- package/dist/cli.cjs +66 -13
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +66 -13
- package/dist/cli.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +7 -1
- package/dist/index.d.mts +7 -1
- package/dist/index.mjs +1 -1
- package/dist/{src-CklEIXTX.mjs → src-BLeXr_hR.mjs} +165 -32
- package/dist/src-BLeXr_hR.mjs.map +1 -0
- package/dist/{src-D4rVJg36.cjs → src-BpAv-H0Z.cjs} +165 -32
- package/dist/src-BpAv-H0Z.cjs.map +1 -0
- package/package.json +2 -1
- package/dist/src-CklEIXTX.mjs.map +0 -1
- package/dist/src-D4rVJg36.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-BpAv-H0Z.cjs');
|
|
4
4
|
|
|
5
5
|
//#region ../core/src/config/define-config.ts
|
|
6
6
|
const defineConfig = (config) => config;
|
package/dist/index.d.cts
CHANGED
|
@@ -48,6 +48,7 @@ interface DockerDoctorConfig {
|
|
|
48
48
|
//#region ../core/src/project-info/discover.d.ts
|
|
49
49
|
export declare const discoverProject: (rootDir: string, options?: {
|
|
50
50
|
ignoreFiles?: readonly string[];
|
|
51
|
+
onSkippedDirectory?: (relativeDir: string) => void;
|
|
51
52
|
}) => Promise<ProjectInfo>;
|
|
52
53
|
//#endregion
|
|
53
54
|
//#region ../core/src/parsers/dockerfile-parser.d.ts
|
|
@@ -79,6 +80,10 @@ export declare const calculateScore: (diagnostics: Diagnostic[]) => {
|
|
|
79
80
|
};
|
|
80
81
|
//#endregion
|
|
81
82
|
//#region ../core/src/report.d.ts
|
|
83
|
+
interface ReportFailure {
|
|
84
|
+
file: string;
|
|
85
|
+
message: string;
|
|
86
|
+
}
|
|
82
87
|
interface JsonReport {
|
|
83
88
|
diagnostics: {
|
|
84
89
|
category: RuleCategory;
|
|
@@ -90,13 +95,14 @@ interface JsonReport {
|
|
|
90
95
|
rule: string;
|
|
91
96
|
severity: "error" | "warning" | "info";
|
|
92
97
|
}[];
|
|
98
|
+
failures: ReportFailure[];
|
|
93
99
|
label: string;
|
|
94
100
|
project: ProjectInfo;
|
|
95
101
|
schemaVersion: number;
|
|
96
102
|
score: number;
|
|
97
103
|
timestamp: string;
|
|
98
104
|
}
|
|
99
|
-
export declare const toJsonReport: (diagnostics: Diagnostic[], score: number, label: string, project: ProjectInfo) => JsonReport;
|
|
105
|
+
export declare const toJsonReport: (diagnostics: Diagnostic[], score: number, label: string, project: ProjectInfo, failures?: ReportFailure[]) => JsonReport;
|
|
100
106
|
//#endregion
|
|
101
107
|
//#region src/index.d.ts
|
|
102
108
|
export declare const version: string;
|
package/dist/index.d.mts
CHANGED
|
@@ -48,6 +48,7 @@ interface DockerDoctorConfig {
|
|
|
48
48
|
//#region ../core/src/project-info/discover.d.ts
|
|
49
49
|
export declare const discoverProject: (rootDir: string, options?: {
|
|
50
50
|
ignoreFiles?: readonly string[];
|
|
51
|
+
onSkippedDirectory?: (relativeDir: string) => void;
|
|
51
52
|
}) => Promise<ProjectInfo>;
|
|
52
53
|
//#endregion
|
|
53
54
|
//#region ../core/src/parsers/dockerfile-parser.d.ts
|
|
@@ -79,6 +80,10 @@ export declare const calculateScore: (diagnostics: Diagnostic[]) => {
|
|
|
79
80
|
};
|
|
80
81
|
//#endregion
|
|
81
82
|
//#region ../core/src/report.d.ts
|
|
83
|
+
interface ReportFailure {
|
|
84
|
+
file: string;
|
|
85
|
+
message: string;
|
|
86
|
+
}
|
|
82
87
|
interface JsonReport {
|
|
83
88
|
diagnostics: {
|
|
84
89
|
category: RuleCategory;
|
|
@@ -90,13 +95,14 @@ interface JsonReport {
|
|
|
90
95
|
rule: string;
|
|
91
96
|
severity: "error" | "warning" | "info";
|
|
92
97
|
}[];
|
|
98
|
+
failures: ReportFailure[];
|
|
93
99
|
label: string;
|
|
94
100
|
project: ProjectInfo;
|
|
95
101
|
schemaVersion: number;
|
|
96
102
|
score: number;
|
|
97
103
|
timestamp: string;
|
|
98
104
|
}
|
|
99
|
-
export declare const toJsonReport: (diagnostics: Diagnostic[], score: number, label: string, project: ProjectInfo) => JsonReport;
|
|
105
|
+
export declare const toJsonReport: (diagnostics: Diagnostic[], score: number, label: string, project: ProjectInfo, failures?: ReportFailure[]) => JsonReport;
|
|
100
106
|
//#endregion
|
|
101
107
|
//#region src/index.d.ts
|
|
102
108
|
export declare const version: string;
|
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-BLeXr_hR.mjs";
|
|
3
3
|
|
|
4
4
|
//#region ../core/src/config/define-config.ts
|
|
5
5
|
const defineConfig = (config) => config;
|
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { LineCounter, isAlias, isMap, isScalar, isSeq, parse, parseDocument } from "yaml";
|
|
5
5
|
|
|
6
6
|
//#region package.json
|
|
7
|
-
var version = "0.
|
|
7
|
+
var version = "0.6.0";
|
|
8
8
|
|
|
9
9
|
//#endregion
|
|
10
10
|
//#region ../core/src/project-info/ignore.ts
|
|
@@ -60,20 +60,66 @@ const createIgnoreMatcher = (patterns) => {
|
|
|
60
60
|
|
|
61
61
|
//#endregion
|
|
62
62
|
//#region ../core/src/project-info/discover.ts
|
|
63
|
-
const
|
|
64
|
-
|
|
63
|
+
const PRUNED_DIRECTORIES = [
|
|
64
|
+
".git",
|
|
65
|
+
".hg",
|
|
66
|
+
".svn",
|
|
67
|
+
"node_modules",
|
|
68
|
+
"bower_components",
|
|
69
|
+
".pnpm-store",
|
|
70
|
+
".yarn",
|
|
71
|
+
".venv",
|
|
72
|
+
"venv",
|
|
73
|
+
"__pycache__",
|
|
74
|
+
".tox",
|
|
75
|
+
".mypy_cache",
|
|
76
|
+
".pytest_cache",
|
|
77
|
+
".ruff_cache",
|
|
78
|
+
".gradle",
|
|
79
|
+
".turbo",
|
|
80
|
+
".next",
|
|
81
|
+
".nuxt",
|
|
82
|
+
".svelte-kit",
|
|
83
|
+
".parcel-cache",
|
|
84
|
+
".cache",
|
|
85
|
+
"coverage",
|
|
86
|
+
"dist"
|
|
87
|
+
];
|
|
88
|
+
const PRUNED = new Set(PRUNED_DIRECTORIES);
|
|
89
|
+
const toPosixRelative = (rootDir, file) => path.relative(rootDir, file).split(path.sep).join("/");
|
|
90
|
+
const SUBTREE_SUFFIX = "/**";
|
|
91
|
+
const createPrunedDirectoryMatcher = (patterns = []) => createIgnoreMatcher(patterns.filter((pattern) => pattern.endsWith(SUBTREE_SUFFIX)).map((pattern) => pattern.slice(0, -3)));
|
|
92
|
+
const isPermissionError = (error) => error instanceof Error && "code" in error && (error.code === "EACCES" || error.code === "EPERM");
|
|
93
|
+
const walk = async (dir, options, fileList = []) => {
|
|
94
|
+
let files;
|
|
95
|
+
try {
|
|
96
|
+
files = await fs.readdir(dir, { withFileTypes: true });
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (dir !== options.rootDir && isPermissionError(error)) {
|
|
99
|
+
options.onSkippedDirectory?.(toPosixRelative(options.rootDir, dir));
|
|
100
|
+
return fileList;
|
|
101
|
+
}
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
65
104
|
await Promise.all(files.map(async (file) => {
|
|
66
105
|
const filePath = path.join(dir, file.name);
|
|
106
|
+
if (file.isSymbolicLink()) {
|
|
107
|
+
fileList.push(filePath);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
67
110
|
if (file.isDirectory()) {
|
|
68
|
-
if (
|
|
69
|
-
await walk(filePath, fileList);
|
|
111
|
+
if (PRUNED.has(file.name) || options.isPrunedByConfig(toPosixRelative(options.rootDir, filePath))) return;
|
|
112
|
+
await walk(filePath, options, fileList);
|
|
70
113
|
} else fileList.push(filePath);
|
|
71
114
|
}));
|
|
72
115
|
return fileList;
|
|
73
116
|
};
|
|
74
|
-
const toPosixRelative = (rootDir, file) => path.relative(rootDir, file).split(path.sep).join("/");
|
|
75
117
|
const discoverProject = async (rootDir, options) => {
|
|
76
|
-
const allFiles = await walk(rootDir
|
|
118
|
+
const allFiles = await walk(rootDir, {
|
|
119
|
+
isPrunedByConfig: createPrunedDirectoryMatcher(options?.ignoreFiles),
|
|
120
|
+
onSkippedDirectory: options?.onSkippedDirectory,
|
|
121
|
+
rootDir
|
|
122
|
+
});
|
|
77
123
|
const dockerfiles = [];
|
|
78
124
|
const composeFiles = [];
|
|
79
125
|
const dockerignores = [];
|
|
@@ -258,7 +304,7 @@ const parseDockerfile = (content, file = "Dockerfile") => {
|
|
|
258
304
|
const lineNum = i + 1;
|
|
259
305
|
const insideHeredoc = state.heredocQueue.length > 0;
|
|
260
306
|
if (!insideHeredoc && trimmed.startsWith("#")) continue;
|
|
261
|
-
if (!
|
|
307
|
+
if (!insideHeredoc && trimmed === "") continue;
|
|
262
308
|
state.rawAccumulator.push(rawLine);
|
|
263
309
|
if (insideHeredoc) processHeredocLine(state, trimmed);
|
|
264
310
|
else processInstructionLine(state, trimmed, lineNum);
|
|
@@ -429,6 +475,13 @@ const createDiagnostic = (file, ruleKey, severity, message, help, line) => ({
|
|
|
429
475
|
severity
|
|
430
476
|
});
|
|
431
477
|
|
|
478
|
+
//#endregion
|
|
479
|
+
//#region ../core/src/rules/package-managers.ts
|
|
480
|
+
const APT_GET_INSTALL_RE = /\bapt-get(?:\s+-\S+)*\s+install\b/u;
|
|
481
|
+
const APT_GET_UPDATE_RE = /\bapt-get(?:\s+-\S+)*\s+update\b/u;
|
|
482
|
+
const hasAptGetInstall = (text) => APT_GET_INSTALL_RE.test(text);
|
|
483
|
+
const hasAptGetUpdate = (text) => APT_GET_UPDATE_RE.test(text);
|
|
484
|
+
|
|
432
485
|
//#endregion
|
|
433
486
|
//#region ../core/src/rules/best-practices.ts
|
|
434
487
|
const requireHealthcheck = {
|
|
@@ -444,6 +497,16 @@ const requireHealthcheck = {
|
|
|
444
497
|
key: "docker-doctor/require-healthcheck",
|
|
445
498
|
message: "Add a HEALTHCHECK instruction"
|
|
446
499
|
};
|
|
500
|
+
const AUTO_EXTRACT_SUFFIXES = [
|
|
501
|
+
".tar",
|
|
502
|
+
".tar.gz",
|
|
503
|
+
".tgz",
|
|
504
|
+
".tar.bz2",
|
|
505
|
+
".tbz2",
|
|
506
|
+
".tbz",
|
|
507
|
+
".tar.xz",
|
|
508
|
+
".txz"
|
|
509
|
+
];
|
|
447
510
|
const preferCopyOverAdd = {
|
|
448
511
|
category: "Best Practices",
|
|
449
512
|
check(instructions, file) {
|
|
@@ -452,7 +515,7 @@ const preferCopyOverAdd = {
|
|
|
452
515
|
const src = inst.args.split(/\s+/u).find((p) => !p.startsWith("--"));
|
|
453
516
|
if (!src) continue;
|
|
454
517
|
const isRemote = src.startsWith("http://") || src.startsWith("https://");
|
|
455
|
-
const isArchive =
|
|
518
|
+
const isArchive = AUTO_EXTRACT_SUFFIXES.some((suffix) => src.toLowerCase().endsWith(suffix));
|
|
456
519
|
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));
|
|
457
520
|
}
|
|
458
521
|
return diagnostics;
|
|
@@ -490,8 +553,8 @@ const combineAptUpdateInstall = {
|
|
|
490
553
|
check(instructions, file) {
|
|
491
554
|
const diagnostics = [];
|
|
492
555
|
for (const inst of instructions) if (inst.instruction === "RUN") {
|
|
493
|
-
const hasUpdate = inst.args
|
|
494
|
-
const hasInstall = inst.args
|
|
556
|
+
const hasUpdate = hasAptGetUpdate(inst.args);
|
|
557
|
+
const hasInstall = hasAptGetInstall(inst.args);
|
|
495
558
|
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));
|
|
496
559
|
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));
|
|
497
560
|
}
|
|
@@ -571,18 +634,32 @@ const avoidRunCd = {
|
|
|
571
634
|
key: "docker-doctor/avoid-run-cd",
|
|
572
635
|
message: "Avoid changing directories with cd in RUN"
|
|
573
636
|
};
|
|
637
|
+
const PACKAGE_LIST_START_RE = /\b(?:apt-get(?:\s+-\S+)*\s+install|apk\s+add|yum\s+install|dnf\s+install)\b/u;
|
|
638
|
+
const SHELL_SEPARATOR_RE$1 = /[;|]|&&/u;
|
|
639
|
+
const WHITESPACE_RE$1 = /\s+/u;
|
|
640
|
+
const collectPackageList = (raw) => {
|
|
641
|
+
const start = PACKAGE_LIST_START_RE.exec(raw);
|
|
642
|
+
if (!start) return [];
|
|
643
|
+
const packages = [];
|
|
644
|
+
const tokens = raw.slice(start.index + start[0].length).split(WHITESPACE_RE$1);
|
|
645
|
+
for (const token of tokens) {
|
|
646
|
+
if (SHELL_SEPARATOR_RE$1.test(token)) break;
|
|
647
|
+
if (token !== "" && token !== "\\" && !token.startsWith("-")) packages.push(token);
|
|
648
|
+
}
|
|
649
|
+
return packages;
|
|
650
|
+
};
|
|
574
651
|
const sortMultilineArgs = {
|
|
575
652
|
category: "Best Practices",
|
|
576
653
|
check(instructions, file) {
|
|
577
654
|
const diagnostics = [];
|
|
578
655
|
for (const inst of instructions) if (inst.instruction === "RUN") {
|
|
579
656
|
const { raw } = inst;
|
|
580
|
-
const isPackageInstall = raw
|
|
657
|
+
const isPackageInstall = hasAptGetInstall(raw) || raw.includes("apk add") || raw.includes("yum install") || raw.includes("dnf install");
|
|
581
658
|
const hasContinuation = raw.includes("\\\n") || raw.includes("\\\r\n");
|
|
582
659
|
if (isPackageInstall && hasContinuation) {
|
|
583
|
-
const packages = raw
|
|
660
|
+
const packages = collectPackageList(raw);
|
|
584
661
|
if (packages.length > 1) {
|
|
585
|
-
const sorted = packages.toSorted(
|
|
662
|
+
const sorted = packages.toSorted();
|
|
586
663
|
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));
|
|
587
664
|
}
|
|
588
665
|
}
|
|
@@ -844,7 +921,10 @@ const composeModelRules = [undefinedModelReference, pinModelVersion];
|
|
|
844
921
|
const INTERPOLATION_WITH_DEFAULT = /\$\{[^}:?-]+:?-(?<fallback>[^}]*)\}/gu;
|
|
845
922
|
const INTERPOLATION_WITHOUT_DEFAULT = /\$\{[^}]*\}|\$[A-Za-z_][A-Za-z0-9_]*/gu;
|
|
846
923
|
const HOME_INTERPOLATION = /\$\{HOME\}|\$HOME(?![A-Za-z0-9_])/gu;
|
|
847
|
-
const
|
|
924
|
+
const PWD_INTERPOLATION = /\$\{PWD\}|\$PWD(?![A-Za-z0-9_])/gu;
|
|
925
|
+
const LEADING_INTERPOLATION_RE = /^\$(?:\{[^}]*\}|[A-Za-z_][A-Za-z0-9_]*)/u;
|
|
926
|
+
const applyFoldsAndDefaults = (value) => value.replace(HOME_INTERPOLATION, "~").replace(PWD_INTERPOLATION, ".").replace(INTERPOLATION_WITH_DEFAULT, (_match, fallback) => fallback);
|
|
927
|
+
const resolveInterpolationDefaults = (value) => applyFoldsAndDefaults(value).replace(INTERPOLATION_WITHOUT_DEFAULT, "");
|
|
848
928
|
const PATH_SHAPED = /^(?:[/.~]|[A-Za-z]:)/u;
|
|
849
929
|
const splitShortSyntax = (spec) => {
|
|
850
930
|
const parts = [];
|
|
@@ -895,12 +975,16 @@ const volumeMount = (volume) => {
|
|
|
895
975
|
* The host path a bind mount reads from, with interpolation defaults
|
|
896
976
|
* applied and Windows separators folded to `/`. Undefined when the entry is
|
|
897
977
|
* not a bind mount (named volume, tmpfs, anonymous volume) or when the
|
|
898
|
-
* source
|
|
978
|
+
* source starts with an interpolation that has no default. A source such as
|
|
979
|
+
* `${DATA_DIR}/pg` names no known host location, so no rule can say whether
|
|
980
|
+
* it is inside the project.
|
|
899
981
|
*/
|
|
900
982
|
const bindMountSource = (mount) => {
|
|
901
983
|
if (mount.type !== void 0 && mount.type !== "bind") return;
|
|
902
984
|
if (!mount.source) return;
|
|
903
|
-
const
|
|
985
|
+
const withDefaults = applyFoldsAndDefaults(mount.source);
|
|
986
|
+
if (LEADING_INTERPOLATION_RE.test(withDefaults)) return;
|
|
987
|
+
const resolved = withDefaults.replace(INTERPOLATION_WITHOUT_DEFAULT, "").replaceAll("\\", "/");
|
|
904
988
|
if (resolved === "" || !PATH_SHAPED.test(resolved)) return;
|
|
905
989
|
return resolved;
|
|
906
990
|
};
|
|
@@ -908,15 +992,16 @@ const isDockerSocketPath = (hostPath) => hostPath.endsWith("/docker.sock") || ho
|
|
|
908
992
|
const DOCKER_SOCKET_TARGET = "/var/run/docker.sock";
|
|
909
993
|
/**
|
|
910
994
|
* Whether the entry bind-mounts the Docker socket, under any of its host
|
|
911
|
-
* spellings. A
|
|
912
|
-
* still
|
|
995
|
+
* spellings. A source that starts with `${VAR}` names no known host path,
|
|
996
|
+
* but a `/docker.sock` suffix or a socket target still identifies it.
|
|
913
997
|
*/
|
|
914
998
|
const mountsDockerSocket = (mount) => {
|
|
915
999
|
const source = bindMountSource(mount);
|
|
916
1000
|
if (source !== void 0) return isDockerSocketPath(source);
|
|
917
1001
|
if (mount.type !== void 0 && mount.type !== "bind") return false;
|
|
918
1002
|
if (!mount.source) return false;
|
|
919
|
-
|
|
1003
|
+
const remainder = resolveInterpolationDefaults(mount.source);
|
|
1004
|
+
return isDockerSocketPath(remainder) || remainder === "" && resolveInterpolationDefaults(mount.target ?? "") === DOCKER_SOCKET_TARGET;
|
|
920
1005
|
};
|
|
921
1006
|
const WINDOWS_DRIVE_ROOT = /^[A-Za-z]:\/?$/u;
|
|
922
1007
|
const WINDOWS_DRIVE_PREFIX = /^[A-Za-z]:/u;
|
|
@@ -966,7 +1051,11 @@ const SECRET_KEY_PATTERNS = [
|
|
|
966
1051
|
];
|
|
967
1052
|
const isSecretKey = (key) => SECRET_KEY_PATTERNS.some((regex) => regex.test(key));
|
|
968
1053
|
const CREDENTIAL_FREE_URL = /^[a-z][a-z0-9+.-]*:\/\/[^@/\s]*(?:\/|$)/iu;
|
|
969
|
-
const
|
|
1054
|
+
const SURROUNDING_QUOTES_RE = /^(?<quote>["'])(?<inner>.*)\k<quote>$/su;
|
|
1055
|
+
const isLiteralSecretValue = (value) => {
|
|
1056
|
+
const unquoted = value.match(SURROUNDING_QUOTES_RE)?.groups?.inner ?? value;
|
|
1057
|
+
return unquoted.length > 0 && !unquoted.startsWith("$") && !CREDENTIAL_FREE_URL.test(unquoted);
|
|
1058
|
+
};
|
|
970
1059
|
|
|
971
1060
|
//#endregion
|
|
972
1061
|
//#region ../core/src/rules/compose-security.ts
|
|
@@ -1073,6 +1162,10 @@ const preferReadOnlyBindMount = {
|
|
|
1073
1162
|
key: "docker-doctor/prefer-read-only-bind-mount",
|
|
1074
1163
|
message: "Bind mounts outside the project should be read-only"
|
|
1075
1164
|
};
|
|
1165
|
+
const literalValue = (value) => {
|
|
1166
|
+
if (typeof value === "string") return value;
|
|
1167
|
+
if (typeof value === "number") return String(value);
|
|
1168
|
+
};
|
|
1076
1169
|
const noPlaintextSecrets = {
|
|
1077
1170
|
category: "Compose",
|
|
1078
1171
|
check(composeContent, file, context) {
|
|
@@ -1088,15 +1181,16 @@ const noPlaintextSecrets = {
|
|
|
1088
1181
|
if (eqIndex <= 0) continue;
|
|
1089
1182
|
const key = entry.slice(0, eqIndex);
|
|
1090
1183
|
const value = entry.slice(eqIndex + 1);
|
|
1091
|
-
if (isSecretKey(key) &&
|
|
1184
|
+
if (isSecretKey(key) && isLiteralSecretValue(value)) flag(name, key, context?.locate?.([
|
|
1092
1185
|
"services",
|
|
1093
1186
|
name,
|
|
1094
1187
|
"environment",
|
|
1095
1188
|
index
|
|
1096
1189
|
]));
|
|
1097
1190
|
}
|
|
1098
|
-
else if (environment && typeof environment === "object") {
|
|
1099
|
-
|
|
1191
|
+
else if (environment && typeof environment === "object") for (const [key, value] of Object.entries(environment)) {
|
|
1192
|
+
const literal = literalValue(value);
|
|
1193
|
+
if (literal !== void 0 && isSecretKey(key) && isLiteralSecretValue(literal)) flag(name, key, context?.locate?.([
|
|
1100
1194
|
"services",
|
|
1101
1195
|
name,
|
|
1102
1196
|
"environment",
|
|
@@ -1156,7 +1250,7 @@ const cleanPackageCache = {
|
|
|
1156
1250
|
const diagnostics = [];
|
|
1157
1251
|
for (const inst of instructions) if (inst.instruction === "RUN") {
|
|
1158
1252
|
const { args } = inst;
|
|
1159
|
-
if (args
|
|
1253
|
+
if (hasAptGetInstall(args) && !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));
|
|
1160
1254
|
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));
|
|
1161
1255
|
}
|
|
1162
1256
|
return diagnostics;
|
|
@@ -1166,7 +1260,40 @@ const cleanPackageCache = {
|
|
|
1166
1260
|
key: "docker-doctor/clean-package-cache",
|
|
1167
1261
|
message: "Clean up package manager cache in the same RUN layer"
|
|
1168
1262
|
};
|
|
1169
|
-
const
|
|
1263
|
+
const SHELL_SEPARATOR_RE = /\s*(?:&&|\|\||;|\|)\s*/u;
|
|
1264
|
+
const WHITESPACE_RE = /\s+/u;
|
|
1265
|
+
const ENV_ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*=/u;
|
|
1266
|
+
const NODE_PACKAGE_MANAGERS = /* @__PURE__ */ new Set([
|
|
1267
|
+
"npm",
|
|
1268
|
+
"yarn",
|
|
1269
|
+
"pnpm",
|
|
1270
|
+
"bun"
|
|
1271
|
+
]);
|
|
1272
|
+
const INSTALL_VERBS = /* @__PURE__ */ new Set([
|
|
1273
|
+
"install",
|
|
1274
|
+
"ci",
|
|
1275
|
+
"i"
|
|
1276
|
+
]);
|
|
1277
|
+
const NON_DEV_INSTALL_FLAGS = /* @__PURE__ */ new Set([
|
|
1278
|
+
"-g",
|
|
1279
|
+
"--global",
|
|
1280
|
+
"--production",
|
|
1281
|
+
"--omit=dev",
|
|
1282
|
+
"--only=prod",
|
|
1283
|
+
"--only=production",
|
|
1284
|
+
"--prod",
|
|
1285
|
+
"-P",
|
|
1286
|
+
"-p"
|
|
1287
|
+
]);
|
|
1288
|
+
const segmentInstallsDevDependencies = (segment) => {
|
|
1289
|
+
const tokens = segment.split(WHITESPACE_RE).filter(Boolean);
|
|
1290
|
+
const commandStart = tokens.findIndex((token) => !ENV_ASSIGNMENT_RE.test(token));
|
|
1291
|
+
if (commandStart === -1) return false;
|
|
1292
|
+
const [manager, verb, ...options] = tokens.slice(commandStart);
|
|
1293
|
+
if (!(NODE_PACKAGE_MANAGERS.has(manager) && INSTALL_VERBS.has(verb))) return false;
|
|
1294
|
+
return !tokens.slice(0, commandStart).includes("NODE_ENV=production") && options.every((option) => option.startsWith("-") && !NON_DEV_INSTALL_FLAGS.has(option));
|
|
1295
|
+
};
|
|
1296
|
+
const installsDevDependencies = (args) => !args.includes("prune") && maskQuotedText(args).split(SHELL_SEPARATOR_RE).some(segmentInstallsDevDependencies);
|
|
1170
1297
|
const avoidDevDependencies = {
|
|
1171
1298
|
category: "Image Size",
|
|
1172
1299
|
check(instructions, file) {
|
|
@@ -1212,11 +1339,12 @@ const imageSizeRules = [
|
|
|
1212
1339
|
|
|
1213
1340
|
//#endregion
|
|
1214
1341
|
//#region ../core/src/rules/performance.ts
|
|
1342
|
+
const BUILD_TOOL_COMMAND_RE = /(?:^|&&|\|\||;|\||\(|`|\$\()\s*(?:make|cmake)(?=\s|$)/u;
|
|
1215
1343
|
const useMultiStage = {
|
|
1216
1344
|
category: "Performance",
|
|
1217
1345
|
check(instructions, file) {
|
|
1218
1346
|
if (instructions.filter((inst) => inst.instruction === "FROM").length === 1) {
|
|
1219
|
-
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
|
|
1347
|
+
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)];
|
|
1220
1348
|
}
|
|
1221
1349
|
return [];
|
|
1222
1350
|
},
|
|
@@ -1275,7 +1403,8 @@ const hasDockerignoreFor = (dockerfilePath, projectFiles) => {
|
|
|
1275
1403
|
const lastSlash = dockerfilePath.lastIndexOf("/");
|
|
1276
1404
|
const dir = lastSlash === -1 ? "" : dockerfilePath.slice(0, lastSlash);
|
|
1277
1405
|
const adjacent = dir === "" ? ".dockerignore" : `${dir}/.dockerignore`;
|
|
1278
|
-
|
|
1406
|
+
const perDockerfile = `${dockerfilePath}.dockerignore`;
|
|
1407
|
+
return projectFiles.some((f) => f === adjacent || f === perDockerfile || f === ".dockerignore");
|
|
1279
1408
|
};
|
|
1280
1409
|
const useDockerignore = {
|
|
1281
1410
|
category: "Performance",
|
|
@@ -1687,13 +1816,13 @@ const calculateScore = (diagnostics) => {
|
|
|
1687
1816
|
|
|
1688
1817
|
//#endregion
|
|
1689
1818
|
//#region ../core/src/report.ts
|
|
1690
|
-
const REPORT_SCHEMA_VERSION =
|
|
1819
|
+
const REPORT_SCHEMA_VERSION = 4;
|
|
1691
1820
|
const resolveCategory = (diagnostic) => {
|
|
1692
1821
|
const category = diagnostic.category ?? findRule(diagnostic.rule)?.category;
|
|
1693
1822
|
if (!category) throw new Error(`Diagnostic for unknown rule "${diagnostic.rule}" has no category`);
|
|
1694
1823
|
return category;
|
|
1695
1824
|
};
|
|
1696
|
-
const toJsonReport = (diagnostics, score, label, project) => ({
|
|
1825
|
+
const toJsonReport = (diagnostics, score, label, project, failures = []) => ({
|
|
1697
1826
|
diagnostics: diagnostics.map((d) => ({
|
|
1698
1827
|
category: resolveCategory(d),
|
|
1699
1828
|
column: d.column,
|
|
@@ -1704,13 +1833,17 @@ const toJsonReport = (diagnostics, score, label, project) => ({
|
|
|
1704
1833
|
rule: d.rule,
|
|
1705
1834
|
severity: d.severity
|
|
1706
1835
|
})),
|
|
1836
|
+
failures: failures.map(({ file, message }) => ({
|
|
1837
|
+
file,
|
|
1838
|
+
message
|
|
1839
|
+
})),
|
|
1707
1840
|
label,
|
|
1708
1841
|
project,
|
|
1709
|
-
schemaVersion:
|
|
1842
|
+
schemaVersion: 4,
|
|
1710
1843
|
score,
|
|
1711
1844
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1712
1845
|
});
|
|
1713
1846
|
|
|
1714
1847
|
//#endregion
|
|
1715
1848
|
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 };
|
|
1716
|
-
//# sourceMappingURL=src-
|
|
1849
|
+
//# sourceMappingURL=src-BLeXr_hR.mjs.map
|