Skip to content

feat(scan-container): support config-driven and manifest-file targets#112

Merged
eFAILution merged 2 commits intofeat/argus-portabilityfrom
feat/scan-container-config-driven
May 5, 2026
Merged

feat(scan-container): support config-driven and manifest-file targets#112
eFAILution merged 2 commits intofeat/argus-portabilityfrom
feat/scan-container-config-driven

Conversation

@eFAILution
Copy link
Copy Markdown
Collaborator

Description

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. After this PR, both the CLI flag surface and the config schema activate the container lifecycle — same containers: shape used by the existing GitHub Actions parse-container-config action, now first-class for the SDK CLI.

User-reported symptoms this fixes (from the issue prompt):

  1. argus scan container --config argus.yml exited with usage error requiring --discover or --image, even when the config had containers.images.
  2. Adding --discover just to pass the gate sometimes still reported "No container targets found" because the gate ran before config was loaded.
  3. No clear story for "config-only" scans where users wanted to drive everything from argus.yml.

Changes Made

  • Added new scanner/workflow
  • Modified existing scanner/workflow
  • Updated documentation
  • Fixed bug
  • Other

Details

Lifecycle gate, refactored. New _load_container_config(args) helper loads --config first, 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):

containers:
  images:
    - image: myorg/myapp:latest
    - image: myorg/worker:1.4.0
      dockerfile: docker/Dockerfile.worker
      context: .
  discover: true
  search_paths: [".", "docker/"]
  scanners: [trivy, grype, syft]

This is the same shape argus.container.discovery.parse_container_config already understood — the bug was purely in the CLI dispatcher path, not the parser. argus.example.yml now documents the full schema in a commented block.

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 instead of opaque stacktraces:

  • containers: not a mapping → "must be a mapping, got . Expected: containers:\n images:\n - image: "
  • YAML parse error → "YAML parse error in : "
  • Config file not found → "Config file not found: "
  • Merged config with zero resolvable targets → clear "Provide one of: --image, --discover, containers.images, containers.discover" error.

UX touch-ups. Both the argus scan container help text and the config-only usage error now list --config FILE alongside --image and --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

  • Unit tests added/updated
  • Integration tests added/updated
  • Manual testing performed
  • Tested with different scanner combinations

Test Results

  • Full SDK suite green: 1434 passed, 8 skipped, 7 deselected (+6 net from this change).
  • Manual smoke: argus scan container --config argus.yml with a populated containers.images block now dispatches to the lifecycle path (no --discover/--image required); empty/no containers: shows the new config-aware usage error.

New tests (argus/tests/test_cli.py):

Test Acceptance criterion mapped
test_container_lifecycle_activates_from_config_only "argus scan container --config argus.yml succeeds when containers.images is defined"
test_container_lifecycle_cli_image_overrides_config "CLI --image overrides config images"
test_container_lifecycle_cli_discover_overrides_search_paths "CLI --discover overrides config search paths"
test_container_lifecycle_malformed_config_emits_actionable_error "malformed containers config emits actionable error"
test_container_lifecycle_no_targets_returns_usage_error "Clear errors and docs for missing/invalid container targets"
test_container_lifecycle_yaml_parse_error_is_caught Defensive — friendly error instead of yaml.YAMLError traceback

One existing routing test (test_routes_to_container_with_discover) updated to tolerate the new container_config= kwarg the dispatcher passes — 1-line **_kwargs swallow, no semantic change.

Acceptance Criteria

Criterion Status
argus scan container --config argus.yml succeeds when containers.images is defined ✅ Verified by test_container_lifecycle_activates_from_config_only and manual smoke
No mandatory --discover/--image requirement in config-driven mode ✅ Lifecycle gate consults config alongside CLI flags
Deterministic target parsing from containers.images ✅ Unchanged parser (parse_container_config); now properly reached
Clear errors and docs for missing/invalid container targets ✅ 3 distinct error paths covered (malformed mapping, YAML parse, zero targets) plus updated help text and example yml

