feat(scan-container): support config-driven and manifest-file targets#112
Conversation
Closes the gap where ``argus scan container`` would only honor CLI
flags (``--discover``, ``--image``) and a populated ``argus.yml``
``containers:`` block was effectively unreachable: the lifecycle
gate exited with a usage error before config was ever consulted.
What changed
- The dispatcher in ``cmd_scan`` now loads ``--config`` *first* via
the new ``_load_container_config`` helper, then activates the
container lifecycle when EITHER CLI flags are present OR the
config has a populated ``containers:`` block. ``argus scan
container --config argus.yml`` works end-to-end with no flags.
- ``argus.yml`` (or any YAML passed to ``--config``) is the manifest
format. Same shape ``parse_container_config`` already understood:
containers:
images:
- image: myorg/app:latest
- image: myorg/worker:1.4.0
dockerfile: docker/Dockerfile.worker
context: .
discover: true
search_paths: [".", "docker/"]
scanners: [trivy, grype, syft]
Existing parser support (``argus/container/discovery.py``) was
unchanged; this PR plumbs the path from CLI through to the engine
without going through the usage-error gate.
CLI override semantics (explicit > implicit)
- ``--image REF`` (repeatable) replaces ``containers.images``.
- ``--discover PATH`` replaces ``containers.search_paths`` and sets
``discover: true``.
- ``--scanners`` replaces ``containers.scanners``.
Error handling
- Malformed config gets actionable messages, not opaque stacktraces:
- ``containers:`` not a mapping → "must be a mapping, got <type>.
Expected: containers:\n images:\n - image: <ref>"
- YAML parse error → "YAML parse error in <file>: <detail>"
- Config file not found → "Config file not found: <path>"
- A merged config with zero resolvable targets (no images, no
discover, no search_paths) hits a clear "Provide one of: --image,
--discover, containers.images, containers.discover" error,
whether triggered by direct ``_cmd_container_scan`` callers or
the dispatcher fall-through.
UX touch-ups
- Help text on ``argus scan`` and the config-only usage-error
message both list ``--config FILE`` alongside ``--image`` /
``--discover``, so users see the full set of target sources up
front instead of inferring it from docs.
- ``argus.example.yml`` documents the ``containers:`` schema in a
commented block — first-class example for users adopting
config-driven scans.
Tests (+6)
- ``test_container_lifecycle_activates_from_config_only``
- ``test_container_lifecycle_cli_image_overrides_config``
- ``test_container_lifecycle_cli_discover_overrides_search_paths``
- ``test_container_lifecycle_malformed_config_emits_actionable_error``
- ``test_container_lifecycle_no_targets_returns_usage_error``
- ``test_container_lifecycle_yaml_parse_error_is_caught``
One existing routing test (``test_routes_to_container_with_discover``)
updated to tolerate the new ``container_config=`` kwarg the
dispatcher now passes — the test only verifies routing, not
config plumbing, so the change is a 1-line ``**_kwargs`` swallow.
Validation
- Full SDK suite green: 1434 passed (+6), 8 skipped.
- Manual confirmation: ``argus scan container --config argus.yml``
with a populated ``containers.images`` block dispatches to the
lifecycle path; with empty/no ``containers:`` it shows the new
config-aware usage error.
Out of scope
- Source-scan log line ``lint-dockerfile: no config file (tool has
no canonical discovery)`` is a hadolint-config message unrelated
to container lifecycle; left for a separate doc/wording pass.
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 - 28 vulnerabilities (22 unique)Image: Combined (Deduplicated)
🔷 Trivy Scanner (28 findings, 22 unique)
⚓ Grype Scanner (0 findings, 0 unique)✅ No vulnerabilities detected by Grype 🟡 scanner-bandit - 1 vulnerabilities (1 unique)Image: Combined (Deduplicated)
🔷 Trivy Scanner (1 findings, 1 unique)
⚓ Grype Scanner (0 findings, 0 unique)✅ No vulnerabilities detected by Grype
|
| 🚨 Critical | 🟡 Medium | 🔵 Low | Total | Unique | |
|---|---|---|---|---|---|
| 0 | 7 | 41 | 63 | 113 | 49 |
🔷 Trivy Scanner (113 findings, 48 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-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-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 |
| CVE-2011-3374 | 🔵 LOW | apt | 3.0.3 | N/A |
| TEMP-0841856-B18BAF | 🔵 LOW | bash | 5.2.37-2+b8 | N/A |
...and 63 more
⚓ Grype Scanner (0 findings, 0 unique)
✅ No vulnerabilities detected by Grype
⚠️ scanner-supply-chain - 8 vulnerabilities (8 unique)
Image: ghcr.io/huntridge-labs/argus/scanner-supply-chain:d53cf5e46f7a940b695739357903907ba48bbad6
Combined (Deduplicated)
| 🚨 Critical | 🟡 Medium | 🔵 Low | Total | Unique | |
|---|---|---|---|---|---|
| 0 | 4 | 4 | 0 | 8 | 8 |
🔷 Trivy Scanner (8 findings, 8 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-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
Container config examples (yml, json, js variants in
``examples/configs/`` plus the ``containers:`` block in
``argus.example.yml``) previously led with mutable-tag references
(``busybox:latest``) and buried digest pinning lower down. Pinned
digests are the more secure default — byte-level immutable, no CVE
attribution drift, fully Dependabot-updatable when you use the
simple-string + digest form on one line.
Reordered all four files to lead with digest-pinned examples and
demote tag-only references to an "acceptable fallback" tier with
explicit framing about the security tradeoff:
Tier 1 (preferred):
- simple string + digest pin (Dependabot-updatable)
- structured form with `digest:` (Renovate-updatable)
Tier 2 (acceptable fallback):
- tag-only string (mutable, use only for ad-hoc scans)
Header comments in each file now state the recommendation up front
so users see the security posture before scrolling to examples.
The Dependabot-maintenance block is preserved (and clarified — the
simple-string form with embedded digest is BOTH digest-pinned AND
Dependabot-updatable, which the previous examples didn't make
obvious).
No schema changes — all four formats already supported pinning;
this is a docs-only nudge toward the safer default.
Validation: every config still parses (PyYAML for yml, json.load
for json, ``require()`` for js).
Description
Closes the gap where
argus scan containerwould only honor CLI flags (--discover,--image) and a populatedargus.ymlcontainers:block was effectively unreachable: the lifecycle gate exited with a usage error before config was ever consulted. After this PR, both the CLI flag surface and the config schema activate the container lifecycle — samecontainers:shape used by the existing GitHub Actionsparse-container-configaction, now first-class for the SDK CLI.User-reported symptoms this fixes (from the issue prompt):
argus scan container --config argus.ymlexited with usage error requiring--discoveror--image, even when the config hadcontainers.images.--discoverjust to pass the gate sometimes still reported "No container targets found" because the gate ran before config was loaded.argus.yml.Changes Made
Details
Lifecycle gate, refactored. New
_load_container_config(args)helper loads--configfirst, validates the YAML structure, applies CLI overrides, and returns a merged dict. New_container_config_has_targets(config)checks whether the merged dict has any way to resolve targets. The dispatcher now activates the lifecycle when either signal is positive — CLI flags OR config-defined targets — and falls through to a clear usage error only when neither is present.Manifest schema (canonical, supported via
--config FILE):This is the same shape
argus.container.discovery.parse_container_configalready understood — the bug was purely in the CLI dispatcher path, not the parser.argus.example.ymlnow documents the full schema in a commented block.CLI override semantics (explicit > implicit):
--image REF(repeatable) replacescontainers.images--discover PATHreplacescontainers.search_pathsand setsdiscover: true--scannersreplacescontainers.scannersError handling. Malformed config gets actionable messages instead of opaque stacktraces:
containers:not a mapping → "must be a mapping, got . Expected: containers:\n images:\n - image: "--image,--discover,containers.images,containers.discover" error.UX touch-ups. Both the
argus scan containerhelp text and the config-only usage error now list--config FILEalongside--imageand--discover, so users see the full set of target sources up front.Out of scope (explicit)
The user's prompt mentioned the source-scan log line
lint-dockerfile: no config file (tool has no canonical discovery). That's a hadolint-config message unrelated to container lifecycle activation; tightening the wording is fair as a separate doc/wording pass and not bundled here.Testing
Test Results
argus scan container --config argus.ymlwith a populatedcontainers.imagesblock now dispatches to the lifecycle path (no--discover/--imagerequired); empty/nocontainers:shows the new config-aware usage error.New tests (
argus/tests/test_cli.py):test_container_lifecycle_activates_from_config_onlytest_container_lifecycle_cli_image_overrides_configtest_container_lifecycle_cli_discover_overrides_search_pathstest_container_lifecycle_malformed_config_emits_actionable_errortest_container_lifecycle_no_targets_returns_usage_errortest_container_lifecycle_yaml_parse_error_is_caughtOne existing routing test (
test_routes_to_container_with_discover) updated to tolerate the newcontainer_config=kwarg the dispatcher passes — 1-line**_kwargsswallow, no semantic change.Acceptance Criteria
argus scan container --config argus.ymlsucceeds whencontainers.imagesis definedtest_container_lifecycle_activates_from_config_onlyand manual smoke--discover/--imagerequirement in config-driven modecontainers.imagesparse_container_config); now properly reachedSecurity Considerations
Security Details
The config loader uses
yaml.safe_load(not the unsafe loader) and validates structure before passing to the engine. Config-driven mode doesn't add any new file-system reach beyond what the existing CLI flag mode already had.AI Context Updates (.ai/)
.ai/architecture.yamlupdated (if components/structure changed).ai/workflows.yamlupdated (if commands/tasks changed).ai/decisions.yamlupdated (if design decision made).ai/errors.yamlupdated (if common error addressed)Checklist
argus.example.ymldocuments the schema;docs/cli-reference.mdregenerated by pre-commitRelated Issues
Closes # (n/a — Codex-style user prompt from in-product testing)