@docker-doctor/cli 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +0,0 @@
1
- {"version":3,"file":"src-BpAv-H0Z.cjs","names":["path","fs","parse","LineCounter","parseDocument","isAlias","isMap","isScalar","isSeq","SHELL_SEPARATOR_RE","WHITESPACE_RE","path","fs","parseYaml","path"],"sources":["../package.json","../../core/src/project-info/ignore.ts","../../core/src/project-info/discover.ts","../../core/src/errors/config-error.ts","../../core/src/errors/parse-error.ts","../../core/src/parsers/shell-quotes.ts","../../core/src/parsers/dockerfile-parser.ts","../../core/src/parsers/compose-parser.ts","../../core/src/parsers/exec-form.ts","../../core/src/parsers/image-ref.ts","../../core/src/rules/create-diagnostic.ts","../../core/src/rules/package-managers.ts","../../core/src/rules/best-practices.ts","../../core/src/rules/compose-services.ts","../../core/src/rules/compose.ts","../../core/src/rules/compose-models.ts","../../core/src/rules/compose-mounts.ts","../../core/src/rules/secret-keywords.ts","../../core/src/rules/compose-security.ts","../../core/src/rules/image-size.ts","../../core/src/rules/performance.ts","../../core/src/rules/security.ts","../../core/src/rules/index.ts","../../core/src/runners/resolve-severity.ts","../../core/src/runners/dockerfile-runner.ts","../../core/src/runners/compose-runner.ts","../../core/src/schemas/config.ts","../../core/src/config/unknown-keys.ts","../../core/src/config/loader.ts","../../core/src/scoring.ts","../../core/src/report.ts"],"sourcesContent":["","const GLOB_SPECIALS_RE = /[.+^${}()|[\\]\\\\]/gu;\n\n/**\n * Compiles one glob pattern from `ignore.files` to a RegExp over\n * POSIX-style relative paths. Supported syntax is the subset the docs\n * promise: `**` crosses directory separators (`**` followed by `/` matches\n * zero or more whole segments), `*` and `?` stay within one segment.\n * Brace expansion and character classes are not supported.\n */\nconst globToRegExp = (pattern: string): RegExp => {\n let source = \"^\";\n let index = 0;\n while (index < pattern.length) {\n const char = pattern[index];\n if (char === \"*\") {\n if (pattern[index + 1] === \"*\") {\n if (pattern[index + 2] === \"/\") {\n source += \"(?:[^/]*/)*\";\n index += 3;\n } else {\n source += \".*\";\n index += 2;\n }\n } else {\n source += \"[^/]*\";\n index += 1;\n }\n } else if (char === \"?\") {\n source += \"[^/]\";\n index += 1;\n } else {\n source += char.replace(GLOB_SPECIALS_RE, String.raw`\\$&`);\n index += 1;\n }\n }\n return new RegExp(`${source}$`, \"u\");\n};\n\n/**\n * Builds a predicate over root-relative paths from `ignore.files`\n * patterns. Windows separators in the tested path are normalized to `/`\n * before matching, so patterns are always written POSIX-style.\n */\nexport const createIgnoreMatcher = (\n patterns?: readonly string[]\n): ((relativePath: string) => boolean) => {\n if (!patterns || patterns.length === 0) {\n return () => false;\n }\n const regexps = patterns.map(globToRegExp);\n return (relativePath) => {\n const normalized = relativePath.replaceAll(\"\\\\\", \"/\");\n return regexps.some((regexp) => regexp.test(normalized));\n };\n};\n","import type { Dirent } from \"node:fs\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport type { ProjectInfo } from \"../types/index\";\nimport { createIgnoreMatcher } from \"./ignore\";\n\n// Directories that never contain a user's own Dockerfiles: VCS metadata,\n// installed dependencies, caches and build output. `build/`, `out/`,\n// `target/` and `vendor/` are deliberately absent. Real Dockerfiles live in\n// them often enough that skipping would hide findings; use `ignore.files`.\nexport const PRUNED_DIRECTORIES = [\n \".git\",\n \".hg\",\n \".svn\",\n \"node_modules\",\n \"bower_components\",\n \".pnpm-store\",\n \".yarn\",\n \".venv\",\n \"venv\",\n \"__pycache__\",\n \".tox\",\n \".mypy_cache\",\n \".pytest_cache\",\n \".ruff_cache\",\n \".gradle\",\n \".turbo\",\n \".next\",\n \".nuxt\",\n \".svelte-kit\",\n \".parcel-cache\",\n \".cache\",\n \"coverage\",\n \"dist\",\n] as const;\n\nconst PRUNED = new Set<string>(PRUNED_DIRECTORIES);\n\n// ProjectInfo paths are the public contract (JSON report `file`, PR-comment\n// links, rule helpers that split on \"/\"), so they are POSIX on every OS.\nconst toPosixRelative = (rootDir: string, file: string): string =>\n path.relative(rootDir, file).split(path.sep).join(\"/\");\n\nconst SUBTREE_SUFFIX = \"/**\";\n\n// A pattern `X/**` ignores every path under a directory that matches `X`,\n// so such a directory can be skipped without reading it. Other patterns\n// never prune: `examples/*` ignores only the direct children of `examples/`,\n// and the files below them must still be found.\nconst createPrunedDirectoryMatcher = (\n patterns: readonly string[] = []\n): ((relativeDir: string) => boolean) =>\n createIgnoreMatcher(\n patterns\n .filter((pattern) => pattern.endsWith(SUBTREE_SUFFIX))\n .map((pattern) => pattern.slice(0, -SUBTREE_SUFFIX.length))\n );\n\ninterface WalkOptions {\n isPrunedByConfig: (relativeDir: string) => boolean;\n onSkippedDirectory?: (relativeDir: string) => void;\n rootDir: string;\n}\n\nconst isPermissionError = (error: unknown): boolean =>\n error instanceof Error &&\n \"code\" in error &&\n (error.code === \"EACCES\" || error.code === \"EPERM\");\n\nconst walk = async (\n dir: string,\n options: WalkOptions,\n fileList: string[] = []\n): Promise<string[]> => {\n let files: Dirent[];\n try {\n files = await fs.readdir(dir, { withFileTypes: true });\n } catch (error: unknown) {\n // An unreadable subdirectory, such as a root-owned volume directory\n // left by `docker compose up`, is skipped. Errors on the scan root\n // itself still fail the scan.\n if (dir !== options.rootDir && isPermissionError(error)) {\n options.onSkippedDirectory?.(toPosixRelative(options.rootDir, dir));\n return fileList;\n }\n throw error;\n }\n await Promise.all(\n files.map(async (file) => {\n const filePath = path.join(dir, file.name);\n if (file.isSymbolicLink()) {\n // Never traverse a symlinked directory (cycles, trees outside the\n // root). A symlinked file is listed and later read through the link.\n fileList.push(filePath);\n return;\n }\n if (file.isDirectory()) {\n if (\n PRUNED.has(file.name) ||\n options.isPrunedByConfig(toPosixRelative(options.rootDir, filePath))\n ) {\n return;\n }\n await walk(filePath, options, fileList);\n } else {\n fileList.push(filePath);\n }\n })\n );\n return fileList;\n};\n\nexport const discoverProject = async (\n rootDir: string,\n options?: {\n ignoreFiles?: readonly string[];\n // Called once per subdirectory that could not be read (EACCES/EPERM),\n // with its root-relative POSIX path. The scan continues without it.\n onSkippedDirectory?: (relativeDir: string) => void;\n }\n): Promise<ProjectInfo> => {\n const allFiles = await walk(rootDir, {\n isPrunedByConfig: createPrunedDirectoryMatcher(options?.ignoreFiles),\n onSkippedDirectory: options?.onSkippedDirectory,\n rootDir,\n });\n const dockerfiles: string[] = [];\n const composeFiles: string[] = [];\n const dockerignores: string[] = [];\n // Still needed after pruning: file patterns such as `**/compose.yaml`\n // and `examples/*` are only applied here.\n const isIgnored = createIgnoreMatcher(options?.ignoreFiles);\n\n for (const file of allFiles) {\n const relative = toPosixRelative(rootDir, file);\n if (isIgnored(relative)) {\n continue;\n }\n const base = path.basename(file).toLowerCase();\n\n if (base.endsWith(\".dockerignore\")) {\n // `.dockerignore` and BuildKit's per-Dockerfile `<name>.dockerignore`\n // are ignore files, never Dockerfiles — even though the latter also\n // matches the `Dockerfile.*` shape below.\n dockerignores.push(relative);\n continue;\n }\n\n // Match Dockerfile, Dockerfile.*, *.dockerfile\n if (\n base === \"dockerfile\" ||\n base.startsWith(\"dockerfile.\") ||\n base.endsWith(\".dockerfile\")\n ) {\n dockerfiles.push(relative);\n }\n\n // Match docker-compose.yml, docker-compose.*.yml, compose.yml, compose.*.yml, and yaml extensions\n if (\n base === \"docker-compose.yml\" ||\n base === \"docker-compose.yaml\" ||\n base === \"compose.yml\" ||\n base === \"compose.yaml\" ||\n ((base.startsWith(\"docker-compose.\") || base.startsWith(\"compose.\")) &&\n (base.endsWith(\".yml\") || base.endsWith(\".yaml\")))\n ) {\n composeFiles.push(relative);\n }\n }\n\n // The traversal is concurrent, so results arrive in I/O-completion order.\n // Sort at the boundary so identical scans produce byte-identical JSON\n // reports and stable PR-comment row ordering. The default comparator is\n // deliberate: localeCompare would make the order machine-dependent, which\n // is the class of bug this sorting exists to fix.\n return {\n composeFiles: composeFiles.toSorted(),\n dockerfiles: dockerfiles.toSorted(),\n dockerignores: dockerignores.toSorted(),\n };\n};\n","export class ConfigError extends Error {\n readonly _tag = \"ConfigError\" as const;\n\n constructor(options: { readonly message: string }) {\n super(options.message);\n this.name = \"ConfigError\";\n }\n}\n","export class ParseError extends Error {\n readonly _tag = \"ParseError\" as const;\n readonly file: string;\n\n constructor(options: { readonly file: string; readonly message: string }) {\n super(options.message);\n this.name = \"ParseError\";\n this.file = options.file;\n }\n}\n","/**\n * Replaces every character inside a single- or double-quoted region with a\n * space, keeping the quotes themselves and the string length, so callers can\n * run position-sensitive regexes over shell text without matching inside\n * string literals. A backslash inside double quotes escapes the next\n * character; single quotes have no escapes (POSIX sh). An unclosed quote\n * masks to the end of the text.\n *\n * What this deliberately does NOT do: it is not a tokenizer. It performs no\n * variable or command expansion, knows nothing about subshells, backslash\n * line continuations, `$'...'` or `<<` heredoc bodies, and does not split\n * words. It only blanks quoted spans so regexes keep their positions.\n */\nexport const maskQuotedText = (text: string): string => {\n let out = \"\";\n let quote: '\"' | \"'\" | null = null;\n let index = 0;\n while (index < text.length) {\n const char = text[index];\n if (quote === null) {\n if (char === '\"' || char === \"'\") {\n quote = char;\n }\n out += char;\n index += 1;\n continue;\n }\n if (char === quote) {\n quote = null;\n out += char;\n index += 1;\n continue;\n }\n if (quote === '\"' && char === \"\\\\\" && index + 1 < text.length) {\n out += \" \";\n index += 2;\n continue;\n }\n out += \" \";\n index += 1;\n }\n return out;\n};\n","import { ParseError } from \"../errors\";\nimport type { DockerfileInstruction } from \"../types/index\";\nimport { maskQuotedText } from \"./shell-quotes\";\n\nconst DOCKERFILE_KEYWORDS = new Set([\n \"ADD\",\n \"ARG\",\n \"CMD\",\n \"COPY\",\n \"ENTRYPOINT\",\n \"ENV\",\n \"EXPOSE\",\n \"FROM\",\n \"HEALTHCHECK\",\n \"LABEL\",\n \"MAINTAINER\",\n \"ONBUILD\",\n \"RUN\",\n \"SHELL\",\n \"STOPSIGNAL\",\n \"USER\",\n \"VOLUME\",\n \"WORKDIR\",\n]);\n\n// The only instructions BuildKit supports heredocs on.\nconst HEREDOC_INSTRUCTIONS = new Set([\"ADD\", \"COPY\", \"RUN\"]);\n\nconst INSTRUCTION_LINE_RE = /^(?<inst>[A-Za-z]+)\\s+(?<args>.*)$/u;\n\n// Matches a BuildKit heredoc opener like <<EOF, <<-EOF, <<'EOF', <<\"EOF\".\n// Three constraints keep this from matching shell constructs that aren't\n// Dockerfile heredocs: (1) `<<` must be preceded by start-of-line or\n// whitespace, so `1<<3` (shell arithmetic) doesn't match and every\n// character-shifted alignment inside `<<<` (here-strings) fails too; (2) the\n// delimiter must be attached directly to `<<` with no whitespace, so\n// `$((1 << 3))` and shell `cat << EOF` (both redirection, not a Dockerfile\n// heredoc) don't match; (3) callers only invoke this for RUN/COPY/ADD, the\n// only instructions BuildKit supports heredocs on. A fourth constraint lives\n// in findHeredocDelimiters: an opener whose `<<` sits inside a quoted string\n// (`RUN echo \"text <<EOF more\"`) is discarded.\n// Global so a single line (e.g. `COPY <<FILE1 <<FILE2 /dest/`) can open more\n// than one.\nconst HEREDOC_OPENER_RE =\n /(?<=^|\\s)<<-?(?<quote>['\"]?)(?<delim>\\w+)\\k<quote>/gu;\n\ninterface ParserState {\n instructions: DockerfileInstruction[];\n currentInstruction: string;\n currentArgs: string;\n startLine: number;\n rawAccumulator: string[];\n // FIFO of heredoc delimiters still open for the current instruction, in\n // the order they were opened (closed in the same order).\n heredocQueue: string[];\n}\n\nconst createParserState = (): ParserState => ({\n currentArgs: \"\",\n currentInstruction: \"\",\n heredocQueue: [],\n instructions: [],\n rawAccumulator: [],\n startLine: 0,\n});\n\nconst closeInstruction = (state: ParserState): void => {\n if (state.currentInstruction) {\n state.instructions.push({\n args: state.currentArgs,\n instruction: state.currentInstruction,\n line: state.startLine,\n raw: state.rawAccumulator.join(\"\\n\"),\n });\n }\n state.currentInstruction = \"\";\n state.currentArgs = \"\";\n state.rawAccumulator = [];\n};\n\nconst matchInstructionKeyword = (\n lineContent: string\n): { instruction: string; args: string } | null => {\n const match = lineContent.match(INSTRUCTION_LINE_RE);\n const matchedWord = match?.groups?.inst.toUpperCase();\n if (matchedWord && DOCKERFILE_KEYWORDS.has(matchedWord)) {\n return { args: match?.groups?.args ?? \"\", instruction: matchedWord };\n }\n\n const word = lineContent.trim().toUpperCase();\n if (DOCKERFILE_KEYWORDS.has(word)) {\n return { args: \"\", instruction: word };\n }\n\n return null;\n};\n\n// Openers are matched on the original text (a quoted delimiter like\n// <<'EOT' must keep its name) and then dropped when the `<<` itself sits\n// inside a quoted string — the masked copy has a space there instead.\nconst findHeredocDelimiters = (lineContent: string): string[] => {\n const masked = maskQuotedText(lineContent);\n return [...lineContent.matchAll(HEREDOC_OPENER_RE)]\n .filter((match) => masked[match.index] === \"<\")\n .map((match) => match.groups?.delim ?? \"\");\n};\n\n// Heredoc body lines are never treated as instructions (not even comment\n// lines, which are shell-comment content here) — they are folded verbatim\n// into the owning instruction's args until the delimiter line closes the\n// (possibly multiple) open heredoc(s), in the order they were opened.\nconst processHeredocLine = (state: ParserState, trimmed: string): void => {\n if (trimmed === state.heredocQueue[0]) {\n state.heredocQueue.shift();\n } else {\n state.currentArgs += (state.currentArgs ? \" \" : \"\") + trimmed;\n }\n\n if (state.heredocQueue.length === 0) {\n // Last open heredoc just closed: the instruction ends here.\n closeInstruction(state);\n }\n};\n\nconst processInstructionLine = (\n state: ParserState,\n trimmed: string,\n lineNum: number\n): void => {\n let lineContent = trimmed;\n\n const hasContinuation = lineContent.endsWith(\"\\\\\");\n if (hasContinuation) {\n lineContent = lineContent.slice(0, -1).trim();\n }\n\n if (state.currentInstruction) {\n state.currentArgs += (state.currentArgs ? \" \" : \"\") + lineContent;\n } else {\n state.startLine = lineNum;\n // Match the first instruction word (e.g. FROM, RUN, COPY)\n const matched = matchInstructionKeyword(lineContent);\n if (matched) {\n state.currentInstruction = matched.instruction;\n state.currentArgs = matched.args;\n }\n }\n\n if (HEREDOC_INSTRUCTIONS.has(state.currentInstruction)) {\n state.heredocQueue.push(...findHeredocDelimiters(lineContent));\n }\n\n if (state.heredocQueue.length > 0) {\n // A heredoc opener implies continuation even without a trailing\n // backslash — keep accumulating until every opened delimiter closes.\n return;\n }\n\n if (!hasContinuation) {\n closeInstruction(state);\n }\n};\n\nexport const parseDockerfile = (\n content: string,\n file = \"Dockerfile\"\n): DockerfileInstruction[] => {\n const state = createParserState();\n const lines = content.split(/\\r?\\n/u);\n\n for (const [i, rawLine] of lines.entries()) {\n const trimmed = rawLine.trim();\n const lineNum = i + 1;\n const insideHeredoc = state.heredocQueue.length > 0;\n\n // Comment lines are dropped by Docker's parser even mid-continuation, so\n // keep them out of args AND `raw` — raw-based rules must not see them.\n // Heredoc bodies are exempt: a leading `#` there is shell content.\n if (!insideHeredoc && trimmed.startsWith(\"#\")) {\n continue;\n }\n\n // Blank lines never carry instruction text. Outside a heredoc they are\n // skipped even mid-continuation. BuildKit does the same (with a\n // deprecation warning) instead of ending the instruction there.\n if (!insideHeredoc && trimmed === \"\") {\n continue;\n }\n\n state.rawAccumulator.push(rawLine);\n\n if (insideHeredoc) {\n processHeredocLine(state, trimmed);\n } else {\n processInstructionLine(state, trimmed, lineNum);\n }\n }\n\n // A heredoc that never closed swallowed every following instruction, so\n // the rules would scan a truncated file and report it clean. BuildKit\n // rejects this Dockerfile too, so fail it rather than scan a fiction.\n if (state.heredocQueue.length > 0) {\n throw new ParseError({\n file,\n message: `Unterminated heredoc: delimiter \"${state.heredocQueue[0]}\" (opened on line ${state.startLine}) never closed. BuildKit rejects this Dockerfile too.`,\n });\n }\n\n // EOF: a trailing backslash continuation still emits whatever was\n // accumulated so far, rather than dropping it.\n closeInstruction(state);\n\n return state.instructions;\n};\n","import {\n isAlias,\n isMap,\n isScalar,\n isSeq,\n LineCounter,\n parse,\n parseDocument,\n} from \"yaml\";\n\nimport { ParseError } from \"../errors\";\nimport type { ComposeLocator } from \"../types/index\";\n\nexport const parseCompose = (content: string, filepath: string): unknown => {\n try {\n // Compose files rely on YAML 1.1 merge keys (`<<: *anchor`); yaml's\n // default 1.2 schema leaves `<<` as a literal key without this option.\n return parse(content, { merge: true });\n } catch (error: unknown) {\n throw new ParseError({\n file: filepath,\n message: error instanceof Error ? error.message : String(error),\n });\n }\n};\n\n/**\n * Builds a {@link ComposeLocator} over the same source text a compose object\n * was parsed from, so rules can attach line numbers to their diagnostics.\n *\n * Keys pulled in via YAML merge keys (`<<: *anchor`) have no concrete node\n * at the merge site, so paths through them resolve to `undefined` — callers\n * fall back to an unnumbered diagnostic, which matches the old behavior.\n */\nexport const createComposeLocator = (content: string): ComposeLocator => {\n const lineCounter = new LineCounter();\n const doc = parseDocument(content, { lineCounter, merge: true });\n\n return (path) => {\n let node: unknown = doc.contents;\n let offset: number | undefined;\n\n for (const segment of path) {\n if (isAlias(node)) {\n node = node.resolve(doc);\n }\n if (isMap(node)) {\n const pair = node.items.find(\n (item) =>\n isScalar(item.key) && String(item.key.value) === String(segment)\n );\n if (!pair || !isScalar(pair.key)) {\n return;\n }\n offset = pair.key.range?.[0];\n node = pair.value;\n } else if (isSeq(node) && typeof segment === \"number\") {\n const item = node.items[segment];\n if (item === undefined || item === null) {\n return;\n }\n offset = (item as { range?: [number, number, number] | null })\n .range?.[0];\n node = item;\n } else {\n return;\n }\n }\n\n return offset === undefined ? undefined : lineCounter.linePos(offset).line;\n };\n};\n","// Exec form is a JSON array of strings: CMD [\"node\", \"index.js\"]. Docker only\n// treats bracket-wrapped args as exec form when they parse as one — anything\n// else (e.g. CMD [node, index.js], whose tokens are unquoted) falls back to\n// shell form under `/bin/sh -c`.\n//\n// An array holding a non-string element (CMD [1, 2]) is null here too. Docker\n// rejects that outright rather than falling back, so the Dockerfile is broken\n// either way and a shell-form diagnostic still points at the offending line.\nexport const parseExecForm = (args: string): string[] | null => {\n const trimmed = args.trim();\n // Cheap reject first: most instructions are shell form, and reaching them\n // through a thrown JSON.parse would be far more expensive.\n if (!trimmed.startsWith(\"[\")) {\n return null;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed);\n } catch {\n return null;\n }\n\n if (!Array.isArray(parsed)) {\n return null;\n }\n if (!parsed.every((el): el is string => typeof el === \"string\")) {\n return null;\n }\n\n return parsed;\n};\n","import type { DockerfileInstruction } from \"../types/index\";\n\nexport interface ImageRef {\n registry?: string;\n name: string;\n tag?: string;\n digest?: string;\n isVariable: boolean;\n}\n\nexport const parseImageRef = (ref: string): ImageRef => {\n if (ref.includes(\"${\") || ref.startsWith(\"$\")) {\n return { isVariable: true, name: ref };\n }\n\n let remainder = ref;\n let digest: string | undefined;\n\n const atIndex = remainder.indexOf(\"@\");\n if (atIndex !== -1) {\n digest = remainder.slice(atIndex + 1);\n remainder = remainder.slice(0, atIndex);\n }\n\n let tag: string | undefined;\n const lastColonIndex = remainder.lastIndexOf(\":\");\n const lastSlashIndex = remainder.lastIndexOf(\"/\");\n\n if (lastColonIndex !== -1 && lastColonIndex > lastSlashIndex) {\n tag = remainder.slice(lastColonIndex + 1);\n remainder = remainder.slice(0, lastColonIndex);\n }\n\n let registry: string | undefined;\n const firstSlashIndex = remainder.indexOf(\"/\");\n if (firstSlashIndex !== -1) {\n const firstSegment = remainder.slice(0, firstSlashIndex);\n if (\n firstSegment.includes(\".\") ||\n firstSegment.includes(\":\") ||\n firstSegment === \"localhost\"\n ) {\n registry = firstSegment;\n remainder = remainder.slice(firstSlashIndex + 1);\n }\n }\n\n return {\n digest,\n isVariable: false,\n name: remainder,\n registry,\n tag,\n };\n};\n\n/**\n * Docker Hardened Images (free catalog since Dec 2025) are pulled from the\n * dhi.io registry. Enterprise mirrors live under a plain Docker Hub org\n * namespace and cannot be recognized from the ref alone, so they keep the\n * default rule behavior.\n */\nexport const isHardenedImage = (imagePart: string): boolean =>\n imagePart.toLowerCase().startsWith(\"dhi.io/\");\n\n/**\n * DHI runtime variants ship no shell or package manager and run as a\n * nonroot user by default. The `-dev` variants keep a shell for build\n * stages and are not assumed to be nonroot.\n */\nexport const isHardenedRuntimeImage = (imagePart: string): boolean => {\n if (!isHardenedImage(imagePart)) {\n return false;\n }\n const { tag } = parseImageRef(imagePart);\n return !(tag === \"dev\" || tag?.endsWith(\"-dev\"));\n};\n\n/**\n * Why a reference would resolve differently over time: no tag at all, or\n * the mutable `latest` tag without a digest. `undefined` means the ref is\n * pinned. Shared by every pinning rule (base images, service images,\n * models) so they agree on what counts as pinned.\n */\nexport const mutableRefIssue = (\n ref: ImageRef\n): \"untagged\" | \"latest\" | undefined => {\n if (!(ref.tag || ref.digest)) {\n return \"untagged\";\n }\n if (ref.tag === \"latest\" && !ref.digest) {\n return \"latest\";\n }\n return undefined;\n};\n\nexport interface FromArgs {\n base: string | null;\n stage: string | null;\n}\n\n// FROM [--flags] <image|stage> [AS <stage>], flags in any position.\nexport const parseFromArgs = (args: string): FromArgs => {\n const parts = args.split(/\\s+/u).filter(Boolean);\n const asIndex = parts.findIndex((p) => p.toLowerCase() === \"as\");\n const imageParts = asIndex === -1 ? parts : parts.slice(0, asIndex);\n return {\n base: imageParts.find((p) => !p.startsWith(\"--\")) ?? null,\n stage: asIndex === -1 ? null : (parts[asIndex + 1] ?? null),\n };\n};\n\n// The reserved empty base, not a real image: nothing to pin a tag on, no\n// distribution to slim down, and no image config to inherit. One definition\n// so the rules cannot disagree about a given FROM line.\nexport const isScratch = (base: string | null): boolean =>\n base?.toLowerCase() === \"scratch\";\n\nexport const collectStageAliases = (\n instructions: DockerfileInstruction[]\n): Set<string> => {\n const aliases = new Set<string>();\n\n for (const inst of instructions) {\n if (inst.instruction !== \"FROM\") {\n continue;\n }\n\n const { stage } = parseFromArgs(inst.args);\n if (stage) {\n aliases.add(stage.toLowerCase());\n }\n }\n\n return aliases;\n};\n","import type { Diagnostic, DiagnosticSeverity } from \"../types/index\";\n\nexport const createDiagnostic = (\n file: string,\n ruleKey: string,\n severity: DiagnosticSeverity,\n message: string,\n help: string,\n line?: number\n): Diagnostic => ({ file, help, line, message, rule: ruleKey, severity });\n","// `apt-get -y --no-install-recommends install curl` is as common as\n// `apt-get install -y curl`, so options may sit between the command and the\n// subcommand. Known limitation: an option value written as a separate word\n// (`-o Key=Value`) does not start with `-` and breaks the match.\nconst APT_GET_INSTALL_RE = /\\bapt-get(?:\\s+-\\S+)*\\s+install\\b/u;\nconst APT_GET_UPDATE_RE = /\\bapt-get(?:\\s+-\\S+)*\\s+update\\b/u;\n\nexport const hasAptGetInstall = (text: string): boolean =>\n APT_GET_INSTALL_RE.test(text);\n\nexport const hasAptGetUpdate = (text: string): boolean =>\n APT_GET_UPDATE_RE.test(text);\n","import { parseExecForm } from \"../parsers/exec-form\";\nimport { isScratch, parseFromArgs } from \"../parsers/image-ref\";\nimport { maskQuotedText } from \"../parsers/shell-quotes\";\nimport type { Diagnostic, DockerfileRule } from \"../types/index\";\nimport { createDiagnostic } from \"./create-diagnostic\";\nimport { hasAptGetInstall, hasAptGetUpdate } from \"./package-managers\";\n\nexport const requireHealthcheck: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const hasHealthcheck = instructions.some(\n (inst) => inst.instruction === \"HEALTHCHECK\"\n );\n\n // Only suggest healthcheck if it has exposed ports or command lines indicating it runs an app\n const hasExposedPortsOrEntry = instructions.some(\n (inst) =>\n inst.instruction === \"EXPOSE\" ||\n inst.instruction === \"CMD\" ||\n inst.instruction === \"ENTRYPOINT\"\n );\n\n if (!hasHealthcheck && hasExposedPortsOrEntry) {\n return [\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n \"No HEALTHCHECK instruction found. Containers running services should expose healthchecks to enable auto-healing.\",\n this.help,\n 1\n ),\n ];\n }\n return [];\n },\n defaultSeverity: \"info\",\n help: \"Use HEALTHCHECK (e.g., `HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost/ || exit 1`) so Docker can monitor the container's live status.\",\n key: \"docker-doctor/require-healthcheck\",\n message: \"Add a HEALTHCHECK instruction\",\n};\n\n// ADD auto-extracts local tar archives (identity, gzip, bzip2, xz). Zip files\n// are copied verbatim, so COPY is the right instruction for them.\nconst AUTO_EXTRACT_SUFFIXES = [\n \".tar\",\n \".tar.gz\",\n \".tgz\",\n \".tar.bz2\",\n \".tbz2\",\n \".tbz\",\n \".tar.xz\",\n \".txz\",\n] as const;\n\nexport const preferCopyOverAdd: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n\n for (const inst of instructions) {\n if (inst.instruction === \"ADD\") {\n const parts = inst.args.split(/\\s+/u);\n const src = parts.find((p) => !p.startsWith(\"--\"));\n\n if (!src) {\n continue;\n }\n\n // If it's not a remote url (handled by security/no-add-remote) and not a compressed file\n const isRemote =\n src.startsWith(\"http://\") || src.startsWith(\"https://\");\n const isArchive = AUTO_EXTRACT_SUFFIXES.some((suffix) =>\n src.toLowerCase().endsWith(suffix)\n );\n\n if (!isRemote && !isArchive) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n `ADD instruction used for regular files: '${inst.args}'. COPY is simpler and less prone to magic side effects.`,\n this.help,\n inst.line\n )\n );\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Use COPY instead of ADD unless you explicitly need auto-extraction of local compressed archives (tar, zip, etc.).\",\n key: \"docker-doctor/prefer-copy-over-add\",\n message: \"Prefer COPY over ADD\",\n};\n\nexport const useExecForm: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n\n for (const inst of instructions) {\n const takesExecForm =\n inst.instruction === \"CMD\" || inst.instruction === \"ENTRYPOINT\";\n // Bracket-wrapped args that are not a JSON string array still run under\n // /bin/sh -c, so they count as shell form.\n if (takesExecForm && parseExecForm(inst.args) === null) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n `${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.`,\n this.help,\n inst.line\n )\n );\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: 'Write CMD/ENTRYPOINT instructions as JSON arrays (e.g. `ENTRYPOINT [\"node\", \"index.js\"]`) so OS signals (like SIGTERM) are forwarded correctly.',\n key: \"docker-doctor/use-exec-form\",\n message: \"Use exec form for CMD and ENTRYPOINT\",\n};\n\nexport const requireLabels: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const hasLabel = instructions.some((inst) => inst.instruction === \"LABEL\");\n if (!hasLabel) {\n return [\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n \"No LABEL metadata was found in this Dockerfile. Adding labels helps identify build information, maintainers, and descriptions.\",\n this.help,\n 1\n ),\n ];\n }\n return [];\n },\n defaultSeverity: \"info\",\n help: 'Use LABEL instructions (e.g. `LABEL org.opencontainers.image.authors=\"...\"`) to document ownership, license, version, and build info.',\n key: \"docker-doctor/require-labels\",\n message: \"Add LABEL metadata to images\",\n};\n\nexport const combineAptUpdateInstall: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n for (const inst of instructions) {\n if (inst.instruction === \"RUN\") {\n const hasUpdate = hasAptGetUpdate(inst.args);\n const hasInstall = hasAptGetInstall(inst.args);\n\n if (hasUpdate && !hasInstall) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n \"RUN apt-get update used without apt-get install in the same instruction. This can cause caching issues and build failures.\",\n this.help,\n inst.line\n )\n );\n } else if (hasInstall && !hasUpdate) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n \"RUN apt-get install used without apt-get update in the same instruction. Always combine them to ensure up-to-date package installation.\",\n this.help,\n inst.line\n )\n );\n }\n }\n }\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Combine `apt-get update` and `apt-get install` in the same RUN instruction (e.g. `RUN apt-get update && apt-get install -y --no-install-recommends <package> && rm -rf /var/lib/apt/lists/*`).\",\n key: \"docker-doctor/combine-apt-update-install\",\n message: \"Combine apt-get update and apt-get install\",\n};\n\n// An actual pipefail setting: the option must appear adjacent to its flag\n// (`-o pipefail`, `-euo pipefail`, repeated `-o errexit -o pipefail`, or a\n// shell invoked with `-o pipefail`). Matching the flag directly (rather than\n// a shell name) also works inside joined exec-form argv, and cannot backtrack\n// pathologically. Known limitation: the word inside quotes (e.g.\n// `echo \"set -o pipefail\" >> .bashrc`) still matches — same class as the\n// existing test.todo for quoted pipes.\nexport const PIPEFAIL_SETTING_RE = /(?:^|\\s)-[A-Za-z]*o\\s+pipefail\\b/u;\n\n// A RUN line uses a pipe: a single `|` not part of `||`.\nconst HAS_PIPE_RE = /(?<!\\|)\\|(?!\\|)/u;\n\n// `cd` only where a command can start: line start, or after a list/pipe\n// separator or a subshell open. Path segments (/opt/cd) and words never sit\n// in command position.\nconst CD_COMMAND_RE = /(?:^|&&|\\|\\||;|\\||\\(|`|\\$\\()\\s*cd(?=\\s|$)/u;\n\n// SHELL takes exec form; joined, its argv is the prefix every shell-form RUN\n// is wrapped in (e.g. /bin/bash -o pipefail -c). Any other spelling is\n// rejected by Docker, so it cannot be enabling pipefail.\nconst shellDirectiveEnablesPipefail = (args: string): boolean => {\n const argv = parseExecForm(args);\n return argv !== null && PIPEFAIL_SETTING_RE.test(argv.join(\" \"));\n};\n\nexport const usePipefail: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n // FROM <previous stage> inherits that stage's image config — SHELL\n // included (verified against BuildKit); FROM a fresh base image, or the\n // reserved empty stage `scratch`, resets it to the Docker default. A\n // SHELL instruction replaces it for the rest of the current stage.\n // NOTE: FROM ${VAR} (variable base) misses the map lookup and silently\n // resets — fails safe (extra warning, never a missed one).\n const stagePipefail = new Map<string, boolean>();\n let shellHasPipefail = false;\n let currentStage: string | null = null;\n for (const inst of instructions) {\n if (inst.instruction === \"FROM\") {\n const { base, stage } = parseFromArgs(inst.args);\n // Only a reference to an earlier stage carries state forward; a real\n // base image, scratch, or a variable we cannot resolve resets it.\n const parentStage = isScratch(base)\n ? null\n : (base?.toLowerCase() ?? null);\n shellHasPipefail =\n parentStage !== null && stagePipefail.get(parentStage) === true;\n currentStage = stage?.toLowerCase() ?? null;\n if (currentStage) {\n stagePipefail.set(currentStage, shellHasPipefail);\n }\n continue;\n }\n if (inst.instruction === \"SHELL\") {\n shellHasPipefail = shellDirectiveEnablesPipefail(inst.args);\n if (currentStage) {\n stagePipefail.set(currentStage, shellHasPipefail);\n }\n continue;\n }\n if (inst.instruction !== \"RUN\") {\n continue;\n }\n const { args } = inst;\n // SHELL only applies to shell-form RUN; exec-form RUN runs its own\n // argv directly, so check the argv for pipefail instead. Exec-form\n // argv elements arrive already unquoted, so a pipe inside them is a\n // real pipe; shell-form text is masked so a `|` inside a quoted\n // argument (grep -E \"foo|bar\") does not read as a pipeline.\n const execArgv = parseExecForm(args);\n const pipeText =\n execArgv === null ? maskQuotedText(args) : execArgv.join(\" \");\n if (!HAS_PIPE_RE.test(pipeText)) {\n continue;\n }\n const pipefailConfigured =\n execArgv === null\n ? shellHasPipefail || PIPEFAIL_SETTING_RE.test(args)\n : PIPEFAIL_SETTING_RE.test(execArgv.join(\" \"));\n if (!pipefailConfigured) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n \"RUN instruction uses a pipe (|) but does not configure 'pipefail'. If a command in the pipe fails, the step may still succeed silently.\",\n this.help,\n inst.line\n )\n );\n }\n }\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: 'Prepend `set -o pipefail &&` to pipe commands, use exec form with a shell that supports it (e.g., `RUN [\"/bin/bash\", \"-c\", \"set -o pipefail && ...\"]`), or set `SHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]` at the top of the stage.',\n key: \"docker-doctor/use-pipefail\",\n message: \"Use pipefail to catch pipeline command failures\",\n};\n\nexport const absoluteWorkdir: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n for (const inst of instructions) {\n if (inst.instruction === \"WORKDIR\") {\n const path = inst.args.trim();\n const isAbsolute = /^(?:\\/|\\\\|\\$|[a-zA-Z]:)/u.test(path);\n\n if (!isAbsolute) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n `WORKDIR specifies a relative path '${path}'. For clarity and reliability, always use absolute paths.`,\n this.help,\n inst.line\n )\n );\n }\n }\n }\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Always specify absolute paths for WORKDIR instructions (e.g. `WORKDIR /app`).\",\n key: \"docker-doctor/absolute-workdir\",\n message: \"Use absolute paths for WORKDIR\",\n};\n\nexport const avoidRunCd: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n for (const inst of instructions) {\n // Exec-form RUN [\"sh\", \"-c\", \"cd /app && make\"] keeps its whole\n // command inside quotes and is intentionally not flagged: this rule is\n // about shell-form layering.\n if (\n inst.instruction === \"RUN\" &&\n CD_COMMAND_RE.test(maskQuotedText(inst.args))\n ) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n \"Avoid using 'cd' in RUN instructions. Use WORKDIR instead to change the working directory stably across layers.\",\n this.help,\n inst.line\n )\n );\n }\n }\n return diagnostics;\n },\n defaultSeverity: \"info\",\n help: \"Use the WORKDIR instruction instead of `cd` inside RUN to establish directory context.\",\n key: \"docker-doctor/avoid-run-cd\",\n message: \"Avoid changing directories with cd in RUN\",\n};\n\nconst PACKAGE_LIST_START_RE =\n /\\b(?:apt-get(?:\\s+-\\S+)*\\s+install|apk\\s+add|yum\\s+install|dnf\\s+install)\\b/u;\nconst SHELL_SEPARATOR_RE = /[;|]|&&/u;\nconst WHITESPACE_RE = /\\s+/u;\n\n// The words after the install verb up to the next shell separator, minus\n// options and continuation backslashes. Everything past a separator belongs\n// to another command, so it is not part of the list.\nconst collectPackageList = (raw: string): string[] => {\n const start = PACKAGE_LIST_START_RE.exec(raw);\n if (!start) {\n return [];\n }\n const packages: string[] = [];\n const tokens = raw.slice(start.index + start[0].length).split(WHITESPACE_RE);\n for (const token of tokens) {\n if (SHELL_SEPARATOR_RE.test(token)) {\n break;\n }\n if (token !== \"\" && token !== \"\\\\\" && !token.startsWith(\"-\")) {\n packages.push(token);\n }\n }\n return packages;\n};\n\nexport const sortMultilineArgs: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n for (const inst of instructions) {\n if (inst.instruction === \"RUN\") {\n const { raw } = inst;\n const isPackageInstall =\n hasAptGetInstall(raw) ||\n raw.includes(\"apk add\") ||\n raw.includes(\"yum install\") ||\n raw.includes(\"dnf install\");\n\n const hasContinuation = raw.includes(\"\\\\\\n\") || raw.includes(\"\\\\\\r\\n\");\n\n if (isPackageInstall && hasContinuation) {\n const packages = collectPackageList(raw);\n\n if (packages.length > 1) {\n // The default comparator orders by code unit, so the verdict does\n // not depend on the machine's locale.\n const sorted = packages.toSorted();\n const isSorted = packages.every((val, idx) => val === sorted[idx]);\n if (!isSorted) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n \"Multi-line package arguments are not sorted alphanumerically. Keeping them sorted makes maintenance easier and prevents duplicates.\",\n this.help,\n inst.line\n )\n );\n }\n }\n }\n }\n }\n return diagnostics;\n },\n defaultSeverity: \"info\",\n help: \"Sort multi-line package installation lists (e.g. apk/apt package lists) alphabetically.\",\n key: \"docker-doctor/sort-multiline-args\",\n message: \"Sort multi-line arguments alphanumerically\",\n};\n\nexport const useraddNoLogInit: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n for (const inst of instructions) {\n if (\n inst.instruction === \"RUN\" &&\n /\\buseradd\\b/u.test(inst.args) &&\n !inst.args.includes(\"--no-log-init\")\n ) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n \"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.\",\n this.help,\n inst.line\n )\n );\n }\n }\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Pass `--no-log-init` flag to useradd (e.g., `RUN useradd --no-log-init -r -g mygroup myuser`).\",\n key: \"docker-doctor/useradd-no-log-init\",\n message: \"Use --no-log-init with useradd\",\n};\n\nexport const bestPracticesRules = [\n requireHealthcheck,\n preferCopyOverAdd,\n useExecForm,\n requireLabels,\n combineAptUpdateInstall,\n usePipefail,\n absoluteWorkdir,\n avoidRunCd,\n sortMultilineArgs,\n useraddNoLogInit,\n];\n","/**\n * Narrows an unknown compose document to its service entries. A service\n * with a null body (`web:` with nothing under it) is returned as an empty\n * config so rules still check it: it is the least-configured service in\n * the file, not a service to skip. Scalar and array bodies are invalid\n * compose and are dropped. A `services:` written as a YAML sequence is\n * invalid Compose and yields no services.\n */\nexport const composeServices = (\n composeContent: unknown\n): [string, Record<string, unknown>][] => {\n if (\n !composeContent ||\n typeof composeContent !== \"object\" ||\n !(\"services\" in composeContent)\n ) {\n return [];\n }\n const { services } = composeContent as { services?: unknown };\n if (!services || typeof services !== \"object\" || Array.isArray(services)) {\n return [];\n }\n\n const entries: [string, Record<string, unknown>][] = [];\n for (const [name, config] of Object.entries(services)) {\n if (config === null || config === undefined) {\n entries.push([name, {}]);\n } else if (typeof config === \"object\" && !Array.isArray(config)) {\n entries.push([name, config as Record<string, unknown>]);\n }\n }\n return entries;\n};\n","import { mutableRefIssue, parseImageRef } from \"../parsers/image-ref\";\nimport type { ComposeRule, Diagnostic } from \"../types/index\";\nimport { composeServices } from \"./compose-services\";\nimport { createDiagnostic } from \"./create-diagnostic\";\n\nexport const noVersionKey: ComposeRule = {\n category: \"Compose\",\n check(composeContent, file, context) {\n if (\n composeContent &&\n typeof composeContent === \"object\" &&\n \"version\" in composeContent\n ) {\n return [\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n \"The 'version' property is deprecated. Remove it to use standard Compose spec behavior.\",\n this.help,\n context?.locate?.([\"version\"])\n ),\n ];\n }\n return [];\n },\n defaultSeverity: \"warning\",\n help: \"The `version` key is obsolete in the Compose specification. Omitting it defaults to the latest specification.\",\n key: \"docker-doctor/no-version-key\",\n message: \"Remove the `version` key from the Compose file\",\n};\n\nexport const requireResourceLimits: ComposeRule = {\n category: \"Compose\",\n check(composeContent, file, context) {\n const diagnostics: Diagnostic[] = [];\n\n for (const [name, config] of composeServices(composeContent)) {\n const deploy = config.deploy as Record<string, unknown> | undefined;\n const resources = deploy?.resources as\n | Record<string, unknown>\n | undefined;\n const limits = resources?.limits as Record<string, unknown> | undefined;\n const hasDeployLimits = Boolean(limits?.cpus || limits?.memory);\n // Pre-`deploy` spelling of the same limits, still in the spec.\n const hasServiceLevelLimits = Boolean(\n config.mem_limit || config.cpus || config.cpu_quota\n );\n\n if (!(hasDeployLimits || hasServiceLevelLimits)) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n `Service '${name}' does not have CPU or memory limits defined. A resource leak in this service could crash the host.`,\n this.help,\n context?.locate?.([\"services\", name])\n )\n );\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Add resource limits (e.g. `deploy.resources.limits`) to prevent a single service from starving host resources in production.\",\n key: \"docker-doctor/require-resource-limits\",\n message: \"Define resource limits for services\",\n};\n\nexport const requireRestartPolicy: ComposeRule = {\n category: \"Compose\",\n check(composeContent, file, context) {\n const diagnostics: Diagnostic[] = [];\n\n for (const [name, config] of composeServices(composeContent)) {\n const hasRestart = \"restart\" in config;\n const deploy = config.deploy as Record<string, unknown> | undefined;\n const hasDeployRestart = deploy?.restart_policy !== undefined;\n\n if (!hasRestart && !hasDeployRestart) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n `Service '${name}' has no restart policy configured. It will not restart if it crashes or if the host reboots.`,\n this.help,\n context?.locate?.([\"services\", name])\n )\n );\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Define `restart: always` or `restart: unless-stopped` (or `deploy.restart_policy`) so services restart on crashes or host reboot.\",\n key: \"docker-doctor/require-restart-policy\",\n message: \"Set restart policy for services\",\n};\n\nexport const useDependsOnCondition: ComposeRule = {\n category: \"Compose\",\n check(composeContent, file, context) {\n const diagnostics: Diagnostic[] = [];\n\n for (const [name, config] of composeServices(composeContent)) {\n const dependsOn = config.depends_on;\n if (dependsOn && Array.isArray(dependsOn)) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n `Service '${name}' uses shorthand depends_on list. This only checks if containers are started, not if they are ready/healthy.`,\n this.help,\n context?.locate?.([\"services\", name, \"depends_on\"]) ??\n context?.locate?.([\"services\", name])\n )\n );\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"info\",\n help: \"Instead of a simple service list, use `depends_on: { dependency: { condition: service_healthy } }` to ensure dependencies are fully ready before starting.\",\n key: \"docker-doctor/use-depends-on-condition\",\n message: \"Use long-form depends_on with healthcheck conditions\",\n};\n\nexport const pinServiceImage: ComposeRule = {\n category: \"Compose\",\n check(composeContent, file, context) {\n const diagnostics: Diagnostic[] = [];\n\n for (const [name, config] of composeServices(composeContent)) {\n const { image } = config;\n // With `build:` present, `image` only names the locally built\n // artifact — there is nothing to pin.\n if (typeof image !== \"string\" || \"build\" in config) {\n continue;\n }\n\n const ref = parseImageRef(image);\n if (ref.isVariable) {\n continue;\n }\n\n const issue = mutableRefIssue(ref);\n if (!issue) {\n continue;\n }\n const detail =\n issue === \"untagged\"\n ? `Service '${name}' image '${image}' does not specify a tag.`\n : `Service '${name}' image '${image}' uses the mutable 'latest' tag.`;\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n `${detail} Every pull may fetch a different image.`,\n this.help,\n context?.locate?.([\"services\", name, \"image\"])\n )\n );\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Pin the image to a specific tag or digest (e.g. `nginx:1.27-alpine`) so deploys are reproducible and a rollback actually rolls back.\",\n key: \"docker-doctor/pin-service-image\",\n message: \"Pin service images to a specific tag or digest\",\n};\n\nexport const composeRules = [\n noVersionKey,\n requireResourceLimits,\n requireRestartPolicy,\n useDependsOnCondition,\n pinServiceImage,\n];\n","import { mutableRefIssue, parseImageRef } from \"../parsers/image-ref\";\nimport type { ComposeRule, Diagnostic } from \"../types/index\";\nimport { composeServices } from \"./compose-services\";\nimport { createDiagnostic } from \"./create-diagnostic\";\n\n/**\n * Narrows an unknown compose document to its top-level `models:` entries\n * (Docker Model Runner, Compose ≥ 2.35).\n */\nconst topLevelModels = (composeContent: unknown): Record<string, unknown> => {\n if (\n !composeContent ||\n typeof composeContent !== \"object\" ||\n !(\"models\" in composeContent)\n ) {\n return {};\n }\n const { models } = composeContent as { models?: unknown };\n if (!models || typeof models !== \"object\" || Array.isArray(models)) {\n return {};\n }\n return models as Record<string, unknown>;\n};\n\nexport const undefinedModelReference: ComposeRule = {\n category: \"Compose\",\n check(composeContent, file, context) {\n const diagnostics: Diagnostic[] = [];\n const defined = new Set(Object.keys(topLevelModels(composeContent)));\n\n const flag = (\n serviceName: string,\n modelName: string,\n pathTail: string | number\n ) => {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n `Service '${serviceName}' references model '${modelName}', which is not declared in the top-level models section. Compose cannot resolve it.`,\n this.help,\n context?.locate?.([\"services\", serviceName, \"models\", pathTail])\n )\n );\n };\n\n for (const [name, config] of composeServices(composeContent)) {\n const { models } = config;\n if (Array.isArray(models)) {\n // Short syntax: a list of model names.\n for (const [index, entry] of models.entries()) {\n if (typeof entry === \"string\" && !defined.has(entry)) {\n flag(name, entry, index);\n }\n }\n } else if (models && typeof models === \"object\") {\n // Long syntax: a map of model name → binding config.\n for (const modelName of Object.keys(models)) {\n if (!defined.has(modelName)) {\n flag(name, modelName, modelName);\n }\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"error\",\n help: \"Every name under a service's `models:` must match an entry in the top-level `models:` element. Declare the model there (with its `model:` OCI artifact) or fix the reference.\",\n key: \"docker-doctor/undefined-model-reference\",\n message: \"Service model references must be declared in top-level models\",\n};\n\ninterface ModelBinding {\n subject: string;\n model: string;\n path: (string | number)[];\n}\n\n// Top-level `models:` (Compose ≥ 2.38) plus the older service-level\n// `provider: { type: model }` form (Compose ≥ 2.35).\nconst collectModelBindings = (composeContent: unknown): ModelBinding[] => {\n const bindings: ModelBinding[] = [];\n\n for (const [name, config] of Object.entries(topLevelModels(composeContent))) {\n if (!config || typeof config !== \"object\") {\n continue;\n }\n const { model } = config as Record<string, unknown>;\n if (typeof model === \"string\") {\n bindings.push({\n model,\n path: [\"models\", name, \"model\"],\n subject: `Model '${name}' artifact`,\n });\n }\n }\n\n for (const [name, config] of composeServices(composeContent)) {\n const { provider } = config;\n if (!provider || typeof provider !== \"object\") {\n continue;\n }\n const { type, options } = provider as Record<string, unknown>;\n if (type !== \"model\" || !options || typeof options !== \"object\") {\n continue;\n }\n const { model } = options as Record<string, unknown>;\n if (typeof model === \"string\") {\n bindings.push({\n model,\n path: [\"services\", name, \"provider\", \"options\", \"model\"],\n subject: `Service '${name}' model provider`,\n });\n }\n }\n\n return bindings;\n};\n\nexport const pinModelVersion: ComposeRule = {\n category: \"Compose\",\n check(composeContent, file, context) {\n const diagnostics: Diagnostic[] = [];\n\n for (const { subject, model, path } of collectModelBindings(\n composeContent\n )) {\n const ref = parseImageRef(model);\n if (ref.isVariable) {\n continue;\n }\n\n const issue = mutableRefIssue(ref);\n if (!issue) {\n continue;\n }\n const detail =\n issue === \"untagged\"\n ? `${subject} '${model}' does not specify a tag.`\n : `${subject} '${model}' uses the mutable 'latest' tag.`;\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n `${detail} Every pull may fetch different weights.`,\n this.help,\n context?.locate?.(path)\n )\n );\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Pin the model to a specific tag (e.g. `ai/gemma3:4B-Q4_0`) so every environment runs the same weights. Model behavior differences are far harder to debug than software version drift.\",\n key: \"docker-doctor/pin-model-version\",\n message: \"Pin models to a specific tag or digest\",\n};\n\nexport const composeModelRules = [undefinedModelReference, pinModelVersion];\n","import path from \"node:path\";\n\n/**\n * Shared parsing for Compose `volumes:` entries, used by every rule that\n * reasons about bind mounts. A Compose volume entry is either the short\n * `[SOURCE:]TARGET[:MODE]` string or the long-syntax object; both collapse\n * to a `VolumeMount`.\n */\n\n// `${VAR:-default}` resolves to its default; `${VAR}`, `$VAR` and the\n// `?error` forms resolve to nothing.\nconst INTERPOLATION_WITH_DEFAULT = /\\$\\{[^}:?-]+:?-(?<fallback>[^}]*)\\}/gu;\nconst INTERPOLATION_WITHOUT_DEFAULT = /\\$\\{[^}]*\\}|\\$[A-Za-z_][A-Za-z0-9_]*/gu;\n// `$HOME` and `${HOME}` name the same directory `~` does; fold them before\n// the generic interpolation pass would erase them.\nconst HOME_INTERPOLATION = /\\$\\{HOME\\}|\\$HOME(?![A-Za-z0-9_])/gu;\n// Compose runs from the project directory, so `$PWD` and `${PWD}` name it.\nconst PWD_INTERPOLATION = /\\$\\{PWD\\}|\\$PWD(?![A-Za-z0-9_])/gu;\n// A source that starts with an unresolved variable, e.g. `${DATA_DIR}/pg`.\nconst LEADING_INTERPOLATION_RE = /^\\$(?:\\{[^}]*\\}|[A-Za-z_][A-Za-z0-9_]*)/u;\n\n// Folds `$HOME`/`$PWD` and applies `${VAR:-default}` defaults, but leaves\n// interpolations with no default in place.\nconst applyFoldsAndDefaults = (value: string): string =>\n value\n .replace(HOME_INTERPOLATION, \"~\")\n .replace(PWD_INTERPOLATION, \".\")\n .replace(\n INTERPOLATION_WITH_DEFAULT,\n (_match, fallback: string) => fallback\n );\n\nexport const resolveInterpolationDefaults = (value: string): string =>\n applyFoldsAndDefaults(value).replace(INTERPOLATION_WITHOUT_DEFAULT, \"\");\n\n// Named volumes cannot start with a path prefix, so only path-shaped\n// sources are bind mounts.\nexport const PATH_SHAPED = /^(?:[/.~]|[A-Za-z]:)/u;\n\n// `[SOURCE:]TARGET[:MODE]`, where a `${VAR:-default}` source has its own colon.\nconst splitShortSyntax = (spec: string): string[] => {\n const parts: string[] = [];\n let depth = 0;\n let current = \"\";\n for (const char of spec) {\n if (char === \"{\") {\n depth += 1;\n } else if (char === \"}\") {\n depth = Math.max(0, depth - 1);\n }\n if (char === \":\" && depth === 0) {\n parts.push(current);\n current = \"\";\n continue;\n }\n current += char;\n }\n parts.push(current);\n return parts;\n};\n\n// Windows drive-letter paths also carry a colon; `C:\\data:/data` splits into\n// `C`, `\\data`, `/data`, so re-join a lone drive letter with what follows.\nconst DRIVE_LETTER = /^[A-Za-z]$/u;\n\nconst joinDriveLetter = (parts: string[]): string[] => {\n const [first, second, ...rest] = parts;\n if (\n first !== undefined &&\n second !== undefined &&\n DRIVE_LETTER.test(first) &&\n /^[\\\\/]/u.test(second)\n ) {\n return [`${first}:${second}`, ...rest];\n }\n return parts;\n};\n\nexport interface VolumeMount {\n source: string | undefined;\n target: string | undefined;\n /** `:ro` in the short syntax, `read_only: true` in the long syntax. */\n readOnly: boolean;\n /** Long-syntax `type`; the short syntax leaves it undefined. */\n type: string | undefined;\n}\n\nconst shortSyntaxReadOnly = (mode: string | undefined): boolean =>\n mode !== undefined && mode.split(\",\").includes(\"ro\");\n\nexport const volumeMount = (volume: unknown): VolumeMount | undefined => {\n if (typeof volume === \"string\") {\n const [source, target, mode] = joinDriveLetter(splitShortSyntax(volume));\n // A lone path is an anonymous volume at that target, not a bind mount.\n if (target === undefined) {\n return undefined;\n }\n return {\n readOnly: shortSyntaxReadOnly(mode),\n source,\n target,\n type: undefined,\n };\n }\n if (volume && typeof volume === \"object\") {\n const {\n read_only: readOnly,\n source,\n target,\n type,\n } = volume as Record<string, unknown>;\n return {\n readOnly: readOnly === true,\n source: typeof source === \"string\" ? source : undefined,\n target: typeof target === \"string\" ? target : undefined,\n type: typeof type === \"string\" ? type : undefined,\n };\n }\n return undefined;\n};\n\n/**\n * The host path a bind mount reads from, with interpolation defaults\n * applied and Windows separators folded to `/`. Undefined when the entry is\n * not a bind mount (named volume, tmpfs, anonymous volume) or when the\n * source starts with an interpolation that has no default. A source such as\n * `${DATA_DIR}/pg` names no known host location, so no rule can say whether\n * it is inside the project.\n */\nexport const bindMountSource = (mount: VolumeMount): string | undefined => {\n if (mount.type !== undefined && mount.type !== \"bind\") {\n return undefined;\n }\n if (!mount.source) {\n return undefined;\n }\n const withDefaults = applyFoldsAndDefaults(mount.source);\n if (LEADING_INTERPOLATION_RE.test(withDefaults)) {\n return undefined;\n }\n const resolved = withDefaults\n .replace(INTERPOLATION_WITHOUT_DEFAULT, \"\")\n .replaceAll(\"\\\\\", \"/\");\n if (resolved === \"\" || !PATH_SHAPED.test(resolved)) {\n return undefined;\n }\n return resolved;\n};\n\nconst isDockerSocketPath = (hostPath: string): boolean =>\n hostPath.endsWith(\"/docker.sock\") || hostPath.endsWith(\"/pipe/docker_engine\");\n\nconst DOCKER_SOCKET_TARGET = \"/var/run/docker.sock\";\n\n/**\n * Whether the entry bind-mounts the Docker socket, under any of its host\n * spellings. A source that starts with `${VAR}` names no known host path,\n * but a `/docker.sock` suffix or a socket target still identifies it.\n */\nexport const mountsDockerSocket = (mount: VolumeMount): boolean => {\n const source = bindMountSource(mount);\n if (source !== undefined) {\n return isDockerSocketPath(source);\n }\n if (mount.type !== undefined && mount.type !== \"bind\") {\n return false;\n }\n if (!mount.source) {\n return false;\n }\n const remainder = resolveInterpolationDefaults(mount.source);\n return (\n isDockerSocketPath(remainder) ||\n (remainder === \"\" &&\n resolveInterpolationDefaults(mount.target ?? \"\") === DOCKER_SOCKET_TARGET)\n );\n};\n\nconst WINDOWS_DRIVE_ROOT = /^[A-Za-z]:\\/?$/u;\nconst WINDOWS_DRIVE_PREFIX = /^[A-Za-z]:/u;\n\nexport type HostPathScope =\n | \"project\"\n | \"parent\"\n | \"home\"\n | \"home-dotfile\"\n | \"root\"\n | \"system\"\n | \"host\";\n\n// Absolute directories that hold host configuration, credentials, process\n// state, or the kernel's own interfaces. A container that can see them can\n// read more than the service it runs.\nconst SYSTEM_DIRECTORIES = [\n \"/etc\",\n \"/root\",\n \"/home\",\n \"/Users\",\n \"/proc\",\n \"/sys\",\n \"/boot\",\n \"/run\",\n \"/var/run\",\n] as const;\n\nconst startsWithDirectory = (hostPath: string, directory: string): boolean =>\n hostPath === directory || hostPath.startsWith(`${directory}/`);\n\n/**\n * Classifies where on the host a bind-mount source points. `project` is a\n * relative path that stays inside the Compose project directory; everything\n * else reaches outside it.\n */\nexport const classifyHostPath = (hostPath: string): HostPathScope => {\n const collapsed = path.posix.normalize(hostPath);\n const normalized =\n collapsed.length > 1 ? collapsed.replace(/\\/+$/u, \"\") : collapsed;\n\n if (normalized === \"~\") {\n return \"home\";\n }\n if (normalized.startsWith(\"~/\")) {\n return normalized.startsWith(\"~/.\") ? \"home-dotfile\" : \"host\";\n }\n if (normalized === \".\" || normalized.startsWith(\"./\")) {\n return \"project\";\n }\n if (normalized === \"..\" || normalized.startsWith(\"../\")) {\n return \"parent\";\n }\n if (WINDOWS_DRIVE_ROOT.test(normalized)) {\n return \"root\";\n }\n if (WINDOWS_DRIVE_PREFIX.test(normalized)) {\n return \"host\";\n }\n if (normalized === \"/\") {\n return \"root\";\n }\n if (!normalized.startsWith(\"/\")) {\n // `path.posix.normalize(\"./src\")` yields `src`.\n return \"project\";\n }\n for (const directory of SYSTEM_DIRECTORIES) {\n if (startsWithDirectory(normalized, directory)) {\n return \"system\";\n }\n }\n return \"host\";\n};\n","// Shared between the Dockerfile ENV/ARG rule (no-secrets-in-env) and the\n// Compose environment rule (no-plaintext-secrets) so both flag the same\n// key shapes and the same value shapes.\nconst SECRET_KEY_PATTERNS: readonly RegExp[] = [\n // No leading separator, for PGPASSWORD.\n /password(?:[_-]|$)/iu,\n /(?:^|[_-])secret(?:[_-]|$)/iu,\n /(?:^|[_-])token(?:[_-]|$)/iu,\n /(?:^|[_-])api_key(?:[_-]|$)/iu,\n /(?:^|[_-])apikey(?:[_-]|$)/iu,\n /(?:^|[_-])private_key(?:[_-]|$)/iu,\n /(?:^|[_-])auth(?:[_-]|$)/iu,\n // GITHUB_PAT; the trailing separator keeps PATH and PATTERN out.\n /(?:^|[_-])pat(?:[_-]|$)/iu,\n];\n\nexport const isSecretKey = (key: string): boolean =>\n SECRET_KEY_PATTERNS.some((regex) => regex.test(key));\n\n// A URL with no userinfo in its authority. `user:pw@host` does not match.\nconst CREDENTIAL_FREE_URL = /^[a-z][a-z0-9+.-]*:\\/\\/[^@/\\s]*(?:\\/|$)/iu;\n\n// `ENV TOKEN=\"\"` / `ARG KEY=''` declare a build-time secret with no value;\n// the quotes are shell syntax, not a literal.\nconst SURROUNDING_QUOTES_RE = /^(?<quote>[\"'])(?<inner>.*)\\k<quote>$/su;\n\nexport const isLiteralSecretValue = (value: string): boolean => {\n const unquoted = value.match(SURROUNDING_QUOTES_RE)?.groups?.inner ?? value;\n return (\n unquoted.length > 0 &&\n !unquoted.startsWith(\"$\") &&\n !CREDENTIAL_FREE_URL.test(unquoted)\n );\n};\n","import type { ComposeRule, Diagnostic } from \"../types/index\";\nimport type { HostPathScope } from \"./compose-mounts\";\nimport {\n bindMountSource,\n classifyHostPath,\n mountsDockerSocket,\n volumeMount,\n} from \"./compose-mounts\";\nimport { composeServices } from \"./compose-services\";\nimport { createDiagnostic } from \"./create-diagnostic\";\nimport { isLiteralSecretValue, isSecretKey } from \"./secret-keywords\";\n\nexport const noPrivilegedService: ComposeRule = {\n category: \"Compose\",\n check(composeContent, file, context) {\n const diagnostics: Diagnostic[] = [];\n\n for (const [name, config] of composeServices(composeContent)) {\n if (config.privileged === true) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n `Service '${name}' runs in privileged mode. A privileged container has full access to the host's devices and kernel, so compromising this service compromises the host.`,\n this.help,\n context?.locate?.([\"services\", name, \"privileged\"])\n )\n );\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"error\",\n help: \"Remove `privileged: true` and grant only what the service needs: specific capabilities via `cap_add`, or individual device access via `devices`.\",\n key: \"docker-doctor/no-privileged-service\",\n message: \"Do not run services in privileged mode\",\n};\n\nexport const noDockerSocketMount: ComposeRule = {\n category: \"Compose\",\n check(composeContent, file, context) {\n const diagnostics: Diagnostic[] = [];\n\n for (const [name, config] of composeServices(composeContent)) {\n const { volumes } = config;\n if (!Array.isArray(volumes)) {\n continue;\n }\n for (const [index, volume] of volumes.entries()) {\n const mount = volumeMount(volume);\n if (mount && mountsDockerSocket(mount)) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n `Service '${name}' bind-mounts the Docker socket. Anything running in this container can control the Docker daemon: start privileged containers, read every volume, and escape to the host.`,\n this.help,\n context?.locate?.([\"services\", name, \"volumes\", index])\n )\n );\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"error\",\n help: \"If the service genuinely needs the Docker API (agent tooling like MCP gateways often does), prefer `use_api_socket: true` or a filtering socket proxy over a raw bind mount of `/var/run/docker.sock`.\",\n key: \"docker-doctor/no-docker-socket-mount\",\n message: \"Do not bind-mount the Docker socket into services\",\n};\n\nconst BROAD_SCOPE_REASONS: Partial<Record<HostPathScope, string>> = {\n home: \"the whole home directory\",\n \"home-dotfile\":\n \"a hidden directory under the home directory, where credentials and tool configuration live\",\n parent: \"a directory above the Compose project\",\n root: \"the host's root filesystem\",\n system: \"a host system directory\",\n};\n\nexport const noBroadBindMount: ComposeRule = {\n category: \"Compose\",\n check(composeContent, file, context) {\n const diagnostics: Diagnostic[] = [];\n\n for (const [name, config] of composeServices(composeContent)) {\n const { volumes } = config;\n if (!Array.isArray(volumes)) {\n continue;\n }\n for (const [index, volume] of volumes.entries()) {\n const mount = volumeMount(volume);\n if (!mount || mountsDockerSocket(mount)) {\n continue;\n }\n const source = bindMountSource(mount);\n if (source === undefined) {\n continue;\n }\n const reason = BROAD_SCOPE_REASONS[classifyHostPath(source)];\n if (reason === undefined) {\n continue;\n }\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n `Service '${name}' bind-mounts '${source}', ${reason}. Everything under it is readable by the container, and a writable mount is the foothold for VM-escape bugs like CVE-2026-77179.`,\n this.help,\n context?.locate?.([\"services\", name, \"volumes\", index])\n )\n );\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Mount the narrowest directory the service actually reads (`./data`, not `~` or `/`), and add `:ro` unless the service must write to it. Host paths the service only needs at build time belong in the image instead.\",\n key: \"docker-doctor/no-broad-bind-mount\",\n message: \"Do not bind-mount broad host directories into services\",\n};\n\nexport const preferReadOnlyBindMount: ComposeRule = {\n category: \"Compose\",\n check(composeContent, file, context) {\n const diagnostics: Diagnostic[] = [];\n\n for (const [name, config] of composeServices(composeContent)) {\n const { volumes } = config;\n if (!Array.isArray(volumes)) {\n continue;\n }\n for (const [index, volume] of volumes.entries()) {\n const mount = volumeMount(volume);\n if (!mount || mount.readOnly || mountsDockerSocket(mount)) {\n continue;\n }\n const source = bindMountSource(mount);\n if (source === undefined || classifyHostPath(source) === \"project\") {\n continue;\n }\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n `Service '${name}' bind-mounts host path '${source}' read-write. A writable host mount lets the container change files outside the project, and is the foothold for VM-escape bugs like CVE-2026-77179.`,\n this.help,\n context?.locate?.([\"services\", name, \"volumes\", index])\n )\n );\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"info\",\n help: \"Append `:ro` to the short syntax, or set `read_only: true` in the long syntax, unless the service must write to the host path.\",\n key: \"docker-doctor/prefer-read-only-bind-mount\",\n message: \"Bind mounts outside the project should be read-only\",\n};\n\n// YAML reads an unquoted `123456` as a number, and it is still a literal\n// value in the file. Booleans are left out: `AUTH_ENABLED: true` matches the\n// `auth` key pattern but holds no secret.\nconst literalValue = (value: unknown): string | undefined => {\n if (typeof value === \"string\") {\n return value;\n }\n if (typeof value === \"number\") {\n return String(value);\n }\n return undefined;\n};\n\nexport const noPlaintextSecrets: ComposeRule = {\n category: \"Compose\",\n check(composeContent, file, context) {\n const diagnostics: Diagnostic[] = [];\n\n const flag = (name: string, key: string, line: number | undefined) => {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n `Potential secret in service '${name}' environment: '${key}'. A literal value here lives in version control in plain text.`,\n this.help,\n line\n )\n );\n };\n\n for (const [name, config] of composeServices(composeContent)) {\n const { environment } = config;\n\n if (Array.isArray(environment)) {\n // List syntax: \"KEY=value\" entries; a bare \"KEY\" passes the host\n // value through and holds no literal.\n for (const [index, entry] of environment.entries()) {\n if (typeof entry !== \"string\") {\n continue;\n }\n const eqIndex = entry.indexOf(\"=\");\n if (eqIndex <= 0) {\n continue;\n }\n const key = entry.slice(0, eqIndex);\n const value = entry.slice(eqIndex + 1);\n if (isSecretKey(key) && isLiteralSecretValue(value)) {\n flag(\n name,\n key,\n context?.locate?.([\"services\", name, \"environment\", index])\n );\n }\n }\n } else if (environment && typeof environment === \"object\") {\n for (const [key, value] of Object.entries(environment)) {\n const literal = literalValue(value);\n if (\n literal !== undefined &&\n isSecretKey(key) &&\n isLiteralSecretValue(literal)\n ) {\n flag(\n name,\n key,\n context?.locate?.([\"services\", name, \"environment\", key])\n );\n }\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n // oxlint-disable-next-line no-template-curly-in-string -- Compose interpolation syntax, shown literally\n help: \"Move the value to an `env_file` kept out of version control, interpolate it from the host environment (`${VAR}`), or use Compose `secrets:`.\",\n key: \"docker-doctor/no-plaintext-secrets\",\n message: \"Avoid literal secret values in Compose environment\",\n};\n\nexport const composeSecurityRules = [\n noPrivilegedService,\n noDockerSocketMount,\n noBroadBindMount,\n preferReadOnlyBindMount,\n noPlaintextSecrets,\n];\n","import {\n collectStageAliases,\n isHardenedImage,\n isScratch,\n parseFromArgs,\n parseImageRef,\n} from \"../parsers/image-ref\";\nimport { maskQuotedText } from \"../parsers/shell-quotes\";\nimport type {\n Diagnostic,\n DockerfileInstruction,\n DockerfileRule,\n} from \"../types/index\";\nimport { createDiagnostic } from \"./create-diagnostic\";\nimport { hasAptGetInstall } from \"./package-managers\";\n\nexport const preferSlimBase: DockerfileRule = {\n category: \"Image Size\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n const stageAliases = collectStageAliases(instructions);\n\n for (const inst of instructions) {\n if (inst.instruction === \"FROM\") {\n const imagePart = parseFromArgs(inst.args).base;\n if (!imagePart || isScratch(imagePart)) {\n continue;\n }\n\n const ref = parseImageRef(imagePart);\n\n if (ref.isVariable || stageAliases.has(imagePart.toLowerCase())) {\n continue;\n }\n\n // Docker Hardened Images are minimal by construction (dev variants\n // included), whatever their name and tag say.\n if (isHardenedImage(imagePart)) {\n continue;\n }\n\n // Digest pins are already fully deterministic; not our concern here.\n if (ref.digest) {\n continue;\n }\n\n // No tag: pin-image-version owns the untagged case, don't double-report.\n if (!ref.tag) {\n continue;\n }\n\n // Minimal bases identify themselves either in the name (alpine,\n // busybox, gcr.io/distroless/*) or in the tag (node:22-slim,\n // python:3.13-alpine). Judging by tag alone flagged `alpine:3.19`.\n const haystack = `${ref.name} ${ref.tag}`.toLowerCase();\n const isSlim =\n haystack.includes(\"alpine\") ||\n haystack.includes(\"slim\") ||\n haystack.includes(\"distroless\") ||\n haystack.includes(\"busybox\");\n\n if (!isSlim) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n `Base image '${imagePart}' may be a full-OS distribution. Consider using a slim or alpine alternative.`,\n this.help,\n inst.line\n )\n );\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"info\",\n help: \"Prefer tags with `-slim`, `-alpine`, or use distroless base images to minimize the default operating system footprint.\",\n key: \"docker-doctor/prefer-slim-base\",\n message: \"Prefer slim, alpine, or distroless base images\",\n};\n\nconst BUILDKIT_MOUNT_FLAG_RE = /--mount=(?<spec>\\S+)/gu;\n\n// BuildKit accepts `target`, `dst` and `destination` as synonyms.\nconst CACHE_TARGET_KEY_RE = /^(?:target|dst|destination)=/u;\n\n// A `RUN --mount=type=cache,target=<dir>` keeps <dir> in the cache mount, not\n// in the image layer — cleanup commands for that dir are unnecessary (and the\n// Docker-documented apt pattern deliberately omits them).\nconst cacheMountTargets = (args: string): string[] =>\n [...args.matchAll(BUILDKIT_MOUNT_FLAG_RE)]\n .map((match) => (match.groups?.spec ?? \"\").split(\",\"))\n .filter((options) => options.includes(\"type=cache\"))\n .flatMap((options) =>\n options\n .filter((option) => CACHE_TARGET_KEY_RE.test(option))\n .map((option) => option.slice(option.indexOf(\"=\") + 1))\n );\n\nconst APT_CACHE_DIRS = [\"/var/lib/apt\", \"/var/cache/apt\"];\nconst APK_CACHE_DIRS = [\"/var/cache/apk\", \"/etc/apk/cache\"];\n\nconst hasCacheMountFor = (args: string, cacheDirs: string[]): boolean =>\n cacheMountTargets(args).some((target) =>\n cacheDirs.some((dir) => target === dir || target.startsWith(`${dir}/`))\n );\n\nexport const cleanPackageCache: DockerfileRule = {\n category: \"Image Size\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n\n for (const inst of instructions) {\n if (inst.instruction === \"RUN\") {\n const { args } = inst;\n\n // check apt-get install without cleanup\n if (\n hasAptGetInstall(args) &&\n !args.includes(\"rm -rf /var/lib/apt/lists\") &&\n !hasCacheMountFor(args, APT_CACHE_DIRS)\n ) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n `Running 'apt-get install' without removing package lists afterwards. This keeps metadata caches inside the image layer.`,\n this.help,\n inst.line\n )\n );\n }\n\n // check apk add without --no-cache\n if (\n args.includes(\"apk add\") &&\n !args.includes(\"--no-cache\") &&\n !args.includes(\"rm -rf /var/cache/apk\") &&\n !hasCacheMountFor(args, APK_CACHE_DIRS)\n ) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n `Running 'apk add' without '--no-cache' or cleaning the apk cache. This increases layer size.`,\n this.help,\n inst.line\n )\n );\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"For apt-get, append `&& rm -rf /var/lib/apt/lists/*`. For apk, use `apk add --no-cache`. For dnf/yum, run `yum clean all`.\",\n key: \"docker-doctor/clean-package-cache\",\n message: \"Clean up package manager cache in the same RUN layer\",\n};\n\nconst SHELL_SEPARATOR_RE = /\\s*(?:&&|\\|\\||;|\\|)\\s*/u;\nconst WHITESPACE_RE = /\\s+/u;\nconst ENV_ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*=/u;\nconst NODE_PACKAGE_MANAGERS = new Set([\"npm\", \"yarn\", \"pnpm\", \"bun\"]);\nconst INSTALL_VERBS = new Set([\"install\", \"ci\", \"i\"]);\n// Flags that mean the install leaves devDependencies out of the image.\n// `-g`/`--global` install a tool instead of the project, `-P` is pnpm's\n// production flag and `-p` is bun's.\nconst NON_DEV_INSTALL_FLAGS = new Set([\n \"-g\",\n \"--global\",\n \"--production\",\n \"--omit=dev\",\n \"--only=prod\",\n \"--only=production\",\n \"--prod\",\n \"-P\",\n \"-p\",\n]);\n\n// One shell command, e.g. `NODE_ENV=production npm ci --omit=dev`. Only a\n// bare project install counts: a positional word after the verb\n// (`npm install express`) installs named packages, not devDependencies.\nconst segmentInstallsDevDependencies = (segment: string): boolean => {\n const tokens = segment.split(WHITESPACE_RE).filter(Boolean);\n const commandStart = tokens.findIndex(\n (token) => !ENV_ASSIGNMENT_RE.test(token)\n );\n if (commandStart === -1) {\n return false;\n }\n const [manager, verb, ...options] = tokens.slice(commandStart);\n if (!(NODE_PACKAGE_MANAGERS.has(manager) && INSTALL_VERBS.has(verb))) {\n return false;\n }\n const setsProductionEnv = tokens\n .slice(0, commandStart)\n .includes(\"NODE_ENV=production\");\n return (\n !setsProductionEnv &&\n options.every(\n (option) => option.startsWith(\"-\") && !NON_DEV_INSTALL_FLAGS.has(option)\n )\n );\n};\n\nconst installsDevDependencies = (args: string): boolean =>\n !args.includes(\"prune\") &&\n maskQuotedText(args)\n .split(SHELL_SEPARATOR_RE)\n .some(segmentInstallsDevDependencies);\n\nexport const avoidDevDependencies: DockerfileRule = {\n category: \"Image Size\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n\n const stages: {\n name: string | null;\n base: string;\n runs: DockerfileInstruction[];\n }[] = [];\n for (const inst of instructions) {\n if (inst.instruction === \"FROM\") {\n const { base, stage } = parseFromArgs(inst.args);\n stages.push({\n base: base?.toLowerCase() ?? \"\",\n name: stage?.toLowerCase() ?? null,\n runs: [],\n });\n } else if (inst.instruction === \"RUN\" && stages.length > 0) {\n stages.at(-1)?.runs.push(inst);\n }\n }\n if (stages.length === 0) {\n return diagnostics;\n }\n\n // The default build target is the last stage, and its image contains\n // every layer of the local stages it builds FROM — so a dev install in\n // an inherited stage ships just like one in the final stage itself.\n const auditedIndices: number[] = [];\n let index = stages.length - 1;\n while (index >= 0) {\n auditedIndices.push(index);\n const { base } = stages[index];\n index = stages\n .slice(0, index)\n .findIndex((s) => s.name !== null && s.name === base);\n }\n\n const finalIndex = stages.length - 1;\n for (const stageIndex of auditedIndices.toReversed()) {\n const stage = stages[stageIndex];\n for (const inst of stage.runs) {\n if (!installsDevDependencies(inst.args)) {\n continue;\n }\n const where =\n stageIndex === finalIndex\n ? \"in the final stage\"\n : `in stage '${stage.name}', whose layers the final stage inherits,`;\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n `Running package install '${inst.args}' ${where} without omitting devDependencies.`,\n this.help,\n inst.line\n )\n );\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"For Node.js, run `npm prune --production` or install only production dependencies (`npm ci --omit=dev`) in the runtime stage.\",\n key: \"docker-doctor/avoid-dev-dependencies\",\n message: \"Avoid installing dev dependencies in the final stage\",\n};\n\nexport const imageSizeRules = [\n preferSlimBase,\n cleanPackageCache,\n avoidDevDependencies,\n];\n","import { maskQuotedText } from \"../parsers/shell-quotes\";\nimport type { Diagnostic, DockerfileRule } from \"../types/index\";\nimport { createDiagnostic } from \"./create-diagnostic\";\n\n// `make` only where a command can start, so `apk add make` (installing the\n// tool) is not a build step while `cd src && make` is.\nconst BUILD_TOOL_COMMAND_RE =\n /(?:^|&&|\\|\\||;|\\||\\(|`|\\$\\()\\s*(?:make|cmake)(?=\\s|$)/u;\n\nexport const useMultiStage: DockerfileRule = {\n category: \"Performance\",\n check(instructions, file) {\n const fromCount = instructions.filter(\n (inst) => inst.instruction === \"FROM\"\n ).length;\n if (fromCount === 1) {\n // Check if it's not a trivial/short Dockerfile (e.g., has some build steps)\n const hasBuildSteps = instructions.some(\n (inst) =>\n inst.instruction === \"RUN\" &&\n (inst.args.includes(\"npm run build\") ||\n inst.args.includes(\"yarn build\") ||\n inst.args.includes(\"bun run build\") ||\n inst.args.includes(\"cargo build\") ||\n BUILD_TOOL_COMMAND_RE.test(maskQuotedText(inst.args)))\n );\n\n if (hasBuildSteps) {\n return [\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n \"Only one build stage (FROM) was detected, but build instructions were found. Multi-stage builds can significantly reduce final image size.\",\n this.help,\n instructions.find((inst) => inst.instruction === \"FROM\")?.line || 1\n ),\n ];\n }\n }\n return [];\n },\n defaultSeverity: \"info\",\n help: \"Use multi-stage builds (multiple FROM statements) to separate build dependencies from the runtime image and reduce size.\",\n key: \"docker-doctor/use-multi-stage\",\n message: \"Use multi-stage builds\",\n};\n\nexport const orderLayers: DockerfileRule = {\n category: \"Performance\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n let copyAllLine = -1;\n\n for (const inst of instructions) {\n if (inst.instruction === \"FROM\") {\n copyAllLine = -1;\n }\n\n if (inst.instruction === \"COPY\" || inst.instruction === \"ADD\") {\n const parts = inst.args.split(/\\s+/u);\n const src = parts.find((p) => !p.startsWith(\"--\"));\n\n if (!src) {\n continue;\n }\n\n const normalized = src.replace(/^\\.\\//u, \"\");\n const isCopyAll =\n normalized === \".\" ||\n normalized === \"\" ||\n normalized === \"*\" ||\n normalized === \"src\" ||\n normalized.startsWith(\"src/\");\n\n if (isCopyAll && copyAllLine === -1) {\n copyAllLine = inst.line;\n }\n }\n\n if (inst.instruction === \"RUN\" && copyAllLine !== -1) {\n const args = inst.args.toLowerCase();\n if (\n args.includes(\"npm install\") ||\n args.includes(\"npm ci\") ||\n args.includes(\"yarn install\") ||\n args.includes(\"bun install\") ||\n args.includes(\"pip install\") ||\n args.includes(\"cargo fetch\")\n ) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n `Running package installation command '${inst.args}' after copying application files (at line ${copyAllLine}). This invalidates the cache on any code changes.`,\n this.help,\n inst.line\n )\n );\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Copy dependency definition files (like package.json, lockfiles) and run install commands BEFORE copying the rest of the application source code.\",\n key: \"docker-doctor/order-layers\",\n message: \"Order layers to maximize build cache utility\",\n};\n\nexport const minimizeLayers: DockerfileRule = {\n category: \"Performance\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n let consecutiveRunCount = 0;\n let firstRunLine = -1;\n\n for (const inst of instructions) {\n if (inst.instruction === \"RUN\") {\n if (consecutiveRunCount === 0) {\n firstRunLine = inst.line;\n }\n consecutiveRunCount += 1;\n } else {\n if (consecutiveRunCount > 2) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n `Found ${consecutiveRunCount} consecutive RUN instructions starting at line ${firstRunLine}. Consider combining them into a single RUN layer.`,\n this.help,\n firstRunLine\n )\n );\n }\n consecutiveRunCount = 0;\n }\n }\n\n if (consecutiveRunCount > 2) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n `Found ${consecutiveRunCount} consecutive RUN instructions starting at line ${firstRunLine}. Consider combining them into a single RUN layer.`,\n this.help,\n firstRunLine\n )\n );\n }\n\n return diagnostics;\n },\n defaultSeverity: \"info\",\n help: \"Combine consecutive RUN instructions using `&&` and `\\\\` to reduce the total layer count and image size.\",\n key: \"docker-doctor/minimize-layers\",\n message: \"Minimize the number of image layers\",\n};\n\n// Docker reads .dockerignore from the build-context root, which we cannot know\n// statically. Accept the locations that cover real usage: next to the\n// Dockerfile, at the scan root (the common monorepo-root build context), or\n// BuildKit's per-Dockerfile `<Dockerfile>.dockerignore`, which it prefers\n// when building with `-f`. Paths here are scan-root-relative and always\n// \"/\"-separated.\nconst hasDockerignoreFor = (\n dockerfilePath: string,\n projectFiles: string[]\n): boolean => {\n const lastSlash = dockerfilePath.lastIndexOf(\"/\");\n const dir = lastSlash === -1 ? \"\" : dockerfilePath.slice(0, lastSlash);\n const adjacent = dir === \"\" ? \".dockerignore\" : `${dir}/.dockerignore`;\n const perDockerfile = `${dockerfilePath}.dockerignore`;\n return projectFiles.some(\n (f) => f === adjacent || f === perDockerfile || f === \".dockerignore\"\n );\n};\n\nexport const useDockerignore: DockerfileRule = {\n category: \"Performance\",\n check(instructions, file, context) {\n // If copying everything, we definitely need .dockerignore\n const hasCopyAll = instructions.some((inst) => {\n if (inst.instruction === \"COPY\" || inst.instruction === \"ADD\") {\n // COPY --from=<stage> reads from a previous build stage, not the\n // build context, so .dockerignore is irrelevant to it.\n const isStageCopy = /(?:^|\\s)--from=/u.test(inst.args);\n if (isStageCopy) {\n return false;\n }\n\n const parts = inst.args.split(/\\s+/u);\n const src = parts.find((p) => !p.startsWith(\"--\"));\n if (!src) {\n return false;\n }\n return src === \".\" || src === \"./\" || src === \"*\";\n }\n return false;\n });\n\n if (\n hasCopyAll &&\n context?.projectFiles &&\n !hasDockerignoreFor(file, context.projectFiles)\n ) {\n return [\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n \"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.\",\n this.help,\n 1\n ),\n ];\n }\n return [];\n },\n defaultSeverity: \"warning\",\n help: \"Create a .dockerignore file in the same directory as the Dockerfile to prevent copying unnecessary files (like node_modules, logs, build artifacts).\",\n key: \"docker-doctor/use-dockerignore\",\n message: \"Add a .dockerignore file\",\n};\n\nexport const performanceRules = [\n useMultiStage,\n orderLayers,\n minimizeLayers,\n useDockerignore,\n];\n","import {\n collectStageAliases,\n isHardenedRuntimeImage,\n isScratch,\n mutableRefIssue,\n parseFromArgs,\n parseImageRef,\n} from \"../parsers/image-ref\";\nimport type { Diagnostic, DockerfileRule } from \"../types/index\";\nimport { createDiagnostic } from \"./create-diagnostic\";\nimport { isLiteralSecretValue, isSecretKey } from \"./secret-keywords\";\n\n// USER accepts \"user\", \"uid\", \"user:group\" and \"uid:gid\". Only the user half\n// decides whether the container runs as root; the group half is irrelevant.\nconst isRootUser = (value: string): boolean => {\n const [user] = value.split(\":\");\n return user === \"root\" || user === \"0\";\n};\n\nexport const noRootUser: DockerfileRule = {\n category: \"Security\",\n check(instructions, file) {\n // A stage built FROM a previous stage inherits that stage's image\n // config, USER included; a fresh base image resets it to root.\n const stageUser = new Map<string, string>();\n let currentStage: string | null = null;\n let lastUser = \"root\";\n let lastUserLine = 1;\n\n for (const inst of instructions) {\n if (inst.instruction === \"FROM\") {\n const { base, stage } = parseFromArgs(inst.args);\n // DHI runtime bases default to a nonroot user; \"nonroot\" is a\n // sentinel that isRootUser treats as safe until a USER overrides it.\n const baseDefaultUser =\n base && isHardenedRuntimeImage(base) ? \"nonroot\" : \"root\";\n lastUser = stageUser.get(base?.toLowerCase() ?? \"\") ?? baseDefaultUser;\n lastUserLine = inst.line;\n currentStage = stage?.toLowerCase() ?? null;\n if (currentStage) {\n stageUser.set(currentStage, lastUser);\n }\n } else if (inst.instruction === \"USER\") {\n lastUser = inst.args.trim().toLowerCase();\n lastUserLine = inst.line;\n if (currentStage) {\n stageUser.set(currentStage, lastUser);\n }\n }\n }\n\n if (isRootUser(lastUser)) {\n return [\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n \"The container runs as root. Running as root allows potential container breakout vulnerabilities.\",\n this.help,\n lastUserLine\n ),\n ];\n }\n\n return [];\n },\n defaultSeverity: \"warning\",\n help: \"Add a non-root user (e.g., `USER node` or `USER 1000`) to improve security.\",\n key: \"docker-doctor/no-root-user\",\n message: \"Run the container as a non-root user\",\n};\n\nexport const noSecretsInEnv: DockerfileRule = {\n category: \"Security\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n\n for (const inst of instructions) {\n if (inst.instruction === \"ENV\" || inst.instruction === \"ARG\") {\n const args = inst.args.trim();\n if (inst.instruction === \"ENV\" && !args.includes(\"=\")) {\n // KEY VALUE format\n const match = args.match(/^(?<key>[^\\s]+)\\s+(?<value>.*)$/u);\n if (match?.groups) {\n const { key, value } = match.groups;\n const keyIsSecret = isSecretKey(key);\n if (\n keyIsSecret &&\n isLiteralSecretValue(value) &&\n !value.startsWith(\"{\")\n ) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n `Potential secret found in ${inst.instruction}: '${key}'. Secrets baked into images can be extracted easily by anyone with image access.`,\n this.help,\n inst.line\n )\n );\n }\n }\n } else {\n // Existing KEY=VALUE logic\n const parts = args.split(/\\s+/u);\n for (const part of parts) {\n const eqIndex = part.indexOf(\"=\");\n let key = \"\";\n let value = \"\";\n\n if (eqIndex > 0) {\n key = part.slice(0, eqIndex);\n value = part.slice(eqIndex + 1);\n } else {\n key = part;\n }\n\n const keyIsSecret = isSecretKey(key);\n if (\n keyIsSecret &&\n isLiteralSecretValue(value) &&\n !value.startsWith(\"{\")\n ) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n `Potential secret found in ${inst.instruction}: '${key}'. Secrets baked into images can be extracted easily by anyone with image access.`,\n this.help,\n inst.line\n )\n );\n }\n }\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"error\",\n help: \"Use Docker Secrets, build arguments passed at runtime, or environment variables at runtime instead of baking them into the image.\",\n key: \"docker-doctor/no-secrets-in-env\",\n message: \"Avoid storing secrets in ENV or ARG instructions\",\n};\n\nexport const pinImageVersion: DockerfileRule = {\n category: \"Security\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n const stageAliases = collectStageAliases(instructions);\n\n for (const inst of instructions) {\n if (inst.instruction === \"FROM\") {\n // FROM image or FROM image:tag or FROM image@sha256:hash\n // Also respect multi-stage builds (AS stageName)\n const imagePart = parseFromArgs(inst.args).base;\n\n if (!imagePart || isScratch(imagePart)) {\n continue;\n }\n\n const ref = parseImageRef(imagePart);\n\n if (ref.isVariable || stageAliases.has(imagePart.toLowerCase())) {\n continue;\n }\n\n const issue = mutableRefIssue(ref);\n if (issue) {\n const detail =\n issue === \"untagged\"\n ? `Base image '${imagePart}' does not specify a tag.`\n : `Base image '${imagePart}' uses the mutable 'latest' tag.`;\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n `${detail} This makes builds non-deterministic.`,\n this.help,\n inst.line\n )\n );\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Specify a concrete tag instead of `latest` or no tag (e.g., `node:22.2.0-alpine` instead of `node`).\",\n key: \"docker-doctor/pin-image-version\",\n message: \"Pin base images to a specific tag or digest\",\n};\n\nexport const noAddRemote: DockerfileRule = {\n category: \"Security\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n\n for (const inst of instructions) {\n if (inst.instruction === \"ADD\") {\n const parts = inst.args.split(/\\s+/u);\n const src = parts.find((p) => !p.startsWith(\"--\"));\n\n if (!src) {\n continue;\n }\n\n if (src.startsWith(\"http://\") || src.startsWith(\"https://\")) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity,\n `ADD instruction uses a remote URL '${src}'. Remote files added via ADD cannot be cleaned up in later layers, increasing image size.`,\n this.help,\n inst.line\n )\n );\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Use `RUN curl` or `RUN wget` instead of ADD for remote URLs, and delete the downloaded archive in the same layer to minimize size.\",\n key: \"docker-doctor/no-add-remote\",\n message: \"Avoid using ADD with remote URLs\",\n};\n\nexport const securityRules = [\n noRootUser,\n noSecretsInEnv,\n pinImageVersion,\n noAddRemote,\n];\n","import type {\n DockerfileRule,\n ComposeRule,\n RuleDefinition,\n} from \"../types/index\";\nimport { bestPracticesRules } from \"./best-practices\";\nimport { composeRules } from \"./compose\";\nimport { composeModelRules } from \"./compose-models\";\nimport { composeSecurityRules } from \"./compose-security\";\nimport { imageSizeRules } from \"./image-size\";\nimport { performanceRules } from \"./performance\";\nimport { securityRules } from \"./security\";\n\nexport const allDockerfileRules: DockerfileRule[] = [\n ...securityRules,\n ...performanceRules,\n ...bestPracticesRules,\n ...imageSizeRules,\n];\n\nexport const allComposeRules: ComposeRule[] = [\n ...composeRules,\n ...composeSecurityRules,\n ...composeModelRules,\n];\n\nexport const allRules: RuleDefinition[] = [\n ...allDockerfileRules,\n ...allComposeRules,\n];\n\nexport const findRule = (key: string): RuleDefinition | undefined =>\n allRules.find((rule) => rule.key === key);\n","import type { RuleDefinition, RuleSeverity } from \"../types/index\";\n\n// Precedence: per-rule config > category config > the rule's default.\nexport const resolveSeverity = (\n rule: RuleDefinition,\n rulesConfig?: Record<string, RuleSeverity>,\n categoriesConfig?: Record<string, RuleSeverity>\n): RuleSeverity =>\n rulesConfig?.[rule.key] ??\n categoriesConfig?.[rule.category] ??\n rule.defaultSeverity;\n","import { allDockerfileRules } from \"../rules/index\";\nimport type {\n DockerfileInstruction,\n Diagnostic,\n RuleSeverity,\n} from \"../types/index\";\nimport { resolveSeverity } from \"./resolve-severity\";\n\nexport const runDockerfileRules = (\n instructions: DockerfileInstruction[],\n file: string,\n projectFiles: string[],\n rulesConfig?: Record<string, RuleSeverity>,\n categoriesConfig?: Record<string, RuleSeverity>\n): Diagnostic[] => {\n // A file with no instructions (empty, comment-only, a stub) has nothing\n // to check. Whole-file rules such as no-root-user would otherwise report\n // the absence of a USER line as \"runs as root\".\n if (instructions.length === 0) {\n return [];\n }\n\n const diagnostics: Diagnostic[] = [];\n\n for (const rule of allDockerfileRules) {\n const severity = resolveSeverity(rule, rulesConfig, categoriesConfig);\n if (severity === \"off\") {\n continue;\n }\n\n const ruleDiagnostics = rule.check(instructions, file, { projectFiles });\n\n for (const diag of ruleDiagnostics) {\n diag.category = rule.category;\n if (severity !== rule.defaultSeverity) {\n diag.severity = severity;\n }\n }\n\n diagnostics.push(...ruleDiagnostics);\n }\n\n return diagnostics;\n};\n","import { allComposeRules } from \"../rules/index\";\nimport type { ComposeLocator, Diagnostic, RuleSeverity } from \"../types/index\";\nimport { resolveSeverity } from \"./resolve-severity\";\n\nexport const runComposeRules = (\n composeContent: unknown,\n file: string,\n rulesConfig?: Record<string, RuleSeverity>,\n categoriesConfig?: Record<string, RuleSeverity>,\n locate?: ComposeLocator\n): Diagnostic[] => {\n const diagnostics: Diagnostic[] = [];\n\n for (const rule of allComposeRules) {\n const severity = resolveSeverity(rule, rulesConfig, categoriesConfig);\n if (severity === \"off\") {\n continue;\n }\n\n const ruleDiagnostics = rule.check(composeContent, file, { locate });\n\n for (const diag of ruleDiagnostics) {\n diag.category = rule.category;\n if (severity !== rule.defaultSeverity) {\n diag.severity = severity;\n }\n }\n\n diagnostics.push(...ruleDiagnostics);\n }\n\n return diagnostics;\n};\n","import type {\n DockerDoctorConfig,\n RuleCategory,\n RuleSeverity,\n} from \"../types/index\";\n\nexport type { DockerDoctorConfig } from \"../types/index\";\n\nconst RULE_SEVERITIES: readonly RuleSeverity[] = [\n \"error\",\n \"warning\",\n \"info\",\n \"off\",\n];\n\nconst RULE_CATEGORIES: readonly RuleCategory[] = [\n \"Best Practices\",\n \"Compose\",\n \"Image Size\",\n \"Performance\",\n \"Security\",\n];\n\nconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst isRuleSeverity = (value: unknown): value is RuleSeverity =>\n typeof value === \"string\" &&\n (RULE_SEVERITIES as readonly string[]).includes(value);\n\nconst validateRules = (value: unknown): Record<string, RuleSeverity> => {\n if (!isPlainObject(value)) {\n throw new Error(\n `Invalid config: \"rules\" must be an object, got ${typeof value}`\n );\n }\n\n for (const [key, severity] of Object.entries(value)) {\n if (!isRuleSeverity(severity)) {\n throw new Error(\n `Invalid severity ${JSON.stringify(severity)} for rule \"${key}\"`\n );\n }\n }\n\n return value as Record<string, RuleSeverity>;\n};\n\nconst validateCategories = (\n value: unknown\n): Partial<Record<RuleCategory, RuleSeverity>> => {\n if (!isPlainObject(value)) {\n throw new Error(\n `Invalid config: \"categories\" must be an object, got ${typeof value}`\n );\n }\n\n const result: Partial<Record<RuleCategory, RuleSeverity>> = {};\n for (const [key, severity] of Object.entries(value)) {\n if (!(RULE_CATEGORIES as readonly string[]).includes(key)) {\n // Unknown category keys are silently dropped (matches the\n // legacy schema's excess-property behavior).\n continue;\n }\n if (!isRuleSeverity(severity)) {\n throw new Error(\n `Invalid severity ${JSON.stringify(severity)} for category \"${key}\"`\n );\n }\n result[key as RuleCategory] = severity;\n }\n\n return result;\n};\n\nconst validateIgnore = (value: unknown): { files?: string[] } => {\n if (!isPlainObject(value)) {\n throw new Error(\n `Invalid config: \"ignore\" must be an object, got ${typeof value}`\n );\n }\n\n const result: { files?: string[] } = {};\n if (\"files\" in value && value.files !== undefined) {\n if (\n !Array.isArray(value.files) ||\n !value.files.every((item) => typeof item === \"string\")\n ) {\n throw new Error(\n 'Invalid config: \"ignore.files\" must be an array of strings'\n );\n }\n result.files = value.files;\n }\n\n return result;\n};\n\nconst describeInvalidTopLevel = (input: unknown): string => {\n if (input === null) {\n return \"null\";\n }\n if (Array.isArray(input)) {\n return \"array\";\n }\n return typeof input;\n};\n\n/**\n * Validates and normalizes a raw config object, throwing on invalid input.\n *\n * Mirrors the legacy schema's behavior exactly, including silently\n * dropping unknown top-level (and nested) keys rather than throwing\n * or preserving them.\n */\nexport const validateConfig = (input: unknown): DockerDoctorConfig => {\n if (!isPlainObject(input)) {\n throw new Error(\n `Invalid config: expected an object, got ${describeInvalidTopLevel(input)}`\n );\n }\n\n const result: DockerDoctorConfig = {};\n\n if (\"rules\" in input && input.rules !== undefined) {\n result.rules = validateRules(input.rules);\n }\n\n if (\"categories\" in input && input.categories !== undefined) {\n result.categories = validateCategories(input.categories);\n }\n\n if (\"ignore\" in input && input.ignore !== undefined) {\n result.ignore = validateIgnore(input.ignore);\n }\n\n return result;\n};\n","import { allRules } from \"../rules/index\";\nimport type { RuleCategory } from \"../types/index\";\n\nconst KNOWN_CATEGORIES: readonly RuleCategory[] = [\n \"Best Practices\",\n \"Compose\",\n \"Image Size\",\n \"Performance\",\n \"Security\",\n];\n\nexport interface UnknownConfigKeys {\n categories: string[];\n rules: string[];\n}\n\nconst keysOf = (value: unknown): string[] =>\n typeof value === \"object\" && value !== null && !Array.isArray(value)\n ? Object.keys(value)\n : [];\n\n// Config keys are matched exactly, so a typo'd rule key -- or a category\n// written as \"security\" instead of \"Security\" -- passes validation and then\n// matches no rule: a suppression the user believes is active silently does\n// nothing. Reported so the caller can warn, deliberately not an error,\n// because a config naming a rule that a later release removed should still\n// scan. Takes the raw config because validation drops unknown category keys\n// before they reach the typed result.\nexport const collectUnknownConfigKeys = (raw: unknown): UnknownConfigKeys => {\n if (typeof raw !== \"object\" || raw === null) {\n return { categories: [], rules: [] };\n }\n\n const knownRuleKeys = new Set(allRules.map((rule) => rule.key));\n const knownCategories = new Set<string>(KNOWN_CATEGORIES);\n const { categories, rules } = raw as {\n categories?: unknown;\n rules?: unknown;\n };\n\n return {\n categories: keysOf(categories).filter((key) => !knownCategories.has(key)),\n rules: keysOf(rules).filter((key) => !knownRuleKeys.has(key)),\n };\n};\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport { parse as parseYaml } from \"yaml\";\n\nimport { ConfigError } from \"../errors\";\nimport type { DockerDoctorConfig } from \"../schemas/config\";\nimport { validateConfig } from \"../schemas/config\";\nimport { collectUnknownConfigKeys } from \"./unknown-keys\";\n\nconst fileExists = async (filePath: string): Promise<boolean> => {\n try {\n await fs.access(filePath);\n return true;\n } catch {\n return false;\n }\n};\n\nconst parseConfigFile = async (\n filePath: string,\n format: \"JSON\" | \"YAML\",\n parse: (content: string) => unknown\n): Promise<unknown> => {\n try {\n const content = await fs.readFile(filePath, \"utf-8\");\n return parse(content);\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n throw new ConfigError({\n message: `Failed to parse config ${format}: ${msg}`,\n });\n }\n};\n\nconst TYPESCRIPT_CONFIG_PATTERN = /\\.[cm]?ts$/u;\n\n// Node refuses a TypeScript config in two distinguishable ways, and only one\n// of them is a matter of the runtime being too old, so they do not get the\n// same advice.\nconst TYPE_STRIPPING_FAILURES: Record<string, string> = {\n ERR_UNKNOWN_FILE_EXTENSION:\n \"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)\",\n ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING:\n \"Node.js never strips TypeScript types for files under node_modules, on any version, so the config has to live outside it\",\n};\n\n// Returns the reason Node could not read a TypeScript config, or undefined\n// when the failure is something else. Exported for tests: under Bun the\n// import always succeeds, so this is the only part of the branch that can be\n// covered directly.\nexport const typeStrippingFailure = (\n filePath: string,\n error: unknown\n): string | undefined => {\n if (!TYPESCRIPT_CONFIG_PATTERN.test(filePath)) {\n return;\n }\n if (\n typeof error !== \"object\" ||\n error === null ||\n !(\"code\" in error) ||\n typeof error.code !== \"string\"\n ) {\n return;\n }\n return TYPE_STRIPPING_FAILURES[error.code];\n};\n\nconst importConfig = async (filePath: string): Promise<unknown> => {\n if (filePath.endsWith(\".json\")) {\n return parseConfigFile(filePath, \"JSON\", JSON.parse);\n }\n\n if (filePath.endsWith(\".yaml\") || filePath.endsWith(\".yml\")) {\n return parseConfigFile(filePath, \"YAML\", parseYaml);\n }\n\n try {\n const configModule = await import(filePath);\n return configModule.default || configModule;\n } catch (error: unknown) {\n const reason = typeStrippingFailure(filePath, error);\n if (reason) {\n throw new ConfigError({\n message: `Cannot load ${filePath} on Node.js ${process.version}: ${reason}. A docker-doctor.config.yaml or .json needs no TypeScript support at all.`,\n });\n }\n\n const msg = error instanceof Error ? error.message : String(error);\n throw new ConfigError({\n message: `Failed to load config file ${filePath}: ${msg}`,\n });\n }\n};\n\nconst warnUnknownKeys = (\n raw: unknown,\n onWarning: (message: string) => void\n): void => {\n const unknown = collectUnknownConfigKeys(raw);\n for (const key of unknown.rules) {\n onWarning(\n `Unknown rule \"${key}\" in config — it matches no rule and has no effect.`\n );\n }\n for (const key of unknown.categories) {\n onWarning(\n `Unknown category \"${key}\" in config — categories are case-sensitive (e.g. \"Best Practices\", \"Security\").`\n );\n }\n};\n\nexport const loadConfig = async (\n rootDir: string,\n customPath?: string,\n // Called once per unrecognized config key. Optional so existing callers are\n // unaffected; without it, unknown keys stay silent as before.\n onWarning?: (message: string) => void\n): Promise<DockerDoctorConfig> => {\n let configObject: unknown = null;\n\n if (customPath) {\n const fullPath = path.resolve(rootDir, customPath);\n if (!(await fileExists(fullPath))) {\n throw new ConfigError({\n message: `Specified config file not found at ${fullPath}`,\n });\n }\n configObject = await importConfig(fullPath);\n } else {\n const candidates = [\n \"docker-doctor.config.ts\",\n \"docker-doctor.config.js\",\n \"docker-doctor.config.mjs\",\n \"docker-doctor.config.cjs\",\n \"docker-doctor.config.json\",\n \"docker-doctor.config.yaml\",\n \"docker-doctor.config.yml\",\n ];\n\n /* eslint-disable no-await-in-loop */\n for (const cand of candidates) {\n const fullPath = path.join(rootDir, cand);\n if (await fileExists(fullPath)) {\n configObject = await importConfig(fullPath);\n break;\n }\n }\n /* eslint-enable no-await-in-loop */\n\n if (!configObject) {\n const pkgPath = path.join(rootDir, \"package.json\");\n if (await fileExists(pkgPath)) {\n try {\n const pkgContent = await fs.readFile(pkgPath, \"utf-8\");\n const pkgJson = JSON.parse(pkgContent);\n if (pkgJson.dockerDoctor) {\n configObject = pkgJson.dockerDoctor;\n }\n } catch {\n // ignore package.json read/parse failures\n }\n }\n }\n }\n\n if (!configObject) {\n return {};\n }\n\n try {\n const config = validateConfig(configObject);\n if (onWarning) {\n warnUnknownKeys(configObject, onWarning);\n }\n return config;\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n throw new ConfigError({\n message: `Invalid configuration format: ${msg}`,\n });\n }\n};\n","import type { Diagnostic } from \"./types/index\";\n\nexport const SCORE_BUCKETS = [\n { emoji: \"🏆\", label: \"Excellent\", min: 90 },\n { emoji: \"✅\", label: \"Good\", min: 75 },\n { emoji: \"⚠️\", label: \"Needs Work\", min: 50 },\n { emoji: \"🚨\", label: \"Critical\", min: 0 },\n] as const;\n\nexport const getScoreBucket = (\n score: number\n): (typeof SCORE_BUCKETS)[number] => {\n for (const bucket of SCORE_BUCKETS) {\n if (score >= bucket.min) {\n return bucket;\n }\n }\n return SCORE_BUCKETS.at(-1) as (typeof SCORE_BUCKETS)[number];\n};\n\nexport const calculateScore = (\n diagnostics: Diagnostic[]\n): {\n score: number;\n label: string;\n} => {\n let penalty = 0;\n\n for (const diag of diagnostics) {\n if (diag.severity === \"error\") {\n penalty += 10;\n } else if (diag.severity === \"warning\") {\n penalty += 4;\n } else if (diag.severity === \"info\") {\n penalty += 1;\n }\n }\n\n // Asymptotic decay curve: score = round(100 * e^(-penalty / K)).\n //\n // The old `max(0, 100 - penalty)` formula saturates at 0 once penalty\n // reaches 100 (e.g. ~10 errors), so a messy project and a catastrophic\n // one are indistinguishable and the score can never register a fix.\n // This curve approaches (but never reaches) 0, so it stays monotonic\n // and responsive across the whole range instead of going inert.\n //\n // K=70 was chosen so a single warning (penalty 4) still scores ~94,\n // comfortably inside the \"Excellent\" (>=90) bucket, while errors and\n // repeated warnings still meaningfully erode the score. K=40 (the\n // naive \"half-life at penalty ~28\" choice) was tried first and pushed\n // a single warning down to ~90 - right on the Excellent/Good boundary,\n // which is too harsh a penalty for one warning.\n const K = 70;\n const score = Math.round(100 * Math.exp(-penalty / K));\n const bucket = getScoreBucket(score);\n const label = `${bucket.label} ${bucket.emoji}`;\n\n return { label, score };\n};\n","import { findRule } from \"./rules/index\";\nimport type { Diagnostic, ProjectInfo, RuleCategory } from \"./types/index\";\n\n// Bump whenever the JSON report shape or the score formula/weights change.\n// The unversioned shape shipped before this field existed is implicitly 1.\n// v3 (2026-09): added diagnostics[].category.\n// v4 (2026-09): added failures[], the files discovered but not analyzed\n// (read or parse errors).\nexport const REPORT_SCHEMA_VERSION = 4;\n\nexport interface ReportFailure {\n file: string;\n message: string;\n}\n\nexport interface JsonReport {\n diagnostics: {\n category: RuleCategory;\n column?: number;\n file: string;\n help: string;\n line?: number;\n message: string;\n rule: string;\n severity: \"error\" | \"warning\" | \"info\";\n }[];\n failures: ReportFailure[];\n label: string;\n project: ProjectInfo;\n schemaVersion: number;\n score: number;\n timestamp: string;\n}\n\n// Diagnostics reach the report through a runner, which stamps category.\n// The findRule fallback covers callers that build diagnostics by hand.\nconst resolveCategory = (diagnostic: Diagnostic): RuleCategory => {\n const category = diagnostic.category ?? findRule(diagnostic.rule)?.category;\n if (!category) {\n throw new Error(\n `Diagnostic for unknown rule \"${diagnostic.rule}\" has no category`\n );\n }\n return category;\n};\n\nexport const toJsonReport = (\n diagnostics: Diagnostic[],\n score: number,\n label: string,\n project: ProjectInfo,\n failures: ReportFailure[] = []\n): JsonReport => ({\n diagnostics: diagnostics.map((d) => ({\n category: resolveCategory(d),\n column: d.column,\n file: d.file,\n help: d.help,\n line: d.line,\n message: d.message,\n rule: d.rule,\n severity: d.severity,\n })),\n failures: failures.map(({ file, message }) => ({ file, message })),\n label,\n project,\n schemaVersion: REPORT_SCHEMA_VERSION,\n score,\n timestamp: new Date().toISOString(),\n});\n"],"mappings":"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"}