Security Considerations

  • No security impact
  • Security enhancement
  • Potential security implications

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.yaml updated (if components/structure changed)
  • .ai/workflows.yaml updated (if commands/tasks changed)
  • .ai/decisions.yaml updated (if design decision made)
  • .ai/errors.yaml updated (if common error addressed)
  • N/A — refines an existing CLI subcommand; schema/parser unchanged

Checklist

  • Code follows project style guidelines
  • Documentation updated (if applicable) — argus.example.yml documents the schema; docs/cli-reference.md regenerated by pre-commit
  • All tests pass
  • Reviewed by at least one maintainer
  • Reviewed CONTRIBUTING.md guidelines

Related Issues

Closes # (n/a — Codex-style user prompt from in-product testing)

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
Copy link
Copy Markdown

codecov Bot commented May 5, 2026

Codecov Report

❌ Patch coverage is 84.25926% with 17 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
argus/cli.py 68.51% 17 Missing ⚠️

📢 Thoughts on this report? Let us know!

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

🔒 Argus Container Security Scan

Branch: feat/scan-container-config-driven
Commit: d53cf5e

📊 Combined Findings Summary

🚨 Critical ⚠️ High 🟡 Medium 🔵 Low 📦 Total 🔢 Unique
1 22 61 64 148 148

Scanned: 4 containers | Build Failures: 0

📦 Container Breakdown

Container Image 🚨 Crit ⚠️ High 🟡 Med 🔵 Low Total Unique Status
cli ghcr.io/huntridge-labs/argus/cli:d53cf5e46f7a940b695739357903907ba48bbad6 1 11 15 1 28 28
scanner-bandit ghcr.io/huntridge-labs/argus/scanner-bandit:d53cf5e46f7a940b695739357903907ba48bbad6 0 0 1 0 1 1
scanner-opengrep ghcr.io/huntridge-labs/argus/scanner-opengrep:d53cf5e46f7a940b695739357903907ba48bbad6 0 7 41 63 111 111
scanner-supply-chain ghcr.io/huntridge-labs/argus/scanner-supply-chain:d53cf5e46f7a940b695739357903907ba48bbad6 0 4 4 0 8 8

🔍 Detailed Findings by Container

🚨 cli - 28 vulnerabilities (22 unique)

Image: ghcr.io/huntridge-labs/argus/cli:d53cf5e46f7a940b695739357903907ba48bbad6

Combined (Deduplicated)

🚨 Critical ⚠️ High 🟡 Medium 🔵 Low Total Unique
1 11 15 1 28 22
🔷 Trivy Scanner (28 findings, 22 unique)
CVE Severity Package Version Fixed
CVE-2025-68121 🚨 CRITICAL stdlib v1.24.11 1.24.13, 1.25.7, 1.26.0-rc.3
CVE-2026-32280 ⚠️ HIGH stdlib v1.26.1 1.25.9, 1.26.2
CVE-2026-32281 ⚠️ HIGH stdlib v1.26.1 1.25.9, 1.26.2
CVE-2026-32283 ⚠️ HIGH stdlib v1.26.1 1.25.9, 1.26.2
CVE-2026-33810 ⚠️ HIGH stdlib v1.26.1 1.26.2
CVE-2025-61726 ⚠️ HIGH stdlib v1.24.11 1.24.12, 1.25.6
CVE-2025-61728 ⚠️ HIGH stdlib v1.24.11 1.24.12, 1.25.6
CVE-2026-25679 ⚠️ HIGH stdlib v1.24.11 1.25.8, 1.26.1
CVE-2026-32280 ⚠️ HIGH stdlib v1.24.11 1.25.9, 1.26.2
CVE-2026-32281 ⚠️ HIGH stdlib v1.24.11 1.25.9, 1.26.2
CVE-2026-32283 ⚠️ HIGH stdlib v1.24.11 1.25.9, 1.26.2
CVE-2026-34040 ⚠️ HIGH github.com/docker/docker v28.5.2+incompatible 29.3.1
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
CVE-2025-11579 🟡 MEDIUM github.com/nwaples/rardecode/v2 v2.1.0 2.2.0
CVE-2025-58058 🟡 MEDIUM github.com/ulikunitz/xz v0.5.12 0.5.15
CVE-2025-47914 🟡 MEDIUM golang.org/x/crypto v0.35.0 0.45.0
CVE-2025-58181 🟡 MEDIUM golang.org/x/crypto v0.35.0 0.45.0
CVE-2025-61730 🟡 MEDIUM stdlib v1.24.11 1.24.12, 1.25.6
CVE-2026-27142 🟡 MEDIUM stdlib v1.24.11 1.25.8, 1.26.1
CVE-2026-32282 🟡 MEDIUM stdlib v1.24.11 1.25.9, 1.26.2
CVE-2026-32288 🟡 MEDIUM stdlib v1.24.11 1.25.9, 1.26.2
CVE-2026-32289 🟡 MEDIUM stdlib v1.24.11 1.25.9, 1.26.2
CVE-2026-33997 🟡 MEDIUM github.com/docker/docker v28.5.2+incompatible 29.3.1
CVE-2026-41506 🟡 MEDIUM github.com/go-git/go-git/v5 v5.17.2 5.18.0
CVE-2026-27139 🔵 LOW stdlib v1.24.11 1.25.8, 1.26.1
⚓ Grype Scanner (0 findings, 0 unique)

