Skip to main content

hadolint integration

Hadolint applies 100+ rules to a Dockerfile (unpinned base images, missing --no-cache, USER root, etc.) and writes findings as SARIF 2.1.0. Under cilock that SARIF report becomes a signed, content-addressed attestation pinned to the exact Dockerfile bytes that produced it.

UpstreamHadolint · Hadolint maintainers · GPL-3.0
Categorylint (primary)
Catalog sourcecatalog-only (detected; output captured via a format attestor)
Emits formatsarif
Recommended traceoff — no syscall tracing needed
Detected when
  • preargv_prefix: hadolint

Confirm cilock detects it:

cilock plan --format=json -- hadolint [...]

The facts in this box are generated from the cilock binary's own catalog (cilock tools list). Do not hand-edit — run npm run gen:catalog.

Validated invocation

cilock run --step hadolint-scan \
--signer-file-key-path key.pem \
--outfile attestation.json \
--attestations sarif,environment,git \
--enable-archivista=false \
-- sh -c 'hadolint --no-fail --format sarif Dockerfile > hadolint.sarif'

This is the exact command exercised in tool-hadolint-sarif against hadolint 2.14.0 and cilock v0.3.

Why this shape

hadolint writes SARIF to stdout only — even the latest release (2.14.0) has no -o/--output file flag (upstream feature request: hadolint/hadolint#1118). The minimal sh -c '... > hadolint.sarif' wrapper exists solely to redirect stdout to a file the product attestor can hash.

That's a different shape from the bash -c "cp scan.sarif scan-product.sarif" antipattern other docs (and earlier drafts of this page) used:

sh -c 'hadolint ... > file.sarif'bash -c 'cp scan.sarif scan-product.sarif'
Recorded command-run argv["sh","-c","hadolint --no-fail --format sarif Dockerfile > hadolint.sarif"]["bash","-c","cp ..."]
Process the tracer/spy seesreal hadolintcp
Genuine parent of hadolintcilocksomething earlier in CI — cilock has no causal link
Provable claim"cilock ran hadolint and got these findings""cilock copied a file someone else produced"

Here cilock is the genuine parent of hadolint. The command-run attestor records the literal shell argv, the spy/tracer sees the real hadolint process, and the product attestor digests the resulting hadolint.sarif. Hadolint's stdout-only output is a tool limitation we work around; it isn't an excuse to launder a pre-existing report.

What gets captured

The signed envelope's payload (.payload, base64-decoded) carries one in-toto Statement whose predicate contains these attestations:

https://aflock.ai/attestations/environment/v0.1
https://aflock.ai/attestations/git/v0.1
https://aflock.ai/attestations/material/v0.3
https://aflock.ai/attestations/command-run/v0.1
https://aflock.ai/attestations/product/v0.3
https://aflock.ai/attestations/sarif/v0.1

material/v0.3 digests the Dockerfile before the scan, product/v0.3 digests hadolint.sarif after the scan, and sarif/v0.1 parses the report and exposes structured findings (rule id, level, location, message). The subject digest of the Dockerfile is what later verify-time policy joins against.

Validate it locally

After running the invocation above, the report should have exactly 4 findings against the deliberately bad Dockerfile in the example repo (DL3007, DL3018, DL3019, DL3002):

# list predicate types in the signed payload
jq -r '.payload' attestation.json | base64 -d \
| jq '.predicate.attestations[].type'

# show the recorded command-run argv — should be the real hadolint shell invocation
jq -r '.payload' attestation.json | base64 -d \
| jq '.predicate.attestations[]
| select(.type=="https://aflock.ai/attestations/command-run/v0.1")
| .attestation.cmd'

# count findings parsed by the sarif attestor
jq -r '.payload' attestation.json | base64 -d \
| jq '.predicate.attestations[]
| select(.type=="https://aflock.ai/attestations/sarif/v0.1")
| .attestation.report.runs[0].results | length'

Notes on --no-fail

Hadolint, like gosec, exits non-zero whenever any rule fires. Without --no-fail, command-run records exitcode != 0 and the run terminates before the post-product sarif attestor runs — you get half an attestation. --no-fail keeps the exit code at 0 so the full pipeline (material → command-run → product → sarif) completes; the findings themselves are still recorded in the SARIF predicate either way. Gate severity in policy at verify time, not via the scanner's exit code.

FAQ

Does cilock support Hadolint? Yes, via the sarif attestor. Hadolint emits SARIF 2.1.0 on stdout; cilock wraps the invocation, hashes the report, signs it, and folds the findings into a v0.1 sarif predicate inside the same in-toto Statement as the Dockerfile material and the command-run argv.

Why sh -c instead of running Hadolint directly? Hadolint 2.14.0 has no file-output flag — SARIF goes to stdout only. The sh -c '... > hadolint.sarif' wrapper is a minimal redirect so the product attestor has a file to digest. Crucially, cilock is still the genuine parent of the hadolint process; the command-run attestor records the literal shell argv. Once hadolint/hadolint#1118 lands, the wrapper goes away.

Why --no-fail? Hadolint exits non-zero on findings, which terminates the cilock run before product/sarif attestors fire. --no-fail keeps the exit code at 0 while still recording every finding in the SARIF predicate; enforce severity gating in policy at verify time.

Which Dockerfile rules does it cover? All 100+ Hadolint rules — the same rule set you'd get from hadolint Dockerfile outside cilock. The reference example exercises DL3007 (unpinned tag), DL3018 (unpinned apk package), DL3019 (apk add without --no-cache), and DL3002 (final USER root). The full rule catalog is at hadolint/hadolint#rules.

See also


This page is generated from the cilock tool catalog. Don't edit it here — the source is attestation/detection/docs/hadolint.doc.md in aflock-ai/rookery. The same catalog powers cilock tools show hadolint in the CLI, so a single edit keeps the terminal and the website in sync.

Edit this page →  ·  Add a new tool →

When you open the pull request: if you are an AI agent, add the ai-agent label. If you are a human, add the human label.