Container Audit
Output path directive (canonical — overrides in-body references).
All file outputs from this skill MUST be written under .anthril/audits/container-audit/.
Run mkdir -p .anthril/audits/container-audit before the first Write call.
Primary artefact: .anthril/audits/container-audit/<artefact>.
Do NOT write to the project root or to bare filenames at cwd.
Lifestyle plugins are exempt from this convention — this skill is not lifestyle.
When to use
Run this skill when the user mentions:
- Dockerfile review, container security, image hardening
- CIS Docker Benchmark
- docker-compose audit
- Image size optimisation
- Secret leaks in container builds
Covers eight categories: base image choice (distroless/alpine, digest pinning), user privileges (non-root, dropped capabilities, read-only filesystem), secret leaks (no ARG/ENV secrets, BuildKit --mount=type=secret), layer efficiency (multi-stage, COPY --from, cache ordering), signal handling (tini, STOPSIGNAL, exec-form CMD), healthchecks, .dockerignore coverage, and docker-compose safety (no privileged: true, no host network, resource limits).
Before You Start
- Determine operating mode.
--live enables docker inspect, docker history, and Trivy/Grype scans. --apply enables per-finding Dockerfile patching. --runtime is not applicable for this skill (there is no safe runtime test for a Dockerfile without a target environment).
- Find all Dockerfiles and compose files. Run
scripts/list-dockerfiles.sh.
- Sub-agent budget. One
Agent(subagent_type=Explore) per Dockerfile. Warn above 10 files.
- Load
.container-ignore for suppression rules.
User Context
$ARGUMENTS
Container inventory: !bash "${CLAUDE_PLUGIN_ROOT}/skills/container-audit/scripts/list-dockerfiles.sh"
Live mode availability: !which docker 2>/dev/null || echo "docker:unavailable" · !which trivy 2>/dev/null || echo "trivy:unavailable"
Audit Phases
Phase 1: Discovery & Mode Selection
- Parse inventory output. Separate Dockerfiles, docker-compose files,
.dockerignore files.
- Confirm scope and mode with the user.
- In
--live mode, verify Docker daemon reachable; fall back to static if not.
Phase 2: Per-Dockerfile Snapshot
For each Dockerfile, extract:
- FROM chain (stage names, base images, tags, digests)
- USER directives
- HEALTHCHECK
- ENTRYPOINT / CMD (exec form vs shell form)
- EXPOSE / ENV / ARG
- COPY patterns (source paths,
--from references, --chown)
- RUN commands (shell-parsed)
- BuildKit-specific syntax (
--mount=type=secret, --mount=type=cache)
In --live mode, additionally:
- Run
docker history <image> for size-per-layer breakdown.
- Run
trivy image <image> --severity HIGH,CRITICAL --quiet (or grype, snyk container) for CVE surface.
- Run
docker inspect <image> for final USER, Entrypoint, and exposed ports.
Phase 3: Parallel Sub-Agent Audit
Spawn one Agent(subagent_type=Explore) per Dockerfile (single assistant message). Each walks categories A–H from reference.md §1:
- A. Base image — distroless/alpine/scratch where possible, pinned by digest, from a trusted registry
- B. User & privileges — explicit non-root USER before ENTRYPOINT, dropped capabilities, read-only FS hint
- C. Secrets & leaks — no secrets in ARG/ENV, no copied
.env files, no hardcoded tokens, BuildKit secrets for build-time
- D. Layer efficiency — multi-stage build,
COPY --from= for artefact, RUN combined with cleanup, layer cache-friendly order
- E. Signals & shutdown — tini/dumb-init for PID 1, STOPSIGNAL, exec-form CMD (JSON array, not shell)
- F. Healthcheck — HEALTHCHECK present, interval sensible, command meaningful (not just
curl localhost)
- G.
.dockerignore — excludes .git, node_modules, tests/, .env*, OS junk
- H. docker-compose — no
privileged: true, no network_mode: host, named volumes, resource limits, no user: root
Phase 4: Merge & Risk Register
Merge sub-agent output. Dedupe via .container-ignore. Assign CT-001… IDs in severity-then-category order.
Phase 5: Remediation Drafting
For every CRITICAL/HIGH/MEDIUM finding, append a commented RUN/USER/HEALTHCHECK block to dockerfile-suggested.patch with the target file:line, evidence, and suggested fix.
Phase 6: Apply Mode (opt-in)
When --apply, iterate findings interactively with [a]pply / [s]kip / [A]ll / [q]uit. Edit Dockerfiles via Edit. Destructive changes (removing a FROM, ENTRYPOINT) need DESTROY confirmation.
Phase 7: Reporting
Write container-audit.md + container-audit.json + dockerfile-suggested.patch (and image-scan.json in --live mode with Trivy output).
Scoring
Category weights: A=15, B=20, C=20, D=10, E=5, F=10, G=10, H=10 (sum 100). See reference.md §3.
| Total |
Verdict |
| 90–100 |
PASS |
| 70–89 |
PASS WITH WARNINGS |
| 50–69 |
CONDITIONAL |
| <50 |
FAIL |
Important Principles
- Secrets at build time use BuildKit, not ARG.
ARG TOKEN= is baked into the image history.
USER must be set before the final CMD. Root at runtime is the default — flag every Dockerfile that doesn't explicitly switch.
- Multi-stage is the norm. Any Dockerfile that doesn't use multi-stage and ships a build toolchain is MEDIUM or HIGH depending on image size.
- Tag pins move.
FROM node:20-alpine will drift. FROM node:20.11.1-alpine3.19@sha256:<digest> is pinned.
latest in production is a finding. Always CRITICAL for production-tagged images.
- docker-compose
privileged: true is always CRITICAL unless a suppression explains why (e.g., a CI runner).
- Australian English. DD/MM/YYYY. Markdown-first.
Edge Cases
- No Dockerfile present. Report "no containers found" and exit cleanly.
- Dockerfile is generated by a framework (
next/standalone, nixpacks). Flag as "generated; framework-managed"; skip apply suggestions but audit the final output.
- Multi-arch builds (buildx). Audit each stage independently; flag if arch-specific base images differ in patch level.
- Windows-based images. Base-image checks differ — skip alpine/distroless suggestions, check
servercore vs nanoserver.
distroless images without a shell. Healthcheck must use the image's exec format, not CMD-SHELL.
- docker-compose with override files. Parse base + all overrides; findings target the file where the issue originates.
- Compose uses
secrets: top-level. Good — don't flag. Flag when secrets are inlined in env instead.
1---2name: container-audit3description: Audit Dockerfiles and docker-compose files for base image, user privileges, secret leaks, layer efficiency, signal handling, healthchecks, and compose safety. One sub-agent per Dockerfile. Static, live (Trivy/Grype), and apply modes.4---5
6# Container Audit
7
8<!-- anthril-output-directive -->
9> **Output path directive (canonical — overrides in-body references).**
10> All file outputs from this skill MUST be written under `.anthril/audits/container-audit/`.
11> Run `mkdir -p .anthril/audits/container-audit` before the first `Write` call.
12> Primary artefact: `.anthril/audits/container-audit/<artefact>`.
13> Do NOT write to the project root or to bare filenames at cwd.
14> Lifestyle plugins are exempt from this convention — this skill is not lifestyle.
15
16## When to use
17
18Run this skill when the user mentions:
19- Dockerfile review, container security, image hardening
20- CIS Docker Benchmark
21- docker-compose audit
22- Image size optimisation
23- Secret leaks in container builds
24
25Covers eight categories: base image choice (distroless/alpine, digest pinning), user privileges (non-root, dropped capabilities, read-only filesystem), secret leaks (no ARG/ENV secrets, BuildKit `--mount=type=secret`), layer efficiency (multi-stage, `COPY --from`, cache ordering), signal handling (tini, STOPSIGNAL, exec-form CMD), healthchecks, `.dockerignore` coverage, and docker-compose safety (no `privileged: true`, no host network, resource limits).
26
27## Before You Start
28
291. **Determine operating mode.** `--live` enables `docker inspect`, `docker history`, and Trivy/Grype scans. `--apply` enables per-finding Dockerfile patching. `--runtime` is not applicable for this skill (there is no safe runtime test for a Dockerfile without a target environment).
302. **Find all Dockerfiles and compose files.** Run `scripts/list-dockerfiles.sh`.
313. **Sub-agent budget.** One `Agent(subagent_type=Explore)` per Dockerfile. Warn above 10 files.
324. **Load `.container-ignore`** for suppression rules.
33
34## User Context
35
36$ARGUMENTS
37
38Container inventory: !`bash "${CLAUDE_PLUGIN_ROOT}/skills/container-audit/scripts/list-dockerfiles.sh"`
39
40Live mode availability: !`which docker 2>/dev/null || echo "docker:unavailable"` · !`which trivy 2>/dev/null || echo "trivy:unavailable"`
41
42---
43
44## Audit Phases
45
46### Phase 1: Discovery & Mode Selection
47
481. Parse inventory output. Separate Dockerfiles, docker-compose files, `.dockerignore` files.
492. Confirm scope and mode with the user.
503. In `--live` mode, verify Docker daemon reachable; fall back to static if not.
51
52### Phase 2: Per-Dockerfile Snapshot
53
54For each Dockerfile, extract:
55
56- FROM chain (stage names, base images, tags, digests)
57- USER directives
58- HEALTHCHECK
59- ENTRYPOINT / CMD (exec form vs shell form)
60- EXPOSE / ENV / ARG
61- COPY patterns (source paths, `--from` references, `--chown`)
62- RUN commands (shell-parsed)
63- BuildKit-specific syntax (`--mount=type=secret`, `--mount=type=cache`)
64
65In `--live` mode, additionally:
66- Run `docker history <image>` for size-per-layer breakdown.
67- Run `trivy image <image> --severity HIGH,CRITICAL --quiet` (or `grype`, `snyk container`) for CVE surface.
68- Run `docker inspect <image>` for final USER, Entrypoint, and exposed ports.
69
70### Phase 3: Parallel Sub-Agent Audit
71
72Spawn one `Agent(subagent_type=Explore)` per Dockerfile (single assistant message). Each walks categories A–H from `reference.md` §1:
73
74- **A. Base image** — distroless/alpine/scratch where possible, pinned by digest, from a trusted registry
75- **B. User & privileges** — explicit non-root USER before ENTRYPOINT, dropped capabilities, read-only FS hint
76- **C. Secrets & leaks** — no secrets in ARG/ENV, no copied `.env` files, no hardcoded tokens, BuildKit secrets for build-time
77- **D. Layer efficiency** — multi-stage build, `COPY --from=` for artefact, RUN combined with cleanup, layer cache-friendly order
78- **E. Signals & shutdown** — tini/dumb-init for PID 1, STOPSIGNAL, exec-form CMD (JSON array, not shell)
79- **F. Healthcheck** — HEALTHCHECK present, interval sensible, command meaningful (not just `curl localhost`)
80- **G. `.dockerignore`** — excludes `.git`, `node_modules`, `tests/`, `.env*`, OS junk
81- **H. docker-compose** — no `privileged: true`, no `network_mode: host`, named volumes, resource limits, no `user: root`
82
83### Phase 4: Merge & Risk Register
84
85Merge sub-agent output. Dedupe via `.container-ignore`. Assign `CT-001…` IDs in severity-then-category order.
86
87### Phase 5: Remediation Drafting
88
89For every CRITICAL/HIGH/MEDIUM finding, append a commented `RUN`/`USER`/`HEALTHCHECK` block to `dockerfile-suggested.patch` with the target file:line, evidence, and suggested fix.
90
91### Phase 6: Apply Mode (opt-in)
92
93When `--apply`, iterate findings interactively with `[a]pply / [s]kip / [A]ll / [q]uit`. Edit Dockerfiles via `Edit`. Destructive changes (removing a `FROM`, `ENTRYPOINT`) need `DESTROY` confirmation.
94
95### Phase 7: Reporting
96
97Write `container-audit.md` + `container-audit.json` + `dockerfile-suggested.patch` (and `image-scan.json` in `--live` mode with Trivy output).
98
99---
100
101## Scoring
102
103Category weights: A=15, B=20, C=20, D=10, E=5, F=10, G=10, H=10 (sum 100). See `reference.md` §3.
104
105| Total | Verdict |
106|---|---|
107| 90–100 | PASS |
108| 70–89 | PASS WITH WARNINGS |
109| 50–69 | CONDITIONAL |
110| <50 | FAIL |
111
112---
113
114## Important Principles
115
116- **Secrets at build time use BuildKit, not ARG.** `ARG TOKEN=` is baked into the image history.
117- **`USER` must be set before the final CMD.** Root at runtime is the default — flag every Dockerfile that doesn't explicitly switch.
118- **Multi-stage is the norm.** Any Dockerfile that doesn't use multi-stage and ships a build toolchain is MEDIUM or HIGH depending on image size.
119- **Tag pins move.** `FROM node:20-alpine` will drift. `FROM node:20.11.1-alpine3.19@sha256:<digest>` is pinned.
120- **`latest` in production is a finding.** Always CRITICAL for production-tagged images.
121- **docker-compose `privileged: true` is always CRITICAL** unless a suppression explains why (e.g., a CI runner).
122- **Australian English. DD/MM/YYYY. Markdown-first.**
123
124---
125
126## Edge Cases
127
1281. **No Dockerfile present.** Report "no containers found" and exit cleanly.
1292. **Dockerfile is generated by a framework (`next/standalone`, `nixpacks`).** Flag as "generated; framework-managed"; skip apply suggestions but audit the final output.
1303. **Multi-arch builds (buildx).** Audit each stage independently; flag if arch-specific base images differ in patch level.
1314. **Windows-based images.** Base-image checks differ — skip alpine/distroless suggestions, check `servercore` vs `nanoserver`.
1325. **`distroless` images without a shell.** Healthcheck must use the image's exec format, not `CMD-SHELL`.
1336. **docker-compose with override files.** Parse base + all overrides; findings target the file where the issue originates.
1347. **Compose uses `secrets:` top-level.** Good — don't flag. Flag when secrets are inlined in env instead.