feat(containers): mutually exclusive image vs dockerfile, per-target cleanup, IDE schema hint#121
Conversation
… cleanup, IDE schema hint Three coupled UX improvements to the containers config block, following the user's review of the option-A schema clarity question. 1. Schema option A: image and dockerfile are mutually exclusive. ``image:`` is for remote pulls only — argus pulls and scans the reference. ``dockerfile:`` (with optional ``context:`` and ``name:``) is for local build-then-scan. The previous shape doubled ``image:`` as both "pull source" and "tag the build as", which was the docker-compose precedent — argus doesn't push images after building, so the compose semantic was never load-bearing for us. The cleaner separation reflects what the tool actually does. Schema validator now rejects entries that have both keys with a clear remediation message. Build entries auto-derive the on-disk image tag from ``name:`` (or from the Dockerfile path when ``name:`` is omitted) — same logic as ``argus scan container --discover`` so explicit-config and auto-discovery produce the same image identifiers locally. Stale configs that still set both keys log a warning and skip the entry instead of crashing the parser, so a half-migrated argus.yml still gets through the engine while surfacing the issue. 2. Per-target ``cleanup:`` override. ``cleanup: true|false`` on a single image entry overrides the engine-level default for that target only. Useful for the case where one base image should stay cached across runs while the rest of the dev images are torn down — set ``cleanup: false`` on the base entry, leave the others alone. ``None`` (the default) defers to the engine-wide setting. 3. IDE schema directive in argus.yml. ``argus init`` already wrote a ``# yaml-language-server: $schema=...`` line at the top of generated configs. ``argus validate`` now warns when an existing config is missing that line so users with hand-edited configs get a one-step path back to IDE-side schema validation in VS Code, IntelliJ, and similar tools that speak the language-server-protocol YAML spec. The dogfood ``argus.yml`` and ``argus.example.yml`` both gain the directive at line 1. Workflow update: ``.github/workflows/build-containers.yml`` matrix preflight previously read ``.image`` from each argus.yml entry. The dogfood entries now use the build-only shape, so the yq query reads ``.name`` instead and the build step still produces the same ``ghcr.io/huntridge-labs/argus/<name>:<sha>`` tag for publishing. Documentation: argus.example.yml's ``containers:`` comment block was rewritten to walk through both shapes side by side, with the mutual-exclusion rule called out and the ``cleanup:`` per-target override documented. Tests: 1567 passing (was 1557; ten new). Existing tests that asserted the old image+dockerfile shape were updated to use option A.
E2E Test Coverage Report
Summary
✅ All Actions Have E2E Coverage |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
🔒 Argus Container Security ScanBranch: 📊 Combined Findings Summary
Scanned: 4 containers | Build Failures: 0 📦 Container Breakdown
🔍 Detailed Findings by Container🚨 cli - 29 vulnerabilities (23 unique)Image: Combined (Deduplicated)
🔷 Trivy Scanner (29 findings, 23 unique)
⚓ Grype Scanner (0 findings, 0 unique)✅ No vulnerabilities detected by Grype 🟡 scanner-bandit - 2 vulnerabilities (2 unique)Image: Combined (Deduplicated)
🔷 Trivy Scanner (2 findings, 2 unique)
⚓ Grype Scanner (0 findings, 0 unique)✅ No vulnerabilities detected by Grype
|
| 🚨 Critical | 🟡 Medium | 🔵 Low | Total | Unique | |
|---|---|---|---|---|---|
| 0 | 7 | 44 | 63 | 114 | 50 |
🔷 Trivy Scanner (114 findings, 49 unique)
| CVE | Severity | Package | Version | Fixed |
|---|---|---|---|---|
| CVE-2026-4878 | libcap2 | 1:2.75-10+b8 | N/A | |
| CVE-2025-69720 | libncursesw6 | 6.5+20250216-2 | N/A | |
| CVE-2026-29111 | libsystemd0 | 257.9-1~deb13u1 | N/A | |
| CVE-2025-69720 | libtinfo6 | 6.5+20250216-2 | N/A | |
| CVE-2026-29111 | libudev1 | 257.9-1~deb13u1 | N/A | |
| CVE-2025-69720 | ncurses-base | 6.5+20250216-2 | N/A | |
| CVE-2025-69720 | ncurses-bin | 6.5+20250216-2 | N/A | |
| CVE-2026-27456 | 🟡 MEDIUM | bsdutils | 1:2.41-5 | N/A |
| CVE-2026-3184 | 🟡 MEDIUM | bsdutils | 1:2.41-5 | N/A |
| CVE-2026-27456 | 🟡 MEDIUM | libblkid1 | 2.41-5 | N/A |
| CVE-2026-3184 | 🟡 MEDIUM | libblkid1 | 2.41-5 | N/A |
| CVE-2026-4046 | 🟡 MEDIUM | libc-bin | 2.41-12+deb13u2 | N/A |
| CVE-2026-4437 | 🟡 MEDIUM | libc-bin | 2.41-12+deb13u2 | N/A |
| CVE-2026-4438 | 🟡 MEDIUM | libc-bin | 2.41-12+deb13u2 | N/A |
| CVE-2026-5435 | 🟡 MEDIUM | libc-bin | 2.41-12+deb13u2 | N/A |
| CVE-2026-5450 | 🟡 MEDIUM | libc-bin | 2.41-12+deb13u2 | N/A |
| CVE-2026-5928 | 🟡 MEDIUM | libc-bin | 2.41-12+deb13u2 | N/A |
| CVE-2026-6238 | 🟡 MEDIUM | libc-bin | 2.41-12+deb13u2 | N/A |
| CVE-2026-4046 | 🟡 MEDIUM | libc6 | 2.41-12+deb13u2 | N/A |
| CVE-2026-4437 | 🟡 MEDIUM | libc6 | 2.41-12+deb13u2 | N/A |
| CVE-2026-4438 | 🟡 MEDIUM | libc6 | 2.41-12+deb13u2 | N/A |
| CVE-2026-5435 | 🟡 MEDIUM | libc6 | 2.41-12+deb13u2 | N/A |
| CVE-2026-5450 | 🟡 MEDIUM | libc6 | 2.41-12+deb13u2 | N/A |
| CVE-2026-5928 | 🟡 MEDIUM | libc6 | 2.41-12+deb13u2 | N/A |
| CVE-2026-6238 | 🟡 MEDIUM | libc6 | 2.41-12+deb13u2 | N/A |
| CVE-2026-27456 | 🟡 MEDIUM | liblastlog2-2 | 2.41-5 | N/A |
| CVE-2026-3184 | 🟡 MEDIUM | liblastlog2-2 | 2.41-5 | N/A |
| CVE-2026-34743 | 🟡 MEDIUM | liblzma5 | 5.8.1-1 | N/A |
| CVE-2026-27456 | 🟡 MEDIUM | libmount1 | 2.41-5 | N/A |
| CVE-2026-3184 | 🟡 MEDIUM | libmount1 | 2.41-5 | N/A |
| CVE-2026-27456 | 🟡 MEDIUM | libsmartcols1 | 2.41-5 | N/A |
| CVE-2026-3184 | 🟡 MEDIUM | libsmartcols1 | 2.41-5 | N/A |
| CVE-2026-40225 | 🟡 MEDIUM | libsystemd0 | 257.9-1~deb13u1 | N/A |
| CVE-2026-40226 | 🟡 MEDIUM | libsystemd0 | 257.9-1~deb13u1 | N/A |
| CVE-2026-4105 | 🟡 MEDIUM | libsystemd0 | 257.9-1~deb13u1 | N/A |
| CVE-2026-40225 | 🟡 MEDIUM | libudev1 | 257.9-1~deb13u1 | N/A |
| CVE-2026-40226 | 🟡 MEDIUM | libudev1 | 257.9-1~deb13u1 | N/A |
| CVE-2026-4105 | 🟡 MEDIUM | libudev1 | 257.9-1~deb13u1 | N/A |
| CVE-2026-27456 | 🟡 MEDIUM | libuuid1 | 2.41-5 | N/A |
| CVE-2026-3184 | 🟡 MEDIUM | libuuid1 | 2.41-5 | N/A |
| CVE-2026-27456 | 🟡 MEDIUM | login | 1:4.16.0-2+really2.41-5 | N/A |
| CVE-2026-3184 | 🟡 MEDIUM | login | 1:4.16.0-2+really2.41-5 | N/A |
| CVE-2026-27456 | 🟡 MEDIUM | mount | 2.41-5 | N/A |
| CVE-2026-3184 | 🟡 MEDIUM | mount | 2.41-5 | N/A |
| CVE-2026-5958 | 🟡 MEDIUM | sed | 4.9-2 | N/A |
| CVE-2026-5704 | 🟡 MEDIUM | tar | 1.35+dfsg-3.1 | N/A |
| CVE-2026-27456 | 🟡 MEDIUM | util-linux | 2.41-5 | N/A |
| CVE-2026-3184 | 🟡 MEDIUM | util-linux | 2.41-5 | N/A |
| CVE-2026-27171 | 🟡 MEDIUM | zlib1g | 1:1.3.dfsg+really1.3.1-1+b1 | N/A |
| CVE-2026-3219 | 🟡 MEDIUM | pip | 26.0.1 | N/A |
...and 64 more
⚓ Grype Scanner (0 findings, 0 unique)
✅ No vulnerabilities detected by Grype
⚠️ scanner-supply-chain - 9 vulnerabilities (9 unique)
Image: ghcr.io/huntridge-labs/argus/scanner-supply-chain:d759e18714426ce38646f547e5fb9886c816f712
Combined (Deduplicated)
| 🚨 Critical | 🟡 Medium | 🔵 Low | Total | Unique | |
|---|---|---|---|---|---|
| 0 | 4 | 5 | 0 | 9 | 9 |
🔷 Trivy Scanner (9 findings, 9 unique)
| CVE | Severity | Package | Version | Fixed |
|---|---|---|---|---|
| CVE-2026-32280 | stdlib | v1.26.1 | 1.25.9, 1.26.2 | |
| CVE-2026-32281 | stdlib | v1.26.1 | 1.25.9, 1.26.2 | |
| CVE-2026-32283 | stdlib | v1.26.1 | 1.25.9, 1.26.2 | |
| CVE-2026-33810 | stdlib | v1.26.1 | 1.26.2 | |
| CVE-2026-3219 | 🟡 MEDIUM | pip | 26.0.1 | N/A |
| CVE-2026-6357 | 🟡 MEDIUM | pip | 26.0.1 | 26.1 |
| CVE-2026-32282 | 🟡 MEDIUM | stdlib | v1.26.1 | 1.25.9, 1.26.2 |
| CVE-2026-32288 | 🟡 MEDIUM | stdlib | v1.26.1 | 1.25.9, 1.26.2 |
| CVE-2026-32289 | 🟡 MEDIUM | stdlib | v1.26.1 | 1.25.9, 1.26.2 |
⚓ Grype Scanner (0 findings, 0 unique)
✅ No vulnerabilities detected by Grype
Generated by Argus
…s output
The container-scan handler ``_cmd_container_scan`` never called
``get_logger("argus", verbose=...)`` like the source-scan handler
does at the top of its body. Without that setup, the ``argus`` logger
has no console handler attached, so every engine-level
``logger.info()`` and ``logger.debug()`` call drops on the floor.
Result: ``argus scan container --verbose`` produced a blank terminal
that just sat there for the duration of the scan, with no signal at
all about which image was being processed or which phase it was in.
Add the ``get_logger`` call inside the handler — same shape as the
source-scan path — so verbose mode now streams the full per-image,
per-phase log: which container is processing, which sub-scanner is
running, when the vulnerability DB updates, what tool stderr looks
like on failure, etc.
Verified locally: argus scan container --verbose now prints
``Scanning 1 container(s)``, ``[1/1] Processing scanner-bandit``,
``Running trivy via container``, ``Updating trivy vulnerability DB``,
and so on as each phase progresses.
Two coupled UX improvements that close the "I don't know what argus
is stuck on" gap surfaced during the option-A schema review.
1. Phase-aware container scan progress.
ContainerEngine accepts an optional ``progress_callback`` that's
invoked at each phase transition per image: ``pull``/``build`` on
start, ``scan`` after build completes, ``done`` on success or
failure. The CLI installs a callback that routes phase events to
the right surface based on the user's flag mix:
* Default (TTY): the spinner's status text updates in place
(``[2/4] scanner-opengrep — scan (12s)`` instead of the static
"Running container scan").
* --no-spinner OR non-TTY: phase events print as persistent
``[idx/total] name — phase (Ns)`` lines on stderr — useful for
CI logs and step-away terminals where the rolling spinner is
useless.
* --quiet: no-op (phase progress suppressed).
* --verbose / --debug: no-op (the logger already streams full
INFO/DEBUG output; the progress emitter would just duplicate it).
2. Verbosity flag re-shape — three orthogonal flags, one mental
model per flag.
The ``-v`` / ``--verbose`` / ``-vv`` ladder was confusing.
Replace with named flags whose names announce their purpose:
* (no flag) — phase-aware spinner with INFO-level engine logging.
The user sees enough to know the scan is making progress.
* --quiet / -q — suppress per-phase INFO lines and bump the
engine logger to WARNING. The spinner still draws unless
--no-spinner is also passed; final summary still prints.
* --debug — full firehose: subprocess output, DB updates, every
engine log line. ``--verbose`` is preserved as a backward-
compatible alias so existing CI scripts keep working.
The ``--quiet`` and ``--no-spinner`` flags target different
concerns and compose orthogonally:
* --quiet (log verbosity): suppress info messages
* --no-spinner (UI rendering): don't draw the animation
This matches *nix convention — git -q, pip -q, pytest -q all
suppress info but don't touch UI rendering.
New helper _configure_logger(args) maps the flag combination to
a stream-handler level: --verbose/--debug to DEBUG, --quiet to
WARNING, default to INFO. _cmd_container_scan now uses it.
Tests: 1580 passing (was 1567; ten more progress-emitter tests
covering each combination of --quiet, --verbose, --debug,
--no-spinner, and TTY-vs-non-TTY routing).
CLI reference regenerated for the new flags.
The four output-control flags (--quiet, --no-spinner, --debug, --verbose) each get individual help text in the auto-generated per- command tables, but the *compose pattern* — the orthogonality of log- verbosity (--quiet) vs UI rendering (--no-spinner) and the four UX modes that fall out — wasn't anywhere in the docs. The cli-reference header warns "Do not edit manually", so the new section is added via ``scripts/ci/gen_cli_docs.py`` so it survives every regeneration. Section sits between Global Options and Commands, displaying a four- row table with When-to-use and What-you-see columns plus a one-line note on the additional modes that fall out from composing flags.
Description
Three coupled UX improvements to the
containers:config block, surfaced during the option-A review with the user.Changes Made
Details
1. Schema option A —
image:anddockerfile:are mutually exclusiveBefore: each entry could declare
image:anddockerfile:andcontext:together, withimage:doing double duty as both pull source and build tag (the docker-compose pattern). After: each entry declares one shape:image:(optionalcleanup:)dockerfile:, optionalcontext:,name:,cleanup:docker build, tags as<name>:argus-scan, scans the resultArgus doesn't push images after building, so the compose
image:tag-after-build semantic was never load-bearing for us. The cleaner separation reflects what the tool actually does.Schema validator rejects entries with both keys with a clear remediation message:
Build entries auto-derive the on-disk image tag from
name:(or from the Dockerfile path whenname:is omitted) — same logic asargus scan container --discover, so explicit-config and auto-discovery produce the same image identifiers locally.Stale configs that still set both keys log a warning and skip the entry instead of crashing the parser, so a half-migrated
argus.ymlstill gets through the engine while surfacing the issue.2. Per-target
cleanup:overridecleanup: true|falseon a single image entry overrides the engine-level default for that target only. Useful for the case where one base image should stay cached across runs while the rest of the dev images get torn down — setcleanup: falseon the base entry, leave the others alone.None(the default) defers to the engine-wide setting.3. IDE schema directive in
argus.ymlargus initalready wrote a# yaml-language-server: $schema=...line at the top of generated configs.argus validatenow warns when an existing config is missing it so users with hand-edited configs get a one-step path back to IDE-side schema validation in VS Code, IntelliJ, and similar tools.The dogfood
argus.ymlandargus.example.ymlboth gain the directive at line 1.Workflow update —
.github/workflows/build-containers.ymlmatrix preflight previously read.imagefrom eachargus.ymlentry. The dogfood entries now use the build-only shape, so the yq query reads.nameinstead and the build step still produces the sameghcr.io/huntridge-labs/argus/<name>:<sha>tag for publishing.Documentation —
argus.example.yml'scontainers:comment block was rewritten to walk through both shapes side by side, with the mutual-exclusion rule called out and thecleanup:per-target override documented.Verification
argus validate:✅ argus.yml is validwith the new shape; summary lists all 4 dogfood Dockerfilesargus scan container --config argus.yml: 4 images built and scanned end-to-end (145 findings, 102 unique). Build tags surface asscanner-bandit:argus-scanetc., consistent with--discover.argus validateagainst a config without the schema directive prints the IDE-hint warning; config with directive is silentTesting
Test Results
name:omitted; defensive parser handling for stale configs with both keysSecurity Considerations
AI Context Updates (.ai/)
argus.example.yml)Checklist
argus.example.ymlcontainers block + IDE-schema warning hint)