v0.10.0
Added
- SARIF results now carry a stable
partialFingerprintsvalue
(composeLintFinding/v1). GitHub Code Scanning uses it to deduplicate
uploads and track an alert across commits; without it, direct SARIF uploads
produced duplicate alerts and lost continuity when code moved. The digest is
derived from the finding's logical identity (file, rule, service, message) and
deliberately excludes the line number, so an alert survives unrelated line
shifts. Additive to the SARIF contract (ADR-015). (#278)
Security
- ClusterFuzzLite hygiene (issue #279). The
cflite-prandcflite-batch
workflow checkouts now setpersist-credentials: falselike every other
workflow, so theGITHUB_TOKENis not left in.git/configwhile PR-author
code runs during fuzzing. The fuzz image'sCOPY .no longer ingests
CLAUDE.md/AGENTS.md— they are added to.dockerignore. (#279)
Fixed
-
Parser line-map robustness (issue #279 E2/E3). A service (or any key) named
__lines__is no longer silently dropped: the loader's line map now hangs off
a private non-string sentinel key instead of the literal string"__lines__",
so it can't collide with user data — a security linter must not skip a service.
And a service that both defines a YAML anchor and is aliased elsewhere now
resolves its own line: previously the alias and the anchor-definer shared one
dict, and only whichever the traversal reached first got its keys recorded, so
the other (often the definer — the most obvious location) reportedline=None.
Line numbers are now recorded per reachable path while the subtree is still
walked once, so the chained-alias DoS guard (issue #154) is preserved. (#279) -
Documentation and grounding drift corrected (issue #279 D1–D6). OWASP
renumbered the Docker Security Cheat Sheet and switched its anchors to a
single-dash slug, so every citation was either pointing at the wrong rule or
landing at page top. All OWASP deep links (rule docs, the README table, and
the embeddedreferences=URLs in code) now use the live single-dash anchors,
and four drifted citations are corrected: CL-0002 and CL-0011 → Rule #3 (Limit
capabilities, where--privilegedis discussed), CL-0003 → Rule #4 (Prevent
in-container privilege escalation), CL-0018 → Rule #2 (Set a user), CL-0020 and
CL-0021 → Rule #12 (Utilize Docker Secrets). CL-0002's finding message no
longer overclaims "functionally equivalent to host root" — it now matches the
doc's "trivially escapable to host root." The CL-0018 doc now reflects that
the rule fires on any root user portion regardless of group (root:1000),
and the CL-0015 doc now documents thetest: ["NONE"]branch the code already
implements. (#279) -
Rule coverage gaps closed (issue #279 R3/R4/R5). CL-0001 now flags any
container-runtime control socket —containerd.sock,crio.sock, and
podman.sockin addition todocker.sock(podman/crio were caught by no
rule before); the rule is retitled "Container runtime socket mounted" and its
message names the runtime. CL-0020 addsPASSPHRASEandENCRYPTION_KEYto
the credential-key list (a generic_KEYsuffix is deliberately not matched
— it false-positives onLICENSE_KEYetc.). CL-0011 adds theSYS_BOOT,
DAC_OVERRIDE, andBPFcapabilities; CL-0016 adds the/dev/fuseand
/dev/kmsgdevices. (#279) -
SARIF rule descriptors are now correct in three ways.
helpUriis set only
to a reference that is actually a URI — rules grounded in a CIS benchmark
(CL-0012, CL-0015, CL-0016, CL-0017) emitted the benchmark prose as
helpUri, which SARIF 2.1.0 declares"format": "uri"and strict validators
/ GitHub Code Scanning reject; the prose still appears inhelp.text. A
configseverity:override now reachesdefaultConfiguration.leveland
properties.security-severityon the rule descriptor, not just the per-result
level— GitHub derives an alert's severity column from the rule, so an
override to e.g.criticalno longer showed Medium while JSON and SARIF
disagreed. And a finding's structuredfixes[]are matched to the finding by
logical identity (rule, line, service, message) rather thanid(), so a
future refactor that copies findings can't silently drop every fix. (#279) -
A rule that raises no longer aborts the entire run. Previously an uncaught
exception from any rule escaped as a traceback and exited 1 —
indistinguishable from a normal "findings at/above threshold" result, and in a
directory sweep every remaining file was lost. The engine now isolates each
rule per service: a failure is reported to stderr and the run continues, and
the CLI maps it to exit 2 ("compose-lint itself couldn't run", ADR-006) so a
crash is never mistaken for a clean lint failure. (#279) -
CL-0005 now flags a bare short-syntax port with no colon (
"3000",3001, a
"3000-3005"range). Docker still publishes it —docker compose upassigns a
random (ephemeral) host port bound to all interfaces (0.0.0.0and[::]) —
so it is the same exposure class the rule targets, and it is the most common
port form in real homelab files. The finding notes the host port is ephemeral
and the guidance binds it to localhost with127.0.0.1::<port>. The in-scalar
autofixer refuses this form (it can't synthesize the empty-host-port syntax).
(#279) -
CL-0021 now flags a password-only userinfo (
scheme://:password@host). The
regex required a non-empty username, but RFC 3986 §3.2.1 permits an empty one
andredis://:password@hostis the standard Redis URL form. The
password-is-a-$VARskip is unchanged. (#279) -
.compose-lint.ymlno longer silently ignores misconfiguration that would
leave a security control at its default. An unknown rule id (a typo'd
CL-001or a retiredCL-9999), an unrecognized top-level key (a misplaced
fail_on:), or an unknown per-rule key (severty:) now prints a stderr
warning instead of being dropped — mirroring the existing unknown-service
warning. Andenabledmust be a real boolean: a quoted'false'or a0is
now a hard error (exit 2) rather than a silent no-op that left the rule
running while the user believed it off. (YAML's barefalse/no/offstill
parse to a real boolean and work.) (#279) -
Text output: the
SUPPRESSEDmarker no longer pushes a suppressed finding's
rule and message columns out of alignment — the severity column is padded to
fit the marker so every row lines up. CL-0020 and CL-0021 (credential-shaped
env keys and inline connection-string credentials) now render the source
excerpt and underline like the other value-naming rules; they had been left
out of the presence-rule set.FORCE_COLOR=0/false(case-insensitive) now
disables color and any other set value — including the empty string — enables
it, matching the chalk/supports-color convention (previouslyFORCE_COLOR=false
turned color on). The excerpt underline now matches the value at a token
boundary and measures display width (East-Asian wide and combining characters),
so it no longer mis-points on a value that is a substring of a longer token or
contains CJK/accented characters. (#278) -
SARIF no longer emits a misleading
ruleIndexfor an unregistered rule.
ruleIndexdefaulted to0, so a result whose rule was absent from the
registry pointed at the first rule (CL-0001) whileruleIdnamed the real one
— a SARIF §3.52.5 contradiction. It is now emitted only when the rule is in
the registry. A result with an unknown or non-positive line likewise omits its
regioninstead of fabricatingstartLine: 1, which had mislocated the alert
at the top of the file. (#278) -
SARIF
$schemanow points at the canonical, immutable OASIS errata01 URL
(docs.oasis-open.org/.../sarif-schema-2.1.0.json) instead of a
raw.githubusercontent.commain-branch link — the schema's own$id, and
no longer a mutable ref. (#278) -
SARIF
artifactLocation.uriis now a conformant, GitHub-resolvable URI
reference. Paths were emitted verbatim, so an absolute path would not resolve
on GitHub Code Scanning and a space or non-ASCII byte
(/tmp/my dir/café.yml) was not a legal RFC-3986 URI reference at all. Files
under the working directory are now emitted as percent-encoded repo-relative
paths tagged with aSRCROOTuriBaseId, declared once per run in
originalUriBaseIdsalongsideinvocations[].workingDirectory; out-of-tree
paths fall back to an absolute, percent-encodedfile:URI. (#278) -
JSON output now emits
serviceas a string and never emits bareNaN/
Infinity. A service name is a YAML mapping key, so a key liketrue, a bare
number, or.nanresolved to a non-string scalar:.nanproduced invalid
JSON ("service": NaN, which RFC 8259 forbids) whiletrue/123produced a
wrongly-typedservicefield (ADR-015 contracts it as a string). The formatter
now coercesservicetostr, and both the JSON and SARIF dumps use
allow_nan=Falseso a stray non-finite float raises instead of writing invalid
JSON. (#278) -
Duplicate mapping keys are now rejected with a parse error, matching Docker
(which refuses them). Previously PyYAML silently let the last value win, so a
service withprivileged: truefollowed byprivileged: false— a file
Docker will not load — reported clean, and the line map pointed at the wrong
occurrence. Detection runs before merge-key (<<) flattening, so an
extends/anchor merge that overrides an inherited key is not misreported as a
duplicate. (#277) -
CL-0011 now flags
CAP_-prefixed capabilities (CAP_SYS_ADMIN,CAP_ALL,
...). Docker treats theCAP_prefix as optional, but the rule keyed on the
bare name and missed the prefixed form entirely. (#277) -
CL-0017 now flags
rsharedmount propagation in both short and long syntax,
not justshared.rsharedis the recursive — and more common — form that
still propagates container mounts to the host. (#277) -
CL-0005 now evaluates the bind-address slot when the host port is a
${VAR}
substitution (${HOSTPORT}:80). Previously a var-valued host port failed the
port pattern and the whole entry was skipped, hiding a wildcard publish. (#277) -
CL-0021 now flags an inline connection-string credential when the username is
a${VAR}but the password is a literal (postgres://${DB_USER}:secret@db).
Only a var-valued password means the secret is parameterized. (#277) -
CL-0020 now flags an unquoted numeric credential value (
DB_PASSWORD: 12345678). The value decodes to an int and was skipped; it is coerced to its
string form before the checks, while YAML boolean toggles stay exempt. (#277) -
security_optdirectives are now matched with their=separator treated as
equivalent to:, the way Docker accepts them. CL-0009 was missing an
=-form profile disable (seccomp=unconfined,label=disable) and CL-0003
was firing on a service already hardened withno-new-privileges=true. A
sharednormalize_security_opthelper canonicalizes the separator (and case)
before every membership/prefix check across the rules and the fix engine.
(#277) -
CL-0005 no longer misses short-syntax ports whose host and container sides are
both<= 59(22:22,25:25,53:53, ...). PyYAML's YAML 1.1 resolvers
parsed these as a single base-60 integer (22:22→1342), so the rule's
str(port)saw no colon and reported the file clean.LineLoadernow drops
the sexagesimalint/floatresolver alternatives and thetimestamp
resolver (a bare date like2024-01-01was becoming a non-JSON-serializable
datetime.date), while keeping YAML 1.1 booleans — Docker coerces
yes/no/on/offto booleans for boolean-typed fields, so keeping them
preserves CL-0002/CL-0007 parity withdocker compose config. (#277) -
Compose override-file tags
!resetand!overrideno longer make a valid
file fail to parse (exit 2).LineLoader(aSafeLoadersubclass) had no
constructor for them, so it raised aConstructorError; it now constructs the
underlying value and ignores the merge directive, which is all the linter
needs. (#277) -
A non-UTF-8 (e.g. latin-1) file now raises a per-file
ComposeErrorinstead
of an uncaughtUnicodeDecodeError. Previously one bad-encoding file aborted
an entire directory sweep. (#277) -
The
fixengine no longer addsno-new-privileges:trueto either side of an
extendsrelationship. Docker concatenates list fields likesecurity_opt
across anextendsmerge, so adding the entry to a service thatextends:
another — or to a base another service extends — could produce a duplicated
item thatdocker compose configrejects. The duplicate only exists after
Docker's merge (our parser does not resolveextends), so the post-apply
reparse guard could not catch it. Both the per-finding CL-0003 fixer and the
CL-0003/CL-0009 coordination pass now refuse both sides and leave the chain
for manual review. (#276, #277)