Skip to content

v0.10.0

Choose a tag to compare

@github-actions github-actions released this 25 May 12:07
· 31 commits to main since this release
v0.10.0
94ddcaa

Added

  • SARIF results now carry a stable partialFingerprints value
    (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-pr and cflite-batch
    workflow checkouts now set persist-credentials: false like every other
    workflow, so the GITHUB_TOKEN is not left in .git/config while PR-author
    code runs during fuzzing. The fuzz image's COPY . 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) reported line=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 embedded references= 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 --privileged is 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 the test: ["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.sock in addition to docker.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 adds PASSPHRASE and ENCRYPTION_KEY to
    the credential-key list (a generic _KEY suffix is deliberately not matched
    — it false-positives on LICENSE_KEY etc.). CL-0011 adds the SYS_BOOT,
    DAC_OVERRIDE, and BPF capabilities; CL-0016 adds the /dev/fuse and
    /dev/kmsg devices. (#279)

  • SARIF rule descriptors are now correct in three ways. helpUri is 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 in help.text. A
    config severity: override now reaches defaultConfiguration.level and
    properties.security-severity on 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. critical no longer showed Medium while JSON and SARIF
    disagreed. And a finding's structured fixes[] are matched to the finding by
    logical identity (rule, line, service, message) rather than id(), 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 up assigns a
    random (ephemeral) host port bound to all interfaces (0.0.0.0 and [::]) —
    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 with 127.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
    and redis://:password@host is the standard Redis URL form. The
    password-is-a-$VAR skip is unchanged. (#279)

  • .compose-lint.yml no longer silently ignores misconfiguration that would
    leave a security control at its default. An unknown rule id (a typo'd
    CL-001 or a retired CL-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. And enabled must be a real boolean: a quoted 'false' or a 0 is
    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 bare false/no/off still
    parse to a real boolean and work.) (#279)

  • Text output: the SUPPRESSED marker 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 (previously FORCE_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 ruleIndex for an unregistered rule.
    ruleIndex defaulted to 0, so a result whose rule was absent from the
    registry pointed at the first rule (CL-0001) while ruleId named 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
    region instead of fabricating startLine: 1, which had mislocated the alert
    at the top of the file. (#278)

  • SARIF $schema now points at the canonical, immutable OASIS errata01 URL
    (docs.oasis-open.org/.../sarif-schema-2.1.0.json) instead of a
    raw.githubusercontent.com main-branch link — the schema's own $id, and
    no longer a mutable ref. (#278)

  • SARIF artifactLocation.uri is 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 a SRCROOT uriBaseId, declared once per run in
    originalUriBaseIds alongside invocations[].workingDirectory; out-of-tree
    paths fall back to an absolute, percent-encoded file: URI. (#278)

  • JSON output now emits service as a string and never emits bare NaN/
    Infinity. A service name is a YAML mapping key, so a key like true, a bare
    number, or .nan resolved to a non-string scalar: .nan produced invalid
    JSON ("service": NaN, which RFC 8259 forbids) while true/123 produced a
    wrongly-typed service field (ADR-015 contracts it as a string). The formatter
    now coerces service to str, and both the JSON and SARIF dumps use
    allow_nan=False so 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 with privileged: true followed by privileged: 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 the CAP_ prefix as optional, but the rule keyed on the
    bare name and missed the prefixed form entirely. (#277)

  • CL-0017 now flags rshared mount propagation in both short and long syntax,
    not just shared. rshared is 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_opt directives 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 with no-new-privileges=true. A
    shared normalize_security_opt helper 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:221342), so the rule's
    str(port) saw no colon and reported the file clean. LineLoader now drops
    the sexagesimal int/float resolver alternatives and the timestamp
    resolver (a bare date like 2024-01-01 was becoming a non-JSON-serializable
    datetime.date), while keeping YAML 1.1 booleans — Docker coerces
    yes/no/on/off to booleans for boolean-typed fields, so keeping them
    preserves CL-0002/CL-0007 parity with docker compose config. (#277)

  • Compose override-file tags !reset and !override no longer make a valid
    file fail to parse (exit 2). LineLoader (a SafeLoader subclass) had no
    constructor for them, so it raised a ConstructorError; 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 ComposeError instead
    of an uncaught UnicodeDecodeError. Previously one bad-encoding file aborted
    an entire directory sweep. (#277)

  • The fix engine no longer adds no-new-privileges:true to either side of an
    extends relationship. Docker concatenates list fields like security_opt
    across an extends merge, so adding the entry to a service that extends:
    another — or to a base another service extends — could produce a duplicated
    item that docker compose config rejects. The duplicate only exists after
    Docker's merge (our parser does not resolve extends), 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)