@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
|
@@ -33,7 +33,7 @@ node_path = __toESM(node_path, 1);
|
|
|
33
33
|
let yaml = require("yaml");
|
|
34
34
|
|
|
35
35
|
//#region package.json
|
|
36
|
-
var version = "0.5.
|
|
36
|
+
var version = "0.5.2";
|
|
37
37
|
|
|
38
38
|
//#endregion
|
|
39
39
|
//#region ../core/src/project-info/ignore.ts
|
|
@@ -100,6 +100,7 @@ const walk = async (dir, fileList = []) => {
|
|
|
100
100
|
}));
|
|
101
101
|
return fileList;
|
|
102
102
|
};
|
|
103
|
+
const toPosixRelative = (rootDir, file) => node_path.default.relative(rootDir, file).split(node_path.default.sep).join("/");
|
|
103
104
|
const discoverProject = async (rootDir, options) => {
|
|
104
105
|
const allFiles = await walk(rootDir);
|
|
105
106
|
const dockerfiles = [];
|
|
@@ -107,10 +108,13 @@ const discoverProject = async (rootDir, options) => {
|
|
|
107
108
|
const dockerignores = [];
|
|
108
109
|
const isIgnored = createIgnoreMatcher(options?.ignoreFiles);
|
|
109
110
|
for (const file of allFiles) {
|
|
110
|
-
const relative =
|
|
111
|
+
const relative = toPosixRelative(rootDir, file);
|
|
111
112
|
if (isIgnored(relative)) continue;
|
|
112
113
|
const base = node_path.default.basename(file).toLowerCase();
|
|
113
|
-
if (base
|
|
114
|
+
if (base.endsWith(".dockerignore")) {
|
|
115
|
+
dockerignores.push(relative);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
114
118
|
if (base === "dockerfile" || base.startsWith("dockerfile.") || base.endsWith(".dockerfile")) dockerfiles.push(relative);
|
|
115
119
|
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);
|
|
116
120
|
}
|
|
@@ -121,6 +125,72 @@ const discoverProject = async (rootDir, options) => {
|
|
|
121
125
|
};
|
|
122
126
|
};
|
|
123
127
|
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region ../core/src/errors/config-error.ts
|
|
130
|
+
var ConfigError = class extends Error {
|
|
131
|
+
_tag = "ConfigError";
|
|
132
|
+
constructor(options) {
|
|
133
|
+
super(options.message);
|
|
134
|
+
this.name = "ConfigError";
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region ../core/src/errors/parse-error.ts
|
|
140
|
+
var ParseError = class extends Error {
|
|
141
|
+
_tag = "ParseError";
|
|
142
|
+
file;
|
|
143
|
+
constructor(options) {
|
|
144
|
+
super(options.message);
|
|
145
|
+
this.name = "ParseError";
|
|
146
|
+
this.file = options.file;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region ../core/src/parsers/shell-quotes.ts
|
|
152
|
+
/**
|
|
153
|
+
* Replaces every character inside a single- or double-quoted region with a
|
|
154
|
+
* space, keeping the quotes themselves and the string length, so callers can
|
|
155
|
+
* run position-sensitive regexes over shell text without matching inside
|
|
156
|
+
* string literals. A backslash inside double quotes escapes the next
|
|
157
|
+
* character; single quotes have no escapes (POSIX sh). An unclosed quote
|
|
158
|
+
* masks to the end of the text.
|
|
159
|
+
*
|
|
160
|
+
* What this deliberately does NOT do: it is not a tokenizer. It performs no
|
|
161
|
+
* variable or command expansion, knows nothing about subshells, backslash
|
|
162
|
+
* line continuations, `$'...'` or `<<` heredoc bodies, and does not split
|
|
163
|
+
* words. It only blanks quoted spans so regexes keep their positions.
|
|
164
|
+
*/
|
|
165
|
+
const maskQuotedText = (text) => {
|
|
166
|
+
let out = "";
|
|
167
|
+
let quote = null;
|
|
168
|
+
let index = 0;
|
|
169
|
+
while (index < text.length) {
|
|
170
|
+
const char = text[index];
|
|
171
|
+
if (quote === null) {
|
|
172
|
+
if (char === "\"" || char === "'") quote = char;
|
|
173
|
+
out += char;
|
|
174
|
+
index += 1;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (char === quote) {
|
|
178
|
+
quote = null;
|
|
179
|
+
out += char;
|
|
180
|
+
index += 1;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (quote === "\"" && char === "\\" && index + 1 < text.length) {
|
|
184
|
+
out += " ";
|
|
185
|
+
index += 2;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
out += " ";
|
|
189
|
+
index += 1;
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
};
|
|
193
|
+
|
|
124
194
|
//#endregion
|
|
125
195
|
//#region ../core/src/parsers/dockerfile-parser.ts
|
|
126
196
|
const DOCKERFILE_KEYWORDS = /* @__PURE__ */ new Set([
|
|
@@ -183,7 +253,10 @@ const matchInstructionKeyword = (lineContent) => {
|
|
|
183
253
|
};
|
|
184
254
|
return null;
|
|
185
255
|
};
|
|
186
|
-
const findHeredocDelimiters = (lineContent) =>
|
|
256
|
+
const findHeredocDelimiters = (lineContent) => {
|
|
257
|
+
const masked = maskQuotedText(lineContent);
|
|
258
|
+
return [...lineContent.matchAll(HEREDOC_OPENER_RE)].filter((match) => masked[match.index] === "<").map((match) => match.groups?.delim ?? "");
|
|
259
|
+
};
|
|
187
260
|
const processHeredocLine = (state, trimmed) => {
|
|
188
261
|
if (trimmed === state.heredocQueue[0]) state.heredocQueue.shift();
|
|
189
262
|
else state.currentArgs += (state.currentArgs ? " " : "") + trimmed;
|
|
@@ -206,7 +279,7 @@ const processInstructionLine = (state, trimmed, lineNum) => {
|
|
|
206
279
|
if (state.heredocQueue.length > 0) return;
|
|
207
280
|
if (!hasContinuation) closeInstruction(state);
|
|
208
281
|
};
|
|
209
|
-
const parseDockerfile = (content) => {
|
|
282
|
+
const parseDockerfile = (content, file = "Dockerfile") => {
|
|
210
283
|
const state = createParserState();
|
|
211
284
|
const lines = content.split(/\r?\n/u);
|
|
212
285
|
for (const [i, rawLine] of lines.entries()) {
|
|
@@ -219,32 +292,14 @@ const parseDockerfile = (content) => {
|
|
|
219
292
|
if (insideHeredoc) processHeredocLine(state, trimmed);
|
|
220
293
|
else processInstructionLine(state, trimmed, lineNum);
|
|
221
294
|
}
|
|
295
|
+
if (state.heredocQueue.length > 0) throw new ParseError({
|
|
296
|
+
file,
|
|
297
|
+
message: `Unterminated heredoc: delimiter "${state.heredocQueue[0]}" (opened on line ${state.startLine}) never closed. BuildKit rejects this Dockerfile too.`
|
|
298
|
+
});
|
|
222
299
|
closeInstruction(state);
|
|
223
300
|
return state.instructions;
|
|
224
301
|
};
|
|
225
302
|
|
|
226
|
-
//#endregion
|
|
227
|
-
//#region ../core/src/errors/config-error.ts
|
|
228
|
-
var ConfigError = class extends Error {
|
|
229
|
-
_tag = "ConfigError";
|
|
230
|
-
constructor(options) {
|
|
231
|
-
super(options.message);
|
|
232
|
-
this.name = "ConfigError";
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
//#endregion
|
|
237
|
-
//#region ../core/src/errors/parse-error.ts
|
|
238
|
-
var ParseError = class extends Error {
|
|
239
|
-
_tag = "ParseError";
|
|
240
|
-
file;
|
|
241
|
-
constructor(options) {
|
|
242
|
-
super(options.message);
|
|
243
|
-
this.name = "ParseError";
|
|
244
|
-
this.file = options.file;
|
|
245
|
-
}
|
|
246
|
-
};
|
|
247
|
-
|
|
248
303
|
//#endregion
|
|
249
304
|
//#region ../core/src/parsers/compose-parser.ts
|
|
250
305
|
const parseCompose = (content, filepath) => {
|
|
@@ -478,6 +533,7 @@ const combineAptUpdateInstall = {
|
|
|
478
533
|
};
|
|
479
534
|
const PIPEFAIL_SETTING_RE = /(?:^|\s)-[A-Za-z]*o\s+pipefail\b/u;
|
|
480
535
|
const HAS_PIPE_RE = /(?<!\|)\|(?!\|)/u;
|
|
536
|
+
const CD_COMMAND_RE = /(?:^|&&|\|\||;|\||\(|`|\$\()\s*cd(?=\s|$)/u;
|
|
481
537
|
const shellDirectiveEnablesPipefail = (args) => {
|
|
482
538
|
const argv = parseExecForm(args);
|
|
483
539
|
return argv !== null && PIPEFAIL_SETTING_RE.test(argv.join(" "));
|
|
@@ -505,8 +561,9 @@ const usePipefail = {
|
|
|
505
561
|
}
|
|
506
562
|
if (inst.instruction !== "RUN") continue;
|
|
507
563
|
const { args } = inst;
|
|
508
|
-
if (!HAS_PIPE_RE.test(args)) continue;
|
|
509
564
|
const execArgv = parseExecForm(args);
|
|
565
|
+
const pipeText = execArgv === null ? maskQuotedText(args) : execArgv.join(" ");
|
|
566
|
+
if (!HAS_PIPE_RE.test(pipeText)) continue;
|
|
510
567
|
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));
|
|
511
568
|
}
|
|
512
569
|
return diagnostics;
|
|
@@ -535,7 +592,7 @@ const avoidRunCd = {
|
|
|
535
592
|
category: "Best Practices",
|
|
536
593
|
check(instructions, file) {
|
|
537
594
|
const diagnostics = [];
|
|
538
|
-
for (const inst of instructions) if (inst.instruction === "RUN" &&
|
|
595
|
+
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));
|
|
539
596
|
return diagnostics;
|
|
540
597
|
},
|
|
541
598
|
defaultSeverity: "info",
|
|
@@ -598,12 +655,13 @@ const bestPracticesRules = [
|
|
|
598
655
|
* with a null body (`web:` with nothing under it) is returned as an empty
|
|
599
656
|
* config so rules still check it: it is the least-configured service in
|
|
600
657
|
* the file, not a service to skip. Scalar and array bodies are invalid
|
|
601
|
-
* compose and are dropped.
|
|
658
|
+
* compose and are dropped. A `services:` written as a YAML sequence is
|
|
659
|
+
* invalid Compose and yields no services.
|
|
602
660
|
*/
|
|
603
661
|
const composeServices = (composeContent) => {
|
|
604
662
|
if (!composeContent || typeof composeContent !== "object" || !("services" in composeContent)) return [];
|
|
605
663
|
const { services } = composeContent;
|
|
606
|
-
if (!services || typeof services !== "object") return [];
|
|
664
|
+
if (!services || typeof services !== "object" || Array.isArray(services)) return [];
|
|
607
665
|
const entries = [];
|
|
608
666
|
for (const [name, config] of Object.entries(services)) if (config === null || config === void 0) entries.push([name, {}]);
|
|
609
667
|
else if (typeof config === "object" && !Array.isArray(config)) entries.push([name, config]);
|
|
@@ -629,7 +687,9 @@ const requireResourceLimits = {
|
|
|
629
687
|
const diagnostics = [];
|
|
630
688
|
for (const [name, config] of composeServices(composeContent)) {
|
|
631
689
|
const limits = (config.deploy?.resources)?.limits;
|
|
632
|
-
|
|
690
|
+
const hasDeployLimits = Boolean(limits?.cpus || limits?.memory);
|
|
691
|
+
const hasServiceLevelLimits = Boolean(config.mem_limit || config.cpus || config.cpu_quota);
|
|
692
|
+
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])));
|
|
633
693
|
}
|
|
634
694
|
return diagnostics;
|
|
635
695
|
},
|
|
@@ -746,24 +806,52 @@ const undefinedModelReference = {
|
|
|
746
806
|
key: "docker-doctor/undefined-model-reference",
|
|
747
807
|
message: "Service model references must be declared in top-level models"
|
|
748
808
|
};
|
|
809
|
+
const collectModelBindings = (composeContent) => {
|
|
810
|
+
const bindings = [];
|
|
811
|
+
for (const [name, config] of Object.entries(topLevelModels(composeContent))) {
|
|
812
|
+
if (!config || typeof config !== "object") continue;
|
|
813
|
+
const { model } = config;
|
|
814
|
+
if (typeof model === "string") bindings.push({
|
|
815
|
+
model,
|
|
816
|
+
path: [
|
|
817
|
+
"models",
|
|
818
|
+
name,
|
|
819
|
+
"model"
|
|
820
|
+
],
|
|
821
|
+
subject: `Model '${name}' artifact`
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
for (const [name, config] of composeServices(composeContent)) {
|
|
825
|
+
const { provider } = config;
|
|
826
|
+
if (!provider || typeof provider !== "object") continue;
|
|
827
|
+
const { type, options } = provider;
|
|
828
|
+
if (type !== "model" || !options || typeof options !== "object") continue;
|
|
829
|
+
const { model } = options;
|
|
830
|
+
if (typeof model === "string") bindings.push({
|
|
831
|
+
model,
|
|
832
|
+
path: [
|
|
833
|
+
"services",
|
|
834
|
+
name,
|
|
835
|
+
"provider",
|
|
836
|
+
"options",
|
|
837
|
+
"model"
|
|
838
|
+
],
|
|
839
|
+
subject: `Service '${name}' model provider`
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
return bindings;
|
|
843
|
+
};
|
|
749
844
|
const pinModelVersion = {
|
|
750
845
|
category: "Compose",
|
|
751
846
|
check(composeContent, file, context) {
|
|
752
847
|
const diagnostics = [];
|
|
753
|
-
for (const
|
|
754
|
-
if (!config || typeof config !== "object") continue;
|
|
755
|
-
const { model } = config;
|
|
756
|
-
if (typeof model !== "string") continue;
|
|
848
|
+
for (const { subject, model, path } of collectModelBindings(composeContent)) {
|
|
757
849
|
const ref = parseImageRef(model);
|
|
758
850
|
if (ref.isVariable) continue;
|
|
759
851
|
const issue = mutableRefIssue(ref);
|
|
760
852
|
if (!issue) continue;
|
|
761
|
-
const detail = issue === "untagged" ?
|
|
762
|
-
diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `${detail} Every pull may fetch different weights.`, this.help, context?.locate?.(
|
|
763
|
-
"models",
|
|
764
|
-
name,
|
|
765
|
-
"model"
|
|
766
|
-
])));
|
|
853
|
+
const detail = issue === "untagged" ? `${subject} '${model}' does not specify a tag.` : `${subject} '${model}' uses the mutable 'latest' tag.`;
|
|
854
|
+
diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `${detail} Every pull may fetch different weights.`, this.help, context?.locate?.(path)));
|
|
767
855
|
}
|
|
768
856
|
return diagnostics;
|
|
769
857
|
},
|
|
@@ -777,18 +865,21 @@ const composeModelRules = [undefinedModelReference, pinModelVersion];
|
|
|
777
865
|
//#endregion
|
|
778
866
|
//#region ../core/src/rules/secret-keywords.ts
|
|
779
867
|
const SECRET_KEY_PATTERNS = [
|
|
780
|
-
/
|
|
868
|
+
/password(?:[_-]|$)/iu,
|
|
781
869
|
/(?:^|[_-])secret(?:[_-]|$)/iu,
|
|
782
870
|
/(?:^|[_-])token(?:[_-]|$)/iu,
|
|
783
871
|
/(?:^|[_-])api_key(?:[_-]|$)/iu,
|
|
872
|
+
/(?:^|[_-])apikey(?:[_-]|$)/iu,
|
|
784
873
|
/(?:^|[_-])private_key(?:[_-]|$)/iu,
|
|
785
|
-
/(?:^|[_-])auth(?:[_-]|$)/iu
|
|
874
|
+
/(?:^|[_-])auth(?:[_-]|$)/iu,
|
|
875
|
+
/(?:^|[_-])pat(?:[_-]|$)/iu
|
|
786
876
|
];
|
|
787
877
|
const isSecretKey = (key) => SECRET_KEY_PATTERNS.some((regex) => regex.test(key));
|
|
878
|
+
const CREDENTIAL_FREE_URL = /^[a-z][a-z0-9+.-]*:\/\/[^@/\s]*(?:\/|$)/iu;
|
|
879
|
+
const isLiteralSecretValue = (value) => value.length > 0 && !value.startsWith("$") && !CREDENTIAL_FREE_URL.test(value);
|
|
788
880
|
|
|
789
881
|
//#endregion
|
|
790
882
|
//#region ../core/src/rules/compose-security.ts
|
|
791
|
-
const DOCKER_SOCKET = "/var/run/docker.sock";
|
|
792
883
|
const noPrivilegedService = {
|
|
793
884
|
category: "Compose",
|
|
794
885
|
check(composeContent, file, context) {
|
|
@@ -805,10 +896,54 @@ const noPrivilegedService = {
|
|
|
805
896
|
key: "docker-doctor/no-privileged-service",
|
|
806
897
|
message: "Do not run services in privileged mode"
|
|
807
898
|
};
|
|
899
|
+
const DOCKER_SOCKET_TARGET = "/var/run/docker.sock";
|
|
900
|
+
const INTERPOLATION_WITH_DEFAULT = /\$\{[^}:?-]+:?-(?<fallback>[^}]*)\}/gu;
|
|
901
|
+
const INTERPOLATION_WITHOUT_DEFAULT = /\$\{[^}]*\}|\$[A-Za-z_][A-Za-z0-9_]*/gu;
|
|
902
|
+
const resolveInterpolationDefaults = (value) => value.replace(INTERPOLATION_WITH_DEFAULT, (_match, fallback) => fallback).replace(INTERPOLATION_WITHOUT_DEFAULT, "");
|
|
903
|
+
const PATH_SHAPED = /^(?:[/.~]|[A-Za-z]:)/u;
|
|
904
|
+
const isDockerSocketPath = (rawPath) => {
|
|
905
|
+
const normalized = rawPath.replaceAll("\\", "/");
|
|
906
|
+
return PATH_SHAPED.test(normalized) && (normalized.endsWith("/docker.sock") || normalized.endsWith("/pipe/docker_engine"));
|
|
907
|
+
};
|
|
908
|
+
const splitShortSyntax = (spec) => {
|
|
909
|
+
const parts = [];
|
|
910
|
+
let depth = 0;
|
|
911
|
+
let current = "";
|
|
912
|
+
for (const char of spec) {
|
|
913
|
+
if (char === "{") depth += 1;
|
|
914
|
+
else if (char === "}") depth = Math.max(0, depth - 1);
|
|
915
|
+
if (char === ":" && depth === 0) {
|
|
916
|
+
parts.push(current);
|
|
917
|
+
current = "";
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
current += char;
|
|
921
|
+
}
|
|
922
|
+
parts.push(current);
|
|
923
|
+
return parts;
|
|
924
|
+
};
|
|
925
|
+
const volumeMount = (volume) => {
|
|
926
|
+
if (typeof volume === "string") {
|
|
927
|
+
const [source, target] = splitShortSyntax(volume);
|
|
928
|
+
return target === void 0 ? void 0 : {
|
|
929
|
+
source,
|
|
930
|
+
target
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
if (volume && typeof volume === "object") {
|
|
934
|
+
const { source, target } = volume;
|
|
935
|
+
return {
|
|
936
|
+
source: typeof source === "string" ? source : void 0,
|
|
937
|
+
target: typeof target === "string" ? target : void 0
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
};
|
|
808
941
|
const mountsDockerSocket = (volume) => {
|
|
809
|
-
|
|
810
|
-
if (
|
|
811
|
-
|
|
942
|
+
const mount = volumeMount(volume);
|
|
943
|
+
if (!mount?.source) return false;
|
|
944
|
+
const resolvedSource = resolveInterpolationDefaults(mount.source);
|
|
945
|
+
if (resolvedSource !== "") return isDockerSocketPath(resolvedSource);
|
|
946
|
+
return resolveInterpolationDefaults(mount.target ?? "") === DOCKER_SOCKET_TARGET;
|
|
812
947
|
};
|
|
813
948
|
const noDockerSocketMount = {
|
|
814
949
|
category: "Compose",
|
|
@@ -831,7 +966,6 @@ const noDockerSocketMount = {
|
|
|
831
966
|
key: "docker-doctor/no-docker-socket-mount",
|
|
832
967
|
message: "Do not bind-mount the Docker socket into services"
|
|
833
968
|
};
|
|
834
|
-
const isLiteralSecretValue = (value) => typeof value === "string" && value.length > 0 && !value.startsWith("$");
|
|
835
969
|
const noPlaintextSecrets = {
|
|
836
970
|
category: "Compose",
|
|
837
971
|
check(composeContent, file, context) {
|
|
@@ -847,7 +981,7 @@ const noPlaintextSecrets = {
|
|
|
847
981
|
if (eqIndex <= 0) continue;
|
|
848
982
|
const key = entry.slice(0, eqIndex);
|
|
849
983
|
const value = entry.slice(eqIndex + 1);
|
|
850
|
-
if (isSecretKey(key) && isLiteralSecretValue(value)) flag(name, key, context?.locate?.([
|
|
984
|
+
if (isSecretKey(key) && typeof value === "string" && isLiteralSecretValue(value)) flag(name, key, context?.locate?.([
|
|
851
985
|
"services",
|
|
852
986
|
name,
|
|
853
987
|
"environment",
|
|
@@ -855,7 +989,7 @@ const noPlaintextSecrets = {
|
|
|
855
989
|
]));
|
|
856
990
|
}
|
|
857
991
|
else if (environment && typeof environment === "object") {
|
|
858
|
-
for (const [key, value] of Object.entries(environment)) if (isSecretKey(key) && isLiteralSecretValue(value)) flag(name, key, context?.locate?.([
|
|
992
|
+
for (const [key, value] of Object.entries(environment)) if (isSecretKey(key) && typeof value === "string" && isLiteralSecretValue(value)) flag(name, key, context?.locate?.([
|
|
859
993
|
"services",
|
|
860
994
|
name,
|
|
861
995
|
"environment",
|
|
@@ -1103,7 +1237,7 @@ const noSecretsInEnv = {
|
|
|
1103
1237
|
const match = args.match(/^(?<key>[^\s]+)\s+(?<value>.*)$/u);
|
|
1104
1238
|
if (match?.groups) {
|
|
1105
1239
|
const { key, value } = match.groups;
|
|
1106
|
-
if (isSecretKey(key) && value
|
|
1240
|
+
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));
|
|
1107
1241
|
}
|
|
1108
1242
|
} else {
|
|
1109
1243
|
const parts = args.split(/\s+/u);
|
|
@@ -1115,7 +1249,7 @@ const noSecretsInEnv = {
|
|
|
1115
1249
|
key = part.slice(0, eqIndex);
|
|
1116
1250
|
value = part.slice(eqIndex + 1);
|
|
1117
1251
|
} else key = part;
|
|
1118
|
-
if (isSecretKey(key) && value
|
|
1252
|
+
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));
|
|
1119
1253
|
}
|
|
1120
1254
|
}
|
|
1121
1255
|
}
|
|
@@ -1195,12 +1329,16 @@ const resolveSeverity = (rule, rulesConfig, categoriesConfig) => rulesConfig?.[r
|
|
|
1195
1329
|
//#endregion
|
|
1196
1330
|
//#region ../core/src/runners/dockerfile-runner.ts
|
|
1197
1331
|
const runDockerfileRules = (instructions, file, projectFiles, rulesConfig, categoriesConfig) => {
|
|
1332
|
+
if (instructions.length === 0) return [];
|
|
1198
1333
|
const diagnostics = [];
|
|
1199
1334
|
for (const rule of allDockerfileRules) {
|
|
1200
1335
|
const severity = resolveSeverity(rule, rulesConfig, categoriesConfig);
|
|
1201
1336
|
if (severity === "off") continue;
|
|
1202
1337
|
const ruleDiagnostics = rule.check(instructions, file, { projectFiles });
|
|
1203
|
-
|
|
1338
|
+
for (const diag of ruleDiagnostics) {
|
|
1339
|
+
diag.category = rule.category;
|
|
1340
|
+
if (severity !== rule.defaultSeverity) diag.severity = severity;
|
|
1341
|
+
}
|
|
1204
1342
|
diagnostics.push(...ruleDiagnostics);
|
|
1205
1343
|
}
|
|
1206
1344
|
return diagnostics;
|
|
@@ -1214,7 +1352,10 @@ const runComposeRules = (composeContent, file, rulesConfig, categoriesConfig, lo
|
|
|
1214
1352
|
const severity = resolveSeverity(rule, rulesConfig, categoriesConfig);
|
|
1215
1353
|
if (severity === "off") continue;
|
|
1216
1354
|
const ruleDiagnostics = rule.check(composeContent, file, { locate });
|
|
1217
|
-
|
|
1355
|
+
for (const diag of ruleDiagnostics) {
|
|
1356
|
+
diag.category = rule.category;
|
|
1357
|
+
if (severity !== rule.defaultSeverity) diag.severity = severity;
|
|
1358
|
+
}
|
|
1218
1359
|
diagnostics.push(...ruleDiagnostics);
|
|
1219
1360
|
}
|
|
1220
1361
|
return diagnostics;
|
|
@@ -1324,6 +1465,16 @@ const parseConfigFile = async (filePath, format, parse) => {
|
|
|
1324
1465
|
throw new ConfigError({ message: `Failed to parse config ${format}: ${msg}` });
|
|
1325
1466
|
}
|
|
1326
1467
|
};
|
|
1468
|
+
const TYPESCRIPT_CONFIG_PATTERN = /\.[cm]?ts$/u;
|
|
1469
|
+
const TYPE_STRIPPING_FAILURES = {
|
|
1470
|
+
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)",
|
|
1471
|
+
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"
|
|
1472
|
+
};
|
|
1473
|
+
const typeStrippingFailure = (filePath, error) => {
|
|
1474
|
+
if (!TYPESCRIPT_CONFIG_PATTERN.test(filePath)) return;
|
|
1475
|
+
if (typeof error !== "object" || error === null || !("code" in error) || typeof error.code !== "string") return;
|
|
1476
|
+
return TYPE_STRIPPING_FAILURES[error.code];
|
|
1477
|
+
};
|
|
1327
1478
|
const importConfig = async (filePath) => {
|
|
1328
1479
|
if (filePath.endsWith(".json")) return parseConfigFile(filePath, "JSON", JSON.parse);
|
|
1329
1480
|
if (filePath.endsWith(".yaml") || filePath.endsWith(".yml")) return parseConfigFile(filePath, "YAML", yaml.parse);
|
|
@@ -1331,6 +1482,8 @@ const importConfig = async (filePath) => {
|
|
|
1331
1482
|
const configModule = await import(filePath);
|
|
1332
1483
|
return configModule.default || configModule;
|
|
1333
1484
|
} catch (error) {
|
|
1485
|
+
const reason = typeStrippingFailure(filePath, error);
|
|
1486
|
+
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.` });
|
|
1334
1487
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1335
1488
|
throw new ConfigError({ message: `Failed to load config file ${filePath}: ${msg}` });
|
|
1336
1489
|
}
|
|
@@ -1425,9 +1578,15 @@ const calculateScore = (diagnostics) => {
|
|
|
1425
1578
|
|
|
1426
1579
|
//#endregion
|
|
1427
1580
|
//#region ../core/src/report.ts
|
|
1428
|
-
const REPORT_SCHEMA_VERSION =
|
|
1581
|
+
const REPORT_SCHEMA_VERSION = 3;
|
|
1582
|
+
const resolveCategory = (diagnostic) => {
|
|
1583
|
+
const category = diagnostic.category ?? findRule(diagnostic.rule)?.category;
|
|
1584
|
+
if (!category) throw new Error(`Diagnostic for unknown rule "${diagnostic.rule}" has no category`);
|
|
1585
|
+
return category;
|
|
1586
|
+
};
|
|
1429
1587
|
const toJsonReport = (diagnostics, score, label, project) => ({
|
|
1430
1588
|
diagnostics: diagnostics.map((d) => ({
|
|
1589
|
+
category: resolveCategory(d),
|
|
1431
1590
|
column: d.column,
|
|
1432
1591
|
file: d.file,
|
|
1433
1592
|
help: d.help,
|
|
@@ -1438,7 +1597,7 @@ const toJsonReport = (diagnostics, score, label, project) => ({
|
|
|
1438
1597
|
})),
|
|
1439
1598
|
label,
|
|
1440
1599
|
project,
|
|
1441
|
-
schemaVersion:
|
|
1600
|
+
schemaVersion: 3,
|
|
1442
1601
|
score,
|
|
1443
1602
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1444
1603
|
});
|
|
@@ -1522,4 +1681,4 @@ Object.defineProperty(exports, 'version', {
|
|
|
1522
1681
|
return version;
|
|
1523
1682
|
}
|
|
1524
1683
|
});
|
|
1525
|
-
//# sourceMappingURL=src-
|
|
1684
|
+
//# sourceMappingURL=src-D3jzc3GX.cjs.map
|