✅ No vulnerabilities detected by Grype

🟡 scanner-bandit - 1 vulnerabilities (1 unique)

Image: ghcr.io/huntridge-labs/argus/scanner-bandit:d53cf5e46f7a940b695739357903907ba48bbad6

Combined (Deduplicated)

🚨 Critical ⚠️ High 🟡 Medium 🔵 Low Total Unique
0 0 1 0 1 1
🔷 Trivy Scanner (1 findings, 1 unique)
CVE Severity Package Version Fixed
CVE-2026-3219 🟡 MEDIUM pip 26.0.1 N/A
⚓ Grype Scanner (0 findings, 0 unique)

✅ No vulnerabilities detected by Grype

⚠️ scanner-opengrep - 113 vulnerabilities (49 unique)

Image: ghcr.io/huntridge-labs/argus/scanner-opengrep:d53cf5e46f7a940b695739357903907ba48bbad6

Combined (Deduplicated)

🚨 Critical ⚠️ High 🟡 Medium 🔵 Low Total Unique
0 7 41 63 113 49
🔷 Trivy Scanner (113 findings, 48 unique)
CVE Severity Package Version Fixed
CVE-2026-4878 ⚠️ HIGH libcap2 1:2.75-10+b8 N/A
CVE-2025-69720 ⚠️ HIGH libncursesw6 6.5+20250216-2 N/A
CVE-2026-29111 ⚠️ HIGH libsystemd0 257.9-1~deb13u1 N/A
CVE-2025-69720 ⚠️ HIGH libtinfo6 6.5+20250216-2 N/A
CVE-2026-29111 ⚠️ HIGH libudev1 257.9-1~deb13u1 N/A
CVE-2025-69720 ⚠️ HIGH ncurses-base 6.5+20250216-2 N/A
CVE-2025-69720 ⚠️ HIGH 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 ⚠️ High 🟡 Medium 🔵 Low Total Unique
0 4 4 0 8 8
🔷 Trivy Scanner (8 findings, 8 unique)
CVE Severity Package Version Fixed
CVE-2026-32280 ⚠️ HIGH stdlib v1.26.1 1.25.9, 1.26.2
CVE-2026-32281 ⚠️ HIGH stdlib v1.26.1 1.25.9, 1.26.2
CVE-2026-32283 ⚠️ HIGH stdlib v1.26.1 1.25.9, 1.26.2
CVE-2026-33810 ⚠️ HIGH 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).
@eFAILution eFAILution merged commit f4e39fd into feat/argus-portability May 5, 2026
32 checks passed
@eFAILution eFAILution deleted the feat/scan-container-config-driven branch May 5, 2026 19:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant