@docker-doctor/cli 0.4.3 → 0.4.4

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