@docker-doctor/cli 0.5.0 → 0.5.2
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 +37 -15
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +37 -15
- package/dist/cli.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +15 -13
- package/dist/index.d.mts +15 -13
- package/dist/index.mjs +1 -1
- package/dist/{src-CpGd43y0.mjs → src-BOGzkp17.mjs} +217 -58
- package/dist/src-BOGzkp17.mjs.map +1 -0
- package/dist/{src-F90_EKmA.cjs → src-D3jzc3GX.cjs} +217 -58
- package/dist/src-D3jzc3GX.cjs.map +1 -0
- package/package.json +4 -4
- package/skill/docker-doctor/references/explain.md +6 -5
- package/dist/src-CpGd43y0.mjs.map +0 -1
- package/dist/src-F90_EKmA.cjs.map +0 -1
package/dist/index.d.cts
CHANGED
|
@@ -13,6 +13,7 @@ interface Diagnostic {
|
|
|
13
13
|
severity: DiagnosticSeverity;
|
|
14
14
|
message: string;
|
|
15
15
|
help: string;
|
|
16
|
+
category?: RuleCategory;
|
|
16
17
|
line?: number;
|
|
17
18
|
column?: number;
|
|
18
19
|
}
|
|
@@ -45,34 +46,34 @@ interface DockerDoctorConfig {
|
|
|
45
46
|
}
|
|
46
47
|
//#endregion
|
|
47
48
|
//#region ../core/src/project-info/discover.d.ts
|
|
48
|
-
declare const discoverProject: (rootDir: string, options?: {
|
|
49
|
+
export declare const discoverProject: (rootDir: string, options?: {
|
|
49
50
|
ignoreFiles?: readonly string[];
|
|
50
51
|
}) => Promise<ProjectInfo>;
|
|
51
52
|
//#endregion
|
|
52
53
|
//#region ../core/src/parsers/dockerfile-parser.d.ts
|
|
53
|
-
declare const parseDockerfile: (content: string) => DockerfileInstruction[];
|
|
54
|
+
export declare const parseDockerfile: (content: string, file?: string) => DockerfileInstruction[];
|
|
54
55
|
//#endregion
|
|
55
56
|
//#region ../core/src/parsers/compose-parser.d.ts
|
|
56
|
-
declare const parseCompose: (content: string, filepath: string) => unknown;
|
|
57
|
+
export declare const parseCompose: (content: string, filepath: string) => unknown;
|
|
57
58
|
//#endregion
|
|
58
59
|
//#region ../core/src/runners/dockerfile-runner.d.ts
|
|
59
|
-
declare const runDockerfileRules: (instructions: DockerfileInstruction[], file: string, projectFiles: string[], rulesConfig?: Record<string, RuleSeverity>, categoriesConfig?: Record<string, RuleSeverity>) => Diagnostic[];
|
|
60
|
+
export declare const runDockerfileRules: (instructions: DockerfileInstruction[], file: string, projectFiles: string[], rulesConfig?: Record<string, RuleSeverity>, categoriesConfig?: Record<string, RuleSeverity>) => Diagnostic[];
|
|
60
61
|
//#endregion
|
|
61
62
|
//#region ../core/src/runners/compose-runner.d.ts
|
|
62
|
-
declare const runComposeRules: (composeContent: unknown, file: string, rulesConfig?: Record<string, RuleSeverity>, categoriesConfig?: Record<string, RuleSeverity>, locate?: ComposeLocator) => Diagnostic[];
|
|
63
|
+
export declare const runComposeRules: (composeContent: unknown, file: string, rulesConfig?: Record<string, RuleSeverity>, categoriesConfig?: Record<string, RuleSeverity>, locate?: ComposeLocator) => Diagnostic[];
|
|
63
64
|
//#endregion
|
|
64
65
|
//#region ../core/src/rules/index.d.ts
|
|
65
|
-
declare const allRules: RuleDefinition[];
|
|
66
|
-
declare const findRule: (key: string) => RuleDefinition | undefined;
|
|
66
|
+
export declare const allRules: RuleDefinition[];
|
|
67
|
+
export declare const findRule: (key: string) => RuleDefinition | undefined;
|
|
67
68
|
//#endregion
|
|
68
69
|
//#region ../core/src/config/define-config.d.ts
|
|
69
|
-
declare const defineConfig: (config: DockerDoctorConfig) => DockerDoctorConfig;
|
|
70
|
+
export declare const defineConfig: (config: DockerDoctorConfig) => DockerDoctorConfig;
|
|
70
71
|
//#endregion
|
|
71
72
|
//#region ../core/src/config/loader.d.ts
|
|
72
|
-
declare const loadConfig: (rootDir: string, customPath?: string, onWarning?: (message: string) => void) => Promise<DockerDoctorConfig>;
|
|
73
|
+
export declare const loadConfig: (rootDir: string, customPath?: string, onWarning?: (message: string) => void) => Promise<DockerDoctorConfig>;
|
|
73
74
|
//#endregion
|
|
74
75
|
//#region ../core/src/scoring.d.ts
|
|
75
|
-
declare const calculateScore: (diagnostics: Diagnostic[]) => {
|
|
76
|
+
export declare const calculateScore: (diagnostics: Diagnostic[]) => {
|
|
76
77
|
score: number;
|
|
77
78
|
label: string;
|
|
78
79
|
};
|
|
@@ -80,6 +81,7 @@ declare const calculateScore: (diagnostics: Diagnostic[]) => {
|
|
|
80
81
|
//#region ../core/src/report.d.ts
|
|
81
82
|
interface JsonReport {
|
|
82
83
|
diagnostics: {
|
|
84
|
+
category: RuleCategory;
|
|
83
85
|
column?: number;
|
|
84
86
|
file: string;
|
|
85
87
|
help: string;
|
|
@@ -94,10 +96,10 @@ interface JsonReport {
|
|
|
94
96
|
score: number;
|
|
95
97
|
timestamp: string;
|
|
96
98
|
}
|
|
97
|
-
declare const toJsonReport: (diagnostics: Diagnostic[], score: number, label: string, project: ProjectInfo) => JsonReport;
|
|
99
|
+
export declare const toJsonReport: (diagnostics: Diagnostic[], score: number, label: string, project: ProjectInfo) => JsonReport;
|
|
98
100
|
//#endregion
|
|
99
101
|
//#region src/index.d.ts
|
|
100
|
-
declare const version: string;
|
|
102
|
+
export declare const version: string;
|
|
101
103
|
//#endregion
|
|
102
|
-
export {
|
|
104
|
+
export type { Diagnostic, DockerDoctorConfig, RuleCategory, RuleSeverity };
|
|
103
105
|
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.mts
CHANGED
|
@@ -13,6 +13,7 @@ interface Diagnostic {
|
|
|
13
13
|
severity: DiagnosticSeverity;
|
|
14
14
|
message: string;
|
|
15
15
|
help: string;
|
|
16
|
+
category?: RuleCategory;
|
|
16
17
|
line?: number;
|
|
17
18
|
column?: number;
|
|
18
19
|
}
|
|
@@ -45,34 +46,34 @@ interface DockerDoctorConfig {
|
|
|
45
46
|
}
|
|
46
47
|
//#endregion
|
|
47
48
|
//#region ../core/src/project-info/discover.d.ts
|
|
48
|
-
declare const discoverProject: (rootDir: string, options?: {
|
|
49
|
+
export declare const discoverProject: (rootDir: string, options?: {
|
|
49
50
|
ignoreFiles?: readonly string[];
|
|
50
51
|
}) => Promise<ProjectInfo>;
|
|
51
52
|
//#endregion
|
|
52
53
|
//#region ../core/src/parsers/dockerfile-parser.d.ts
|
|
53
|
-
declare const parseDockerfile: (content: string) => DockerfileInstruction[];
|
|
54
|
+
export declare const parseDockerfile: (content: string, file?: string) => DockerfileInstruction[];
|
|
54
55
|
//#endregion
|
|
55
56
|
//#region ../core/src/parsers/compose-parser.d.ts
|
|
56
|
-
declare const parseCompose: (content: string, filepath: string) => unknown;
|
|
57
|
+
export declare const parseCompose: (content: string, filepath: string) => unknown;
|
|
57
58
|
//#endregion
|
|
58
59
|
//#region ../core/src/runners/dockerfile-runner.d.ts
|
|
59
|
-
declare const runDockerfileRules: (instructions: DockerfileInstruction[], file: string, projectFiles: string[], rulesConfig?: Record<string, RuleSeverity>, categoriesConfig?: Record<string, RuleSeverity>) => Diagnostic[];
|
|
60
|
+
export declare const runDockerfileRules: (instructions: DockerfileInstruction[], file: string, projectFiles: string[], rulesConfig?: Record<string, RuleSeverity>, categoriesConfig?: Record<string, RuleSeverity>) => Diagnostic[];
|
|
60
61
|
//#endregion
|
|
61
62
|
//#region ../core/src/runners/compose-runner.d.ts
|
|
62
|
-
declare const runComposeRules: (composeContent: unknown, file: string, rulesConfig?: Record<string, RuleSeverity>, categoriesConfig?: Record<string, RuleSeverity>, locate?: ComposeLocator) => Diagnostic[];
|
|
63
|
+
export declare const runComposeRules: (composeContent: unknown, file: string, rulesConfig?: Record<string, RuleSeverity>, categoriesConfig?: Record<string, RuleSeverity>, locate?: ComposeLocator) => Diagnostic[];
|
|
63
64
|
//#endregion
|
|
64
65
|
//#region ../core/src/rules/index.d.ts
|
|
65
|
-
declare const allRules: RuleDefinition[];
|
|
66
|
-
declare const findRule: (key: string) => RuleDefinition | undefined;
|
|
66
|
+
export declare const allRules: RuleDefinition[];
|
|
67
|
+
export declare const findRule: (key: string) => RuleDefinition | undefined;
|
|
67
68
|
//#endregion
|
|
68
69
|
//#region ../core/src/config/define-config.d.ts
|
|
69
|
-
declare const defineConfig: (config: DockerDoctorConfig) => DockerDoctorConfig;
|
|
70
|
+
export declare const defineConfig: (config: DockerDoctorConfig) => DockerDoctorConfig;
|
|
70
71
|
//#endregion
|
|
71
72
|
//#region ../core/src/config/loader.d.ts
|
|
72
|
-
declare const loadConfig: (rootDir: string, customPath?: string, onWarning?: (message: string) => void) => Promise<DockerDoctorConfig>;
|
|
73
|
+
export declare const loadConfig: (rootDir: string, customPath?: string, onWarning?: (message: string) => void) => Promise<DockerDoctorConfig>;
|
|
73
74
|
//#endregion
|
|
74
75
|
//#region ../core/src/scoring.d.ts
|
|
75
|
-
declare const calculateScore: (diagnostics: Diagnostic[]) => {
|
|
76
|
+
export declare const calculateScore: (diagnostics: Diagnostic[]) => {
|
|
76
77
|
score: number;
|
|
77
78
|
label: string;
|
|
78
79
|
};
|
|
@@ -80,6 +81,7 @@ declare const calculateScore: (diagnostics: Diagnostic[]) => {
|
|
|
80
81
|
//#region ../core/src/report.d.ts
|
|
81
82
|
interface JsonReport {
|
|
82
83
|
diagnostics: {
|
|
84
|
+
category: RuleCategory;
|
|
83
85
|
column?: number;
|
|
84
86
|
file: string;
|
|
85
87
|
help: string;
|
|
@@ -94,10 +96,10 @@ interface JsonReport {
|
|
|
94
96
|
score: number;
|
|
95
97
|
timestamp: string;
|
|
96
98
|
}
|
|
97
|
-
declare const toJsonReport: (diagnostics: Diagnostic[], score: number, label: string, project: ProjectInfo) => JsonReport;
|
|
99
|
+
export declare const toJsonReport: (diagnostics: Diagnostic[], score: number, label: string, project: ProjectInfo) => JsonReport;
|
|
98
100
|
//#endregion
|
|
99
101
|
//#region src/index.d.ts
|
|
100
|
-
declare const version: string;
|
|
102
|
+
export declare const version: string;
|
|
101
103
|
//#endregion
|
|
102
|
-
export {
|
|
104
|
+
export type { Diagnostic, DockerDoctorConfig, RuleCategory, RuleSeverity };
|
|
103
105
|
//# sourceMappingURL=index.d.mts.map
|
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-BOGzkp17.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.5.
|
|
7
|
+
var version = "0.5.2";
|
|
8
8
|
|
|
9
9
|
//#endregion
|
|
10
10
|
//#region ../core/src/project-info/ignore.ts
|
|
@@ -71,6 +71,7 @@ const walk = async (dir, fileList = []) => {
|
|
|
71
71
|
}));
|
|
72
72
|
return fileList;
|
|
73
73
|
};
|
|
74
|
+
const toPosixRelative = (rootDir, file) => path.relative(rootDir, file).split(path.sep).join("/");
|
|
74
75
|
const discoverProject = async (rootDir, options) => {
|
|
75
76
|
const allFiles = await walk(rootDir);
|
|
76
77
|
const dockerfiles = [];
|
|
@@ -78,10 +79,13 @@ const discoverProject = async (rootDir, options) => {
|
|
|
78
79
|
const dockerignores = [];
|
|
79
80
|
const isIgnored = createIgnoreMatcher(options?.ignoreFiles);
|
|
80
81
|
for (const file of allFiles) {
|
|
81
|
-
const relative =
|
|
82
|
+
const relative = toPosixRelative(rootDir, file);
|
|
82
83
|
if (isIgnored(relative)) continue;
|
|
83
84
|
const base = path.basename(file).toLowerCase();
|
|
84
|
-
if (base
|
|
85
|
+
if (base.endsWith(".dockerignore")) {
|
|
86
|
+
dockerignores.push(relative);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
85
89
|
if (base === "dockerfile" || base.startsWith("dockerfile.") || base.endsWith(".dockerfile")) dockerfiles.push(relative);
|
|
86
90
|
if (base === "docker-compose.yml" || base === "docker-compose.yaml" || base === "compose.yml" || base === "compose.yaml" || (base.startsWith("docker-compose.") || base.startsWith("compose.")) && (base.endsWith(".yml") || base.endsWith(".yaml"))) composeFiles.push(relative);
|
|
87
91
|
}
|
|
@@ -92,6 +96,72 @@ const discoverProject = async (rootDir, options) => {
|
|
|
92
96
|
};
|
|
93
97
|
};
|
|
94
98
|
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region ../core/src/errors/config-error.ts
|
|
101
|
+
var ConfigError = class extends Error {
|
|
102
|
+
_tag = "ConfigError";
|
|
103
|
+
constructor(options) {
|
|
104
|
+
super(options.message);
|
|
105
|
+
this.name = "ConfigError";
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region ../core/src/errors/parse-error.ts
|
|
111
|
+
var ParseError = class extends Error {
|
|
112
|
+
_tag = "ParseError";
|
|
113
|
+
file;
|
|
114
|
+
constructor(options) {
|
|
115
|
+
super(options.message);
|
|
116
|
+
this.name = "ParseError";
|
|
117
|
+
this.file = options.file;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region ../core/src/parsers/shell-quotes.ts
|
|
123
|
+
/**
|
|
124
|
+
* Replaces every character inside a single- or double-quoted region with a
|
|
125
|
+
* space, keeping the quotes themselves and the string length, so callers can
|
|
126
|
+
* run position-sensitive regexes over shell text without matching inside
|
|
127
|
+
* string literals. A backslash inside double quotes escapes the next
|
|
128
|
+
* character; single quotes have no escapes (POSIX sh). An unclosed quote
|
|
129
|
+
* masks to the end of the text.
|
|
130
|
+
*
|
|
131
|
+
* What this deliberately does NOT do: it is not a tokenizer. It performs no
|
|
132
|
+
* variable or command expansion, knows nothing about subshells, backslash
|
|
133
|
+
* line continuations, `$'...'` or `<<` heredoc bodies, and does not split
|
|
134
|
+
* words. It only blanks quoted spans so regexes keep their positions.
|
|
135
|
+
*/
|
|
136
|
+
const maskQuotedText = (text) => {
|
|
137
|
+
let out = "";
|
|
138
|
+
let quote = null;
|
|
139
|
+
let index = 0;
|
|
140
|
+
while (index < text.length) {
|
|
141
|
+
const char = text[index];
|
|
142
|
+
if (quote === null) {
|
|
143
|
+
if (char === "\"" || char === "'") quote = char;
|
|
144
|
+
out += char;
|
|
145
|
+
index += 1;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (char === quote) {
|
|
149
|
+
quote = null;
|
|
150
|
+
out += char;
|
|
151
|
+
index += 1;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (quote === "\"" && char === "\\" && index + 1 < text.length) {
|
|
155
|
+
out += " ";
|
|
156
|
+
index += 2;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
out += " ";
|
|
160
|
+
index += 1;
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
};
|
|
164
|
+
|
|
95
165
|
//#endregion
|
|
96
166
|
//#region ../core/src/parsers/dockerfile-parser.ts
|
|
97
167
|
const DOCKERFILE_KEYWORDS = /* @__PURE__ */ new Set([
|
|
@@ -154,7 +224,10 @@ const matchInstructionKeyword = (lineContent) => {
|
|
|
154
224
|
};
|
|
155
225
|
return null;
|
|
156
226
|
};
|
|
157
|
-
const findHeredocDelimiters = (lineContent) =>
|
|
227
|
+
const findHeredocDelimiters = (lineContent) => {
|
|
228
|
+
const masked = maskQuotedText(lineContent);
|
|
229
|
+
return [...lineContent.matchAll(HEREDOC_OPENER_RE)].filter((match) => masked[match.index] === "<").map((match) => match.groups?.delim ?? "");
|
|
230
|
+
};
|
|
158
231
|
const processHeredocLine = (state, trimmed) => {
|
|
159
232
|
if (trimmed === state.heredocQueue[0]) state.heredocQueue.shift();
|
|
160
233
|
else state.currentArgs += (state.currentArgs ? " " : "") + trimmed;
|
|
@@ -177,7 +250,7 @@ const processInstructionLine = (state, trimmed, lineNum) => {
|
|
|
177
250
|
if (state.heredocQueue.length > 0) return;
|
|
178
251
|
if (!hasContinuation) closeInstruction(state);
|
|
179
252
|
};
|
|
180
|
-
const parseDockerfile = (content) => {
|
|
253
|
+
const parseDockerfile = (content, file = "Dockerfile") => {
|
|
181
254
|
const state = createParserState();
|
|
182
255
|
const lines = content.split(/\r?\n/u);
|
|
183
256
|
for (const [i, rawLine] of lines.entries()) {
|
|
@@ -190,32 +263,14 @@ const parseDockerfile = (content) => {
|
|
|
190
263
|
if (insideHeredoc) processHeredocLine(state, trimmed);
|
|
191
264
|
else processInstructionLine(state, trimmed, lineNum);
|
|
192
265
|
}
|
|
266
|
+
if (state.heredocQueue.length > 0) throw new ParseError({
|
|
267
|
+
file,
|
|
268
|
+
message: `Unterminated heredoc: delimiter "${state.heredocQueue[0]}" (opened on line ${state.startLine}) never closed. BuildKit rejects this Dockerfile too.`
|
|
269
|
+
});
|
|
193
270
|
closeInstruction(state);
|
|
194
271
|
return state.instructions;
|
|
195
272
|
};
|
|
196
273
|
|
|
197
|
-
//#endregion
|
|
198
|
-
//#region ../core/src/errors/config-error.ts
|
|
199
|
-
var ConfigError = class extends Error {
|
|
200
|
-
_tag = "ConfigError";
|
|
201
|
-
constructor(options) {
|
|
202
|
-
super(options.message);
|
|
203
|
-
this.name = "ConfigError";
|
|
204
|
-
}
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
//#endregion
|
|
208
|
-
//#region ../core/src/errors/parse-error.ts
|
|
209
|
-
var ParseError = class extends Error {
|
|
210
|
-
_tag = "ParseError";
|
|
211
|
-
file;
|
|
212
|
-
constructor(options) {
|
|
213
|
-
super(options.message);
|
|
214
|
-
this.name = "ParseError";
|
|
215
|
-
this.file = options.file;
|
|
216
|
-
}
|
|
217
|
-
};
|
|
218
|
-
|
|
219
274
|
//#endregion
|
|
220
275
|
//#region ../core/src/parsers/compose-parser.ts
|
|
221
276
|
const parseCompose = (content, filepath) => {
|
|
@@ -449,6 +504,7 @@ const combineAptUpdateInstall = {
|
|
|
449
504
|
};
|
|
450
505
|
const PIPEFAIL_SETTING_RE = /(?:^|\s)-[A-Za-z]*o\s+pipefail\b/u;
|
|
451
506
|
const HAS_PIPE_RE = /(?<!\|)\|(?!\|)/u;
|
|
507
|
+
const CD_COMMAND_RE = /(?:^|&&|\|\||;|\||\(|`|\$\()\s*cd(?=\s|$)/u;
|
|
452
508
|
const shellDirectiveEnablesPipefail = (args) => {
|
|
453
509
|
const argv = parseExecForm(args);
|
|
454
510
|
return argv !== null && PIPEFAIL_SETTING_RE.test(argv.join(" "));
|
|
@@ -476,8 +532,9 @@ const usePipefail = {
|
|
|
476
532
|
}
|
|
477
533
|
if (inst.instruction !== "RUN") continue;
|
|
478
534
|
const { args } = inst;
|
|
479
|
-
if (!HAS_PIPE_RE.test(args)) continue;
|
|
480
535
|
const execArgv = parseExecForm(args);
|
|
536
|
+
const pipeText = execArgv === null ? maskQuotedText(args) : execArgv.join(" ");
|
|
537
|
+
if (!HAS_PIPE_RE.test(pipeText)) continue;
|
|
481
538
|
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));
|
|
482
539
|
}
|
|
483
540
|
return diagnostics;
|
|
@@ -506,7 +563,7 @@ const avoidRunCd = {
|
|
|
506
563
|
category: "Best Practices",
|
|
507
564
|
check(instructions, file) {
|
|
508
565
|
const diagnostics = [];
|
|
509
|
-
for (const inst of instructions) if (inst.instruction === "RUN" &&
|
|
566
|
+
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));
|
|
510
567
|
return diagnostics;
|
|
511
568
|
},
|
|
512
569
|
defaultSeverity: "info",
|
|
@@ -569,12 +626,13 @@ const bestPracticesRules = [
|
|
|
569
626
|
* with a null body (`web:` with nothing under it) is returned as an empty
|
|
570
627
|
* config so rules still check it: it is the least-configured service in
|
|
571
628
|
* the file, not a service to skip. Scalar and array bodies are invalid
|
|
572
|
-
* compose and are dropped.
|
|
629
|
+
* compose and are dropped. A `services:` written as a YAML sequence is
|
|
630
|
+
* invalid Compose and yields no services.
|
|
573
631
|
*/
|
|
574
632
|
const composeServices = (composeContent) => {
|
|
575
633
|
if (!composeContent || typeof composeContent !== "object" || !("services" in composeContent)) return [];
|
|
576
634
|
const { services } = composeContent;
|
|
577
|
-
if (!services || typeof services !== "object") return [];
|
|
635
|
+
if (!services || typeof services !== "object" || Array.isArray(services)) return [];
|
|
578
636
|
const entries = [];
|
|
579
637
|
for (const [name, config] of Object.entries(services)) if (config === null || config === void 0) entries.push([name, {}]);
|
|
580
638
|
else if (typeof config === "object" && !Array.isArray(config)) entries.push([name, config]);
|
|
@@ -600,7 +658,9 @@ const requireResourceLimits = {
|
|
|
600
658
|
const diagnostics = [];
|
|
601
659
|
for (const [name, config] of composeServices(composeContent)) {
|
|
602
660
|
const limits = (config.deploy?.resources)?.limits;
|
|
603
|
-
|
|
661
|
+
const hasDeployLimits = Boolean(limits?.cpus || limits?.memory);
|
|
662
|
+
const hasServiceLevelLimits = Boolean(config.mem_limit || config.cpus || config.cpu_quota);
|
|
663
|
+
if (!(hasDeployLimits || hasServiceLevelLimits)) 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])));
|
|
604
664
|
}
|
|
605
665
|
return diagnostics;
|
|
606
666
|
},
|
|
@@ -717,24 +777,52 @@ const undefinedModelReference = {
|
|
|
717
777
|
key: "docker-doctor/undefined-model-reference",
|
|
718
778
|
message: "Service model references must be declared in top-level models"
|
|
719
779
|
};
|
|
780
|
+
const collectModelBindings = (composeContent) => {
|
|
781
|
+
const bindings = [];
|
|
782
|
+
for (const [name, config] of Object.entries(topLevelModels(composeContent))) {
|
|
783
|
+
if (!config || typeof config !== "object") continue;
|
|
784
|
+
const { model } = config;
|
|
785
|
+
if (typeof model === "string") bindings.push({
|
|
786
|
+
model,
|
|
787
|
+
path: [
|
|
788
|
+
"models",
|
|
789
|
+
name,
|
|
790
|
+
"model"
|
|
791
|
+
],
|
|
792
|
+
subject: `Model '${name}' artifact`
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
for (const [name, config] of composeServices(composeContent)) {
|
|
796
|
+
const { provider } = config;
|
|
797
|
+
if (!provider || typeof provider !== "object") continue;
|
|
798
|
+
const { type, options } = provider;
|
|
799
|
+
if (type !== "model" || !options || typeof options !== "object") continue;
|
|
800
|
+
const { model } = options;
|
|
801
|
+
if (typeof model === "string") bindings.push({
|
|
802
|
+
model,
|
|
803
|
+
path: [
|
|
804
|
+
"services",
|
|
805
|
+
name,
|
|
806
|
+
"provider",
|
|
807
|
+
"options",
|
|
808
|
+
"model"
|
|
809
|
+
],
|
|
810
|
+
subject: `Service '${name}' model provider`
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
return bindings;
|
|
814
|
+
};
|
|
720
815
|
const pinModelVersion = {
|
|
721
816
|
category: "Compose",
|
|
722
817
|
check(composeContent, file, context) {
|
|
723
818
|
const diagnostics = [];
|
|
724
|
-
for (const
|
|
725
|
-
if (!config || typeof config !== "object") continue;
|
|
726
|
-
const { model } = config;
|
|
727
|
-
if (typeof model !== "string") continue;
|
|
819
|
+
for (const { subject, model, path } of collectModelBindings(composeContent)) {
|
|
728
820
|
const ref = parseImageRef(model);
|
|
729
821
|
if (ref.isVariable) continue;
|
|
730
822
|
const issue = mutableRefIssue(ref);
|
|
731
823
|
if (!issue) continue;
|
|
732
|
-
const detail = issue === "untagged" ?
|
|
733
|
-
diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `${detail} Every pull may fetch different weights.`, this.help, context?.locate?.(
|
|
734
|
-
"models",
|
|
735
|
-
name,
|
|
736
|
-
"model"
|
|
737
|
-
])));
|
|
824
|
+
const detail = issue === "untagged" ? `${subject} '${model}' does not specify a tag.` : `${subject} '${model}' uses the mutable 'latest' tag.`;
|
|
825
|
+
diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `${detail} Every pull may fetch different weights.`, this.help, context?.locate?.(path)));
|
|
738
826
|
}
|
|
739
827
|
return diagnostics;
|
|
740
828
|
},
|
|
@@ -748,18 +836,21 @@ const composeModelRules = [undefinedModelReference, pinModelVersion];
|
|
|
748
836
|
//#endregion
|
|
749
837
|
//#region ../core/src/rules/secret-keywords.ts
|
|
750
838
|
const SECRET_KEY_PATTERNS = [
|
|
751
|
-
/
|
|
839
|
+
/password(?:[_-]|$)/iu,
|
|
752
840
|
/(?:^|[_-])secret(?:[_-]|$)/iu,
|
|
753
841
|
/(?:^|[_-])token(?:[_-]|$)/iu,
|
|
754
842
|
/(?:^|[_-])api_key(?:[_-]|$)/iu,
|
|
843
|
+
/(?:^|[_-])apikey(?:[_-]|$)/iu,
|
|
755
844
|
/(?:^|[_-])private_key(?:[_-]|$)/iu,
|
|
756
|
-
/(?:^|[_-])auth(?:[_-]|$)/iu
|
|
845
|
+
/(?:^|[_-])auth(?:[_-]|$)/iu,
|
|
846
|
+
/(?:^|[_-])pat(?:[_-]|$)/iu
|
|
757
847
|
];
|
|
758
848
|
const isSecretKey = (key) => SECRET_KEY_PATTERNS.some((regex) => regex.test(key));
|
|
849
|
+
const CREDENTIAL_FREE_URL = /^[a-z][a-z0-9+.-]*:\/\/[^@/\s]*(?:\/|$)/iu;
|
|
850
|
+
const isLiteralSecretValue = (value) => value.length > 0 && !value.startsWith("$") && !CREDENTIAL_FREE_URL.test(value);
|
|
759
851
|
|
|
760
852
|
//#endregion
|
|
761
853
|
//#region ../core/src/rules/compose-security.ts
|
|
762
|
-
const DOCKER_SOCKET = "/var/run/docker.sock";
|
|
763
854
|
const noPrivilegedService = {
|
|
764
855
|
category: "Compose",
|
|
765
856
|
check(composeContent, file, context) {
|
|
@@ -776,10 +867,54 @@ const noPrivilegedService = {
|
|
|
776
867
|
key: "docker-doctor/no-privileged-service",
|
|
777
868
|
message: "Do not run services in privileged mode"
|
|
778
869
|
};
|
|
870
|
+
const DOCKER_SOCKET_TARGET = "/var/run/docker.sock";
|
|
871
|
+
const INTERPOLATION_WITH_DEFAULT = /\$\{[^}:?-]+:?-(?<fallback>[^}]*)\}/gu;
|
|
872
|
+
const INTERPOLATION_WITHOUT_DEFAULT = /\$\{[^}]*\}|\$[A-Za-z_][A-Za-z0-9_]*/gu;
|
|
873
|
+
const resolveInterpolationDefaults = (value) => value.replace(INTERPOLATION_WITH_DEFAULT, (_match, fallback) => fallback).replace(INTERPOLATION_WITHOUT_DEFAULT, "");
|
|
874
|
+
const PATH_SHAPED = /^(?:[/.~]|[A-Za-z]:)/u;
|
|
875
|
+
const isDockerSocketPath = (rawPath) => {
|
|
876
|
+
const normalized = rawPath.replaceAll("\\", "/");
|
|
877
|
+
return PATH_SHAPED.test(normalized) && (normalized.endsWith("/docker.sock") || normalized.endsWith("/pipe/docker_engine"));
|
|
878
|
+
};
|
|
879
|
+
const splitShortSyntax = (spec) => {
|
|
880
|
+
const parts = [];
|
|
881
|
+
let depth = 0;
|
|
882
|
+
let current = "";
|
|
883
|
+
for (const char of spec) {
|
|
884
|
+
if (char === "{") depth += 1;
|
|
885
|
+
else if (char === "}") depth = Math.max(0, depth - 1);
|
|
886
|
+
if (char === ":" && depth === 0) {
|
|
887
|
+
parts.push(current);
|
|
888
|
+
current = "";
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
current += char;
|
|
892
|
+
}
|
|
893
|
+
parts.push(current);
|
|
894
|
+
return parts;
|
|
895
|
+
};
|
|
896
|
+
const volumeMount = (volume) => {
|
|
897
|
+
if (typeof volume === "string") {
|
|
898
|
+
const [source, target] = splitShortSyntax(volume);
|
|
899
|
+
return target === void 0 ? void 0 : {
|
|
900
|
+
source,
|
|
901
|
+
target
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
if (volume && typeof volume === "object") {
|
|
905
|
+
const { source, target } = volume;
|
|
906
|
+
return {
|
|
907
|
+
source: typeof source === "string" ? source : void 0,
|
|
908
|
+
target: typeof target === "string" ? target : void 0
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
};
|
|
779
912
|
const mountsDockerSocket = (volume) => {
|
|
780
|
-
|
|
781
|
-
if (
|
|
782
|
-
|
|
913
|
+
const mount = volumeMount(volume);
|
|
914
|
+
if (!mount?.source) return false;
|
|
915
|
+
const resolvedSource = resolveInterpolationDefaults(mount.source);
|
|
916
|
+
if (resolvedSource !== "") return isDockerSocketPath(resolvedSource);
|
|
917
|
+
return resolveInterpolationDefaults(mount.target ?? "") === DOCKER_SOCKET_TARGET;
|
|
783
918
|
};
|
|
784
919
|
const noDockerSocketMount = {
|
|
785
920
|
category: "Compose",
|
|
@@ -802,7 +937,6 @@ const noDockerSocketMount = {
|
|
|
802
937
|
key: "docker-doctor/no-docker-socket-mount",
|
|
803
938
|
message: "Do not bind-mount the Docker socket into services"
|
|
804
939
|
};
|
|
805
|
-
const isLiteralSecretValue = (value) => typeof value === "string" && value.length > 0 && !value.startsWith("$");
|
|
806
940
|
const noPlaintextSecrets = {
|
|
807
941
|
category: "Compose",
|
|
808
942
|
check(composeContent, file, context) {
|
|
@@ -818,7 +952,7 @@ const noPlaintextSecrets = {
|
|
|
818
952
|
if (eqIndex <= 0) continue;
|
|
819
953
|
const key = entry.slice(0, eqIndex);
|
|
820
954
|
const value = entry.slice(eqIndex + 1);
|
|
821
|
-
if (isSecretKey(key) && isLiteralSecretValue(value)) flag(name, key, context?.locate?.([
|
|
955
|
+
if (isSecretKey(key) && typeof value === "string" && isLiteralSecretValue(value)) flag(name, key, context?.locate?.([
|
|
822
956
|
"services",
|
|
823
957
|
name,
|
|
824
958
|
"environment",
|
|
@@ -826,7 +960,7 @@ const noPlaintextSecrets = {
|
|
|
826
960
|
]));
|
|
827
961
|
}
|
|
828
962
|
else if (environment && typeof environment === "object") {
|
|
829
|
-
for (const [key, value] of Object.entries(environment)) if (isSecretKey(key) && isLiteralSecretValue(value)) flag(name, key, context?.locate?.([
|
|
963
|
+
for (const [key, value] of Object.entries(environment)) if (isSecretKey(key) && typeof value === "string" && isLiteralSecretValue(value)) flag(name, key, context?.locate?.([
|
|
830
964
|
"services",
|
|
831
965
|
name,
|
|
832
966
|
"environment",
|
|
@@ -1074,7 +1208,7 @@ const noSecretsInEnv = {
|
|
|
1074
1208
|
const match = args.match(/^(?<key>[^\s]+)\s+(?<value>.*)$/u);
|
|
1075
1209
|
if (match?.groups) {
|
|
1076
1210
|
const { key, value } = match.groups;
|
|
1077
|
-
if (isSecretKey(key) && value
|
|
1211
|
+
if (isSecretKey(key) && isLiteralSecretValue(value) && !value.startsWith("{")) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Potential secret found in ${inst.instruction}: '${key}'. Secrets baked into images can be extracted easily by anyone with image access.`, this.help, inst.line));
|
|
1078
1212
|
}
|
|
1079
1213
|
} else {
|
|
1080
1214
|
const parts = args.split(/\s+/u);
|
|
@@ -1086,7 +1220,7 @@ const noSecretsInEnv = {
|
|
|
1086
1220
|
key = part.slice(0, eqIndex);
|
|
1087
1221
|
value = part.slice(eqIndex + 1);
|
|
1088
1222
|
} else key = part;
|
|
1089
|
-
if (isSecretKey(key) && value
|
|
1223
|
+
if (isSecretKey(key) && isLiteralSecretValue(value) && !value.startsWith("{")) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Potential secret found in ${inst.instruction}: '${key}'. Secrets baked into images can be extracted easily by anyone with image access.`, this.help, inst.line));
|
|
1090
1224
|
}
|
|
1091
1225
|
}
|
|
1092
1226
|
}
|
|
@@ -1166,12 +1300,16 @@ const resolveSeverity = (rule, rulesConfig, categoriesConfig) => rulesConfig?.[r
|
|
|
1166
1300
|
//#endregion
|
|
1167
1301
|
//#region ../core/src/runners/dockerfile-runner.ts
|
|
1168
1302
|
const runDockerfileRules = (instructions, file, projectFiles, rulesConfig, categoriesConfig) => {
|
|
1303
|
+
if (instructions.length === 0) return [];
|
|
1169
1304
|
const diagnostics = [];
|
|
1170
1305
|
for (const rule of allDockerfileRules) {
|
|
1171
1306
|
const severity = resolveSeverity(rule, rulesConfig, categoriesConfig);
|
|
1172
1307
|
if (severity === "off") continue;
|
|
1173
1308
|
const ruleDiagnostics = rule.check(instructions, file, { projectFiles });
|
|
1174
|
-
|
|
1309
|
+
for (const diag of ruleDiagnostics) {
|
|
1310
|
+
diag.category = rule.category;
|
|
1311
|
+
if (severity !== rule.defaultSeverity) diag.severity = severity;
|
|
1312
|
+
}
|
|
1175
1313
|
diagnostics.push(...ruleDiagnostics);
|
|
1176
1314
|
}
|
|
1177
1315
|
return diagnostics;
|
|
@@ -1185,7 +1323,10 @@ const runComposeRules = (composeContent, file, rulesConfig, categoriesConfig, lo
|
|
|
1185
1323
|
const severity = resolveSeverity(rule, rulesConfig, categoriesConfig);
|
|
1186
1324
|
if (severity === "off") continue;
|
|
1187
1325
|
const ruleDiagnostics = rule.check(composeContent, file, { locate });
|
|
1188
|
-
|
|
1326
|
+
for (const diag of ruleDiagnostics) {
|
|
1327
|
+
diag.category = rule.category;
|
|
1328
|
+
if (severity !== rule.defaultSeverity) diag.severity = severity;
|
|
1329
|
+
}
|
|
1189
1330
|
diagnostics.push(...ruleDiagnostics);
|
|
1190
1331
|
}
|
|
1191
1332
|
return diagnostics;
|
|
@@ -1295,6 +1436,16 @@ const parseConfigFile = async (filePath, format, parse) => {
|
|
|
1295
1436
|
throw new ConfigError({ message: `Failed to parse config ${format}: ${msg}` });
|
|
1296
1437
|
}
|
|
1297
1438
|
};
|
|
1439
|
+
const TYPESCRIPT_CONFIG_PATTERN = /\.[cm]?ts$/u;
|
|
1440
|
+
const TYPE_STRIPPING_FAILURES = {
|
|
1441
|
+
ERR_UNKNOWN_FILE_EXTENSION: "this Node.js is not stripping TypeScript types, which needs 22.18 or newer with stripping left on (--no-experimental-strip-types turns it off)",
|
|
1442
|
+
ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING: "Node.js never strips TypeScript types for files under node_modules, on any version, so the config has to live outside it"
|
|
1443
|
+
};
|
|
1444
|
+
const typeStrippingFailure = (filePath, error) => {
|
|
1445
|
+
if (!TYPESCRIPT_CONFIG_PATTERN.test(filePath)) return;
|
|
1446
|
+
if (typeof error !== "object" || error === null || !("code" in error) || typeof error.code !== "string") return;
|
|
1447
|
+
return TYPE_STRIPPING_FAILURES[error.code];
|
|
1448
|
+
};
|
|
1298
1449
|
const importConfig = async (filePath) => {
|
|
1299
1450
|
if (filePath.endsWith(".json")) return parseConfigFile(filePath, "JSON", JSON.parse);
|
|
1300
1451
|
if (filePath.endsWith(".yaml") || filePath.endsWith(".yml")) return parseConfigFile(filePath, "YAML", parse);
|
|
@@ -1302,6 +1453,8 @@ const importConfig = async (filePath) => {
|
|
|
1302
1453
|
const configModule = await import(filePath);
|
|
1303
1454
|
return configModule.default || configModule;
|
|
1304
1455
|
} catch (error) {
|
|
1456
|
+
const reason = typeStrippingFailure(filePath, error);
|
|
1457
|
+
if (reason) throw new ConfigError({ message: `Cannot load ${filePath} on Node.js ${process.version}: ${reason}. A docker-doctor.config.yaml or .json needs no TypeScript support at all.` });
|
|
1305
1458
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1306
1459
|
throw new ConfigError({ message: `Failed to load config file ${filePath}: ${msg}` });
|
|
1307
1460
|
}
|
|
@@ -1396,9 +1549,15 @@ const calculateScore = (diagnostics) => {
|
|
|
1396
1549
|
|
|
1397
1550
|
//#endregion
|
|
1398
1551
|
//#region ../core/src/report.ts
|
|
1399
|
-
const REPORT_SCHEMA_VERSION =
|
|
1552
|
+
const REPORT_SCHEMA_VERSION = 3;
|
|
1553
|
+
const resolveCategory = (diagnostic) => {
|
|
1554
|
+
const category = diagnostic.category ?? findRule(diagnostic.rule)?.category;
|
|
1555
|
+
if (!category) throw new Error(`Diagnostic for unknown rule "${diagnostic.rule}" has no category`);
|
|
1556
|
+
return category;
|
|
1557
|
+
};
|
|
1400
1558
|
const toJsonReport = (diagnostics, score, label, project) => ({
|
|
1401
1559
|
diagnostics: diagnostics.map((d) => ({
|
|
1560
|
+
category: resolveCategory(d),
|
|
1402
1561
|
column: d.column,
|
|
1403
1562
|
file: d.file,
|
|
1404
1563
|
help: d.help,
|
|
@@ -1409,11 +1568,11 @@ const toJsonReport = (diagnostics, score, label, project) => ({
|
|
|
1409
1568
|
})),
|
|
1410
1569
|
label,
|
|
1411
1570
|
project,
|
|
1412
|
-
schemaVersion:
|
|
1571
|
+
schemaVersion: 3,
|
|
1413
1572
|
score,
|
|
1414
1573
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1415
1574
|
});
|
|
1416
1575
|
|
|
1417
1576
|
//#endregion
|
|
1418
1577
|
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 };
|
|
1419
|
-
//# sourceMappingURL=src-
|
|
1578
|
+
//# sourceMappingURL=src-BOGzkp17.mjs.map
|