Skip to content

fix(scanners): correct execution-failure signaling and OSV container entrypoint#125

Merged
eFAILution merged 3 commits intofeat/argus-portabilityfrom
fix/scanner-execution-and-status-correctness
May 6, 2026
Merged

fix(scanners): correct execution-failure signaling and OSV container entrypoint#125
eFAILution merged 3 commits intofeat/argus-portabilityfrom
fix/scanner-execution-and-status-correctness

Conversation

@eFAILution
Copy link
Copy Markdown
Collaborator

@eFAILution eFAILution commented May 6, 2026

Description

Stacked correctness pass on top of feat/argus-portability covering scanner execution, status reporting, and Windows-specific platform compatibility. Three commits:

  1. 3e7dc78 — initial five fixes (OSV exit-127, lint-yaml false PASS, unified failure metadata contract, reporter degraded status, Windows chmod skip).
  2. 55d0118 — follow-up addressing review of (1): adapter-specific exit-code semantics, new parse_failed state, per-scanner failure reasons in the reporter (replacing misleading "uid mismatch / crashed / wrong entrypoint" boilerplate).
  3. 4760c1d — Windows compatibility: AppLocker / SRP fallback for yamllint, explicit UTF-8 on every container-output consumer.

Changes Made

  • Added new scanner/workflow
  • Modified existing scanner/workflow
  • Updated documentation
  • Fixed bug
  • Other (please specify)

Details

Commit 1 (3e7dc78) — initial fixes

  1. OSV container exit 127 — Docker's --entrypoint doesn't consult the image's $PATH. Pin container_entrypoint = "/osv-scanner".
  2. lint-yaml false PASS — yamllint exit ≥ 2 with empty stdout used to flow through _parse_output("") -> []. Branch on documented exit codes (0/1/≥2).
  3. Unified failure metadatarun_subprocess_scan and _run_in_container now both emit metadata["execution_failed"] + execution_failure_reason.
  4. Reporter degraded statusStatus: PASS (degraded — …) instead of contradicting the warning row above it.
  5. Windows chmod skip — wrap os.chmod(output_dir, 0o777) in platform.system() != "Windows". Linux/macOS keep it.

Commit 2 (55d0118) — follow-up

Adapter exit-code semantics (locked in by tests):

  • OSV-scanner: 0=clean, 1=findings (happy path), ≥2 with empty output=runtime failure. Engine parses results.json whenever the file exists; the exit code is irrelevant to parsing.
  • yamllint: 0=clean, 1=findings (happy path), ≥2 with empty stdout=runtime failure.

New parse_failed state. Fourth state distinct from "execution failure": scanner ran and produced output we just couldn't interpret. Engine + scanner_template now wrap scanner.parse_results() in try/except, emit metadata["parse_failed"] + parse_failure_reason. Other scanners' results keep flowing.

Per-scanner failure reasons. Replaced misleading "uid mismatch / crashed / wrong entrypoint" boilerplate with the actual reason from each adapter, in bullet form:

Warning: 2 scanner(s) did not run cleanly:
  - osv: Tool not found: osv-scanner
  - bandit: yamllint exited 2. stderr: configuration error: ...
  Pass --fail-on-scanner-error to fail the scan when this happens.
Warning: 1 scanner(s) produced output that could not be parsed:
  - opengrep: JSONDecodeError: Expecting value: line 1 col 1. output head: 'unexpected text'
Status: PASS (degraded — 2 did not run, 1 unparsable)

yamllint hardened against FileNotFoundError. --fail-on-scanner-error fires on parse_failed too.

Commit 3 (4760c1d) — Windows platform fixes

Bug 1: yamllint PermissionError on Windows (AppLocker / SRP). On Windows hosts where Software Restriction Policy or AppLocker blocks executable launches from user AppData paths, yamllint.exe raises PermissionError [WinError 5] Access is denied. The Python interpreter is typically whitelisted, so python -m yamllint works on the same machine.

YamllintLinter._run_with_windows_fallback wraps the subprocess call. On sys.platform == 'win32', a PermissionError/OSError on the direct launch triggers a retry with [sys.executable, "-m", "yamllint"] + cmd[1:]. FileNotFoundError is re-raised unchanged so the "yamllint not installed" path still renders cleanly.

Platform guard rationale: AppLocker doesn't exist on Linux. A PermissionError there indicates a genuine permission bug (chmod, mount options) the user needs to see — not a policy case the fallback compensates for. The Linux invocation path is byte-identical to before this commit: same single subprocess.run call, same PATH lookup, same exception handling. The new test test_permission_error_on_linux_does_not_fall_back asserts exactly one subprocess call fires on Linux even when PermissionError is raised.

Bug 2: UnicodeDecodeError reading container output. Docker container output is UTF-8. subprocess.run(text=True) and Path.read_text() fall back to the platform default when encoding= is omitted — cp1252 on Windows — which raises UnicodeDecodeError: 'charmap' codec can't decode byte 0x8f on any non-ASCII byte (CVE descriptions, file paths with non-ASCII characters, scanner banners).

Fix: explicit encoding='utf-8', errors='replace' on every container-output consumer:

  • engine._run_in_container docker subprocess.run
  • scanner_template.run_subprocess_scan subprocess.run
  • yamllint subprocess.run (both direct and Windows fallback)
  • 14 scanner.parse_results .read_text() calls across bandit, checkov, clamav, container, gitleaks, grype, opengrep, osv, supply_chain, trivy, trivy_iac, zap
  • engine + scanner_template parse-failed file-head reads

errors='replace' over 'strict' is intentional: a security tool showing on undecodable bytes is better than one that crashes on otherwise-usable output. Linux is unaffected — C.UTF-8 is the universal default; the change only alters behavior on hosts whose default encoding isn't UTF-8.

Cross-platform safety evidence

  • Linux:
    • Commit 1's chmod still runs (test_chmod_runs_on_non_windows).
    • Commit 2 added zero platform branches.
    • Commit 3's yamllint fallback is if sys.platform != 'win32': raise — Linux never enters the fallback branch (test_permission_error_on_linux_does_not_fall_back asserts exactly one subprocess call). UTF-8 was already the default on Linux, so explicit encoding is a no-op there.
  • macOS: Same as Linux on every dimension.
  • Windows: Three fixes specifically targeting Windows behavior — chmod skip (commit 1), AppLocker fallback (commit 3), UTF-8 decoding (commit 3). Each guarded so non-Windows hosts hit the same code path they always did.
  • Adapter-specific exit-code handling stays at the adapter layer (yamllint owns its ≥ 2 rule, OSV owns its "exit code irrelevant to parsing" rule).
  • Schema/format stable: parse_failed is additive metadata; ScanSummary.passed semantics unchanged.

Testing

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

Test Results

$ .venv/bin/python -m pytest argus/tests/ --no-cov -q
1653 passed, 8 skipped, 7 deselected, 10 warnings

New tests across all three commits (20 new):

Commit 1 (7):

  • test_yamllint.py (new file): exit-code matrix 0 / 1 / 1+findings / 2+empty / 99+empty
  • test_osv.py::test_container_entrypoint_uses_absolute_path
  • test_terminal.py::test_report_status_pass_degraded_when_any_scanner_failed
  • test_terminal.py::test_report_status_pass_clean_when_no_failures
  • test_terminal.py::test_report_status_fail_takes_priority_over_degraded
  • test_engine.py::test_chmod_skipped_on_windows
  • test_engine.py::test_chmod_runs_on_non_windows

Commit 2 (8):

  • test_osv.py::test_parse_results_succeeds_when_json_valid_regardless_of_exit_code (OSV exit-1-with-findings acceptance criterion)
  • test_osv.py::test_parse_results_zero_findings_returns_empty_list_no_exception
  • test_osv.py::test_parse_results_malformed_json_raises_for_engine_to_catch
  • test_yamllint.py::test_filenotfound_returns_execution_failed_not_raised
  • test_engine.py::test_container_parse_exception_marks_parse_failed_not_raised
  • test_terminal.py::test_report_does_not_emit_generic_failure_guesses
  • test_terminal.py::test_report_parse_failed_renders_distinctly_from_execution_failed
  • test_terminal.py::test_report_status_label_lists_both_kinds_when_both_present

Commit 3 (5):

  • test_yamllint.py::test_permission_error_on_windows_falls_back_to_python_module
  • test_yamllint.py::test_permission_error_on_linux_does_not_fall_back (cross-platform safety guard)
  • test_yamllint.py::test_subprocess_uses_utf8_encoding
  • test_engine.py::test_docker_subprocess_uses_utf8_encoding
  • test_osv.py::test_parse_results_handles_non_ascii_utf8_bytes (direct regression for the user's byte 0x8f at position 18203 crash)

Security Considerations

  • No security impact
  • Security enhancement
  • Potential security implications (explain below)

Security Details

The lint-yaml, OSV, degraded-status, and parse-failed fixes close subtle correctness gaps where a scanner-execution failure (yamllint config error, OSV image misconfigured, schema drift) would render as a clean PASS — exactly the kind of false-negative a security pipeline must avoid. The Windows fixes restore parity with Linux on AppLocker-restricted environments and on hosts with non-UTF-8 default encodings, ensuring that what a Linux CI pipeline detects, a Windows developer machine also detects.

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 - No .ai/ updates needed

Six new entries under SCANNER EXECUTION FAILURES and WINDOWS-SPECIFIC SCANNER ERRORS sections: exit-127 absolute-entrypoint pattern (OSV), yamllint silent-failure, reporter degraded-status, parse_failed, Windows AppLocker / SRP, and the cp1252 UnicodeDecodeError pattern.

Checklist

  • Code follows project style guidelines
  • Documentation updated (if applicable)
  • Changelog updated (if applicable)
  • All tests pass
  • Reviewed by at least one maintainer
  • Reviewed CONTRIBUTING.md guidelines

Related Issues

N/A — surfaced from a user bug report and follow-up reviews of each patch.

…entrypoint

Five related correctness fixes around scanner execution and status
reporting. Each is a small, targeted change with a regression test;
the linux-default behavior is unchanged on every path that wasn't
broken.

* OSV container exit 127: ``container_entrypoint`` is now the absolute
  ``/osv-scanner`` path. Docker's ``--entrypoint`` override does not
  consult the image's ``$PATH``, and ``ghcr.io/google/osv-scanner``
  declares ``ENTRYPOINT ["/osv-scanner"]``. The bare name resolved
  nowhere.

* lint-yaml false PASS: yamllint exit >= 2 with empty stdout used to
  flow through ``_parse_output('') -> []`` and render ``Status: PASS``,
  hiding bad-config failures. ``YamllintLinter.scan`` now branches on
  the exit code: 0 = clean, 1 = findings, >= 2 with empty findings =
  ``execution_failed`` + reason from stderr.

* Unified failure metadata contract: ``run_subprocess_scan`` now emits
  ``metadata['execution_failed']`` + ``metadata['execution_failure_reason']``
  on every failure path (FileNotFoundError, TimeoutExpired, no-output).
  Previously it used ``metadata['error']`` while the engine's container
  path used ``execution_failed``; the divergence meant local-path
  failures were invisible to the reporter's warning row and to
  ``--fail-on-scanner-error``.

* Reporter degraded status: ``TerminalReporter`` now prints
  ``Status: PASS (degraded - some scanners did not run)`` when the
  threshold check passes but at least one scanner has
  ``execution_failed=True``. Threshold compliance and execution
  success are independent signals; the previous output had a
  ``Warning: ...`` row directly followed by ``Status: PASS`` which
  contradicted itself. ``ScanSummary.passed`` semantics are unchanged.

* Windows chmod skip: ``os.chmod(output_dir, 0o777)`` in
  ``_run_in_container`` is now guarded by ``platform.system() !=
  'Windows'``. NTFS doesn't honor POSIX bits, ``os.chmod`` only
  flips the read-only attribute on Windows, and the macOS uid-mismatch
  failure mode the chmod guards against doesn't apply on Windows
  (Docker Desktop's bind-mount uid mapping is different). Linux/macOS
  keep the chmod - covered by a new test that asserts mode 0o777
  on non-Windows hosts.

Tests:
* New: ``test_yamllint.py`` covers the full exit-code matrix
  (0 / 1 / 1+findings / 2+empty / 99+empty)
* New: OSV ``test_container_entrypoint_uses_absolute_path`` regression
* New: terminal ``test_report_status_pass_degraded_when_any_scanner_failed``,
  ``test_report_status_pass_clean_when_no_failures``,
  ``test_report_status_fail_takes_priority_over_degraded``
* New: engine ``test_chmod_skipped_on_windows`` and
  ``test_chmod_runs_on_non_windows`` - explicit Linux-safety guard
* Updated: ``test_scanner_template`` and ``test_scanner_scan_methods``
  switched from the old ``metadata['error']`` key to
  ``metadata['execution_failed']`` to match the unified contract

Full SDK suite: 1640 passed, 8 skipped.

.ai/errors.yaml: added three new entries (exit-127 entrypoint,
yamllint silent-failure, and reporter degraded-status) so future
agents recognize the patterns.
@codecov
Copy link
Copy Markdown

codecov Bot commented May 6, 2026

Codecov Report

❌ Patch coverage is 97.63780% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
argus/cli.py 20.00% 4 Missing ⚠️
argus/core/engine.py 88.00% 3 Missing ⚠️
argus/core/scanner_template.py 77.77% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

🔒 Argus Container Security Scan

Branch: fix/scanner-execution-and-status-correctness
Commit: b88262a

📊 Combined Findings Summary

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

Scanned: 4 containers | Build Failures: 0

📦 Container Breakdown

Container Image 🚨 Crit ⚠️ High 🟡 Med 🔵 Low Total Unique Status
cli ghcr.io/huntridge-labs/argus/cli:b88262aaae317303fcf076520266501f4ee22204 1 11 16 1 29 29
scanner-bandit ghcr.io/huntridge-labs/argus/scanner-bandit:b88262aaae317303fcf076520266501f4ee22204 0 0 2 0 2 2
scanner-opengrep ghcr.io/huntridge-labs/argus/scanner-opengrep:b88262aaae317303fcf076520266501f4ee22204 0 7 44 63 114 114
scanner-supply-chain ghcr.io/huntridge-labs/argus/scanner-supply-chain:b88262aaae317303fcf076520266501f4ee22204 0 4 5 0 9 9

🔍 Detailed Findings by Container

🚨 cli - 29 vulnerabilities (23 unique)

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

Combined (Deduplicated)

🚨 Critical ⚠️ High 🟡 Medium 🔵 Low Total Unique
1 11 16 1 29 23
🔷 Trivy Scanner (29 findings, 23 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-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
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 - 2 vulnerabilities (2 unique)

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

Combined (Deduplicated)

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

✅ No vulnerabilities detected by Grype

⚠️ scanner-opengrep - 114 vulnerabilities (50 unique)

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

Combined (Deduplicated)

🚨 Critical ⚠️ High 🟡 Medium 🔵 Low Total Unique
0 7 44 63 114 50
🔷 Trivy Scanner (114 findings, 49 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-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:b88262aaae317303fcf076520266501f4ee22204

Combined (Deduplicated)

🚨 Critical ⚠️ High 🟡 Medium 🔵 Low Total Unique
0 4 5 0 9 9
🔷 Trivy Scanner (9 findings, 9 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-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

…failure reasons

Follow-up correctness pass on top of the previous patch. The first
round addressed the silent-failure paths; this round addresses how
those signals are presented and adds a fourth state the user asked
for explicitly.

Adapter exit-code semantics (locked in by tests):
* OSV-scanner: 0 = no vulns, 1 = vulns found (happy path), >= 2 with
  empty output = real runtime failure. Engine parses results.json
  whenever the file exists; exit code is irrelevant.
* yamllint: 0 = clean, 1 = lint violations (happy path), >= 2 with
  empty stdout = real runtime/config failure.

Changes:

* New 'parse_failed' state. The user asked for four distinct
  states: ran cleanly / ran-with-findings / didn't run /
  ran-but-output-unparsable. The fourth state was previously a
  raised exception that propagated up to the engine's exception
  handler and got rolled up as a generic "scanner failed". Now:
  - engine._run_in_container wraps scanner.parse_results() in
    try/except. On exception: metadata['parse_failed']=True and
    metadata['parse_failure_reason']=<exc + clipped output head>.
    Other scanners' results keep flowing.
  - scanner_template.run_subprocess_scan does the same for the
    local-execution path.
  - parse_failed is orthogonal to execution_failed; reporters render
    them in separate warning blocks.

* Reporter no longer emits generic guesses. The previous "uid
  mismatch / crashed / wrong entrypoint" boilerplate was misleading
  for OSV exit-1-with-findings and yamllint binary-not-found cases.
  TerminalReporter now prints the per-scanner reason
  (execution_failure_reason or parse_failure_reason) verbatim, in
  bullet form, with the scanner name. Status label gets a richer
  breakdown: "PASS (degraded - 1 did not run, 1 unparsable)".

* yamllint hardened against FileNotFoundError. is_available() is
  checked before scan(), but the binary can disappear in the gap
  (CI cleanup race, manual uninstall). scan() now catches
  FileNotFoundError and returns a clean execution_failed ScanResult
  instead of letting the exception escape into the engine's
  exception handler (which would render a stack trace).

* --fail-on-scanner-error fires on parse_failed too. Both states
  represent "the scan didn't fully succeed"; from a CI gating
  perspective they're equivalent.

Cross-platform safety:
* No platform-specific branches added or modified. Linux scanner
  flows are unchanged - the new parse-failed wrapping fires only
  when the parser raises (a previously-broken path everywhere).
* Adapter-specific exit-code handling stays at the adapter layer
  (yamllint owns its >= 2 rule, OSV owns its "exit code irrelevant
  to parsing" rule). No global "non-zero means failure" logic was
  added.
* Existing output formats and result schemas stable: parse_failed
  is additive metadata; ScanSummary.passed semantics unchanged
  (still threshold-only).

Tests added (8 new):
* OSV: parse_results succeeds-with-findings on real fixture
  (exit-1 acceptance case)
* OSV: parse_results returns [] on zero-findings fixture without
  raising
* OSV: parse_results raises on malformed JSON (so the engine
  wrapper catches as parse_failed)
* yamllint: FileNotFoundError returns execution_failed, no
  exception escapes
* engine: parse exception in container path -> parse_failed
  (not execution_failed, not raised)
* terminal: warning block shows per-scanner reason, no generic
  text
* terminal: parse_failed renders distinctly from execution_failed
* terminal: degraded status label lists both kinds when both present

Tests updated (1):
* test_scanner_template's test_unexpected_exception_propagates
  renamed/rewritten to test_parse_exception_emits_parse_failed_metadata
  reflecting the new contract.

Full SDK suite: 1648 passed, 8 skipped.

.ai/errors.yaml: degraded-status entry refreshed with new label
shape; new entry added for the parse_failed pattern.
…ainer output

Two Windows-specific correctness fixes. Linux behavior is unchanged.

Bug 1 — yamllint PermissionError on Windows (AppLocker / SRP):

On Windows hosts where Software Restriction Policy or AppLocker
blocks executable launches from user AppData paths, the direct
``yamllint.exe`` invocation raises ``PermissionError [WinError 5]
Access is denied`` mid-scan. The python interpreter itself is
typically whitelisted, so loading the same package via
``python -m yamllint`` works on the same machine and is
argv-compatible (yamllint's __main__ accepts the same flags).

Fix: ``YamllintLinter._run_with_windows_fallback`` wraps the
subprocess.run call. On ``sys.platform == 'win32'``, a
PermissionError/OSError on the direct invocation triggers a retry
with [sys.executable, '-m', 'yamllint'] + cmd[1:]. FileNotFoundError
is re-raised unchanged so 'yamllint not installed' still renders
as a clean execution_failed (the fallback wouldn't help — yamllint
isn't installed at all).

Linux is fully bypassed: AppLocker doesn't exist there, so a
PermissionError on Linux indicates a genuine permission bug
(chmod, mount options) the user needs to see, not a policy case
the fallback compensates for. The Linux invocation path is
byte-identical to before — same single subprocess.run call, same
PATH lookup, same exception handling.

Bug 2 — UnicodeDecodeError reading container output:

Docker container output (and most CLI scanner output) is UTF-8.
``subprocess.run(text=True)`` and ``Path.read_text()`` fall back
to the platform default when ``encoding=`` is omitted — cp1252
on Windows. Any non-ASCII byte (CVE descriptions with accented
characters, file paths with unicode segments, scanner banners
with arrow glyphs) raises UnicodeDecodeError mid-scan.

Fix: explicit ``encoding='utf-8', errors='replace'`` on every
container-output consumer:
* engine._run_in_container's docker subprocess.run
* scanner_template.run_subprocess_scan's subprocess.run
* yamllint's subprocess.run (both direct + Windows fallback)
* All 14 scanner.parse_results .read_text() calls (bandit,
  checkov, clamav, container, gitleaks, grype, opengrep, osv,
  supply_chain, trivy, trivy_iac, zap)
* Engine + scanner_template parse-failed file-head reads

errors='replace' over 'strict' is intentional: a security tool
that shows U+FFFD on undecodable bytes is better than one that
crashes on otherwise-usable scanner output. Linux unaffected
since LANG=C.UTF-8 is the universal default; this only changes
behavior on hosts whose default ISN'T UTF-8.

Tests added (5 new):
* yamllint: PermissionError on Windows triggers python -m
  fallback, second subprocess call uses sys.executable -m yamllint
* yamllint: PermissionError on Linux does NOT fall back, raised
  through to outer handler as execution_failed
* yamllint: subprocess.run gets encoding='utf-8' + errors='replace'
* engine: docker subprocess.run gets encoding='utf-8' + errors='replace'
* OSV: parse_results round-trips non-ASCII UTF-8 content (CVE
  summaries with accents, file paths with unicode) without
  raising — the user's reported decode crash on byte 0x8f at
  position 18203

Full SDK suite: 1653 passed, 8 skipped.

.ai/errors.yaml: two new entries — Windows AppLocker pattern
and the cp1252 UnicodeDecodeError pattern, both with the
exact platform-guard rationale.
@eFAILution eFAILution merged commit 651c10b into feat/argus-portability May 6, 2026
22 checks passed
@eFAILution eFAILution deleted the fix/scanner-execution-and-status-correctness branch May 6, 2026 20:31
eFAILution added a commit that referenced this pull request May 7, 2026
The smoke step in ``validate-install-from-source`` runs
``argus scan --list`` to verify scanner registration works end-to-
end after ``pip install <repo-root>``. GitHub-hosted runners on
Python 3.13.13 hit a transient SIGSEGV in this exact subprocess
pattern — first observed on the PR #125 merge run (green on
first re-run), and reproduced again here on PR #126.

The crash:
* Doesn't reproduce locally on macOS Python 3.13.13.
* Doesn't reproduce on linux/aarch64 or linux/amd64 in
  python:3.13.13-slim Docker containers.
* Has no Python traceback — bash exits 139 immediately after
  ``argus --version`` succeeds.
* Resolves on retry (PR #125's CI run flipped to green on
  ``gh run rerun``).

Adding a retry loop (max 3 attempts) so the transient flake
doesn't gate this PR. A real registration bug (the class of bug
this smoke is meant to catch) fails all 3 attempts and surfaces
clearly. Each attempt prints what it got so the failure mode is
obvious if it ever does fire.

The lint step in the same job is the deterministic guard — it
inspects every wrapper's action.yml for the required/forbidden
patterns and doesn't depend on subprocess execution at all.
eFAILution added a commit that referenced this pull request May 7, 2026
…DR-019 + CI guard (#126)

* refactor(wrappers): install Argus SDK from composite checkout

Migrates the remaining 14 SDK-using composite wrappers from
``pip install pyyaml`` (which relied on the legacy ``bin/argus``
shim) to ``pip install "${{ github.action_path }}/../../.."``,
matching the pattern already in scanner-container and scanner-zap.

Wrappers migrated (8 scanner + 6 linter):

* scanner-bandit, scanner-checkov, scanner-clamav, scanner-gitleaks,
  scanner-opengrep, scanner-osv, scanner-supply-chain, scanner-trivy-iac
* linter-dockerfile, linter-javascript, linter-json, linter-python,
  linter-terraform, linter-yaml

Each wrapper:
* Step renamed: "Install dependencies" -> "Install Argus SDK"
* Install line: ``pip install pyyaml`` -> ``pip install "${{ github.action_path }}/../../.."``
* Inline rationale comment matching scanner-container's

Why this pattern:

When a consumer pins ``uses: org/argus/.github/actions/<wrapper>@<ref>``,
GitHub Actions clones the entire argus repo at <ref> into
``_actions/org/argus/<ref>/`` (depth-1 of the tree at that ref, not
just the action subdirectory). ``${{ github.action_path }}/../../..``
resolves to that clone root, which contains ``pyproject.toml`` and
the ``argus/`` package — pip-installable directly.

Benefits over ``pip install argus-security==<X>``:

* Pin behavior is automatic: action ref == SDK version, no
  release-it coordination, no PyPI publish lag.
* Air-gap friendly: GHES instances mirroring this repo locally
  don't also need a private PyPI mirror with argus-security
  uploaded. The composite checkout is the SDK source.
* No second distribution channel to keep in sync.

Cross-platform safety:

* Pure YAML edits — no behavior change in the action steps that
  follow. Each wrapper still calls ``argus scan`` the same way.
* The ``if:`` guards on scanner-checkov / scanner-trivy-iac
  ("only install when has_iac == 'true'") are preserved.
* No platform branching introduced.

Validation:

* All 16 SDK-using composite ``action.yml`` files parse as valid
  YAML.
* End-to-end smoke: ``pip install <repo-root>`` in a fresh Python
  3.13.13 venv completes cleanly, ``argus --version`` reports
  0.7.2, ``argus scan --list`` enumerates all 18 scanners.
* SDK suite: 1653 passed, 8 skipped (no regressions).
* Wrapper test suite (``.github/actions/*/tests/``): 509 passed.

Roadmap:

* Marks roadmap items 199 + 200 ("Apply install-from-source to
  remaining 14 wrappers" and "Rename Install dependencies ->
  Install Argus SDK") complete.
* ``scanner-dependency-review`` was listed in the original 14 by
  mistake — it wraps GitHub's ``dependency-review-action`` and
  doesn't use the Argus SDK. Roadmap text updated to reflect that.
* ``bin/argus`` shim removal (item 201) and README/QUICK-START
  TestPyPI flag cleanup (item 197) are independent and tracked
  separately.

* chore: remove bin/argus shim

The shim added the repo root to sys.path so a fresh checkout could
run ``./bin/argus ...`` without ``pip install``. Now that:

* All 16 SDK-using composite wrappers install the SDK from the
  checkout via ``pip install "${{ github.action_path }}/../../.."``
  (PR #126), and
* Pip's entry point ``argus = "argus.cli:main"`` in pyproject.toml
  has been the canonical CLI launcher for every other consumer
  (PyPI install, ``uvx argus``, repo dev installs),

…the shim has zero remaining callers. Audit before delete:

* No workflow files reference ``bin/argus``.
* No tests reference it.
* No production code path references it.
* Doc mentions of ``bin/argus`` are all ``.venv/bin/argus`` (the
  pip-installed entry point in a virtualenv) — completely
  different, unaffected.

Closes roadmap item #201 (Remove bin/argus wrapper).

* docs: codify install-from-source pattern (ADR-019, CONTRIBUTING, CI guard)

Three concentric guards so the install-from-source pattern doesn't
silently regress as new wrappers get added:

* CONTRIBUTING.md "Define action.yml" section now includes the
  exact ``Install Argus SDK`` step, the rationale ("don't replace
  this with pip install argus-security==<X>"), and a pointer to
  ADR-019. Future contributors see the canonical pattern at the
  exact place they're copying from.

* .ai/decisions.yaml ADR-019 records the decision, the alternatives
  considered (PyPI install, PyInstaller binary, Docker action), and
  the consequences (release coordination, GHES portability,
  bin/argus removal). Search-friendly for the next AI agent that
  has to make a similar call.

* .github/workflows/test-actions.yml gains a
  ``validate-install-from-source`` job that lints every SDK-using
  wrapper for:
    - Forbidden: ``run: pip install pyyaml`` (legacy bin/argus shim)
    - Forbidden: ``run: pip install argus-security`` (PyPI install)
    - Required: ``${{ github.action_path }}/../../..`` substring
  Plus an end-to-end smoke that ``pip install <repo-root>`` actually
  produces a working ``argus`` CLI. Forbidden patterns are anchored
  to ``run:`` lines so the rationale comments (which cite the bad
  patterns as counter-examples) don't trigger false positives.
  Patterns live in env vars to keep ``${{ ... }}`` syntax out of the
  run-script body — workflow-injection-safety best practice from
  the GitHub security guidance.

  Exemptions list (codeql, dependency-review, syft, *-summary,
  scn-detector, comment-pr, get-job-id, ai-summary, security-
  summary, linting-summary, parse-container-config) — these don't
  call ``argus scan`` and don't need the SDK installed.

Local dry-run: all 16 SDK-using wrappers pass the guard, all 5
exemptions correctly skipped. CONTRIBUTING.md, decisions.yaml, and
test-actions.yml all parse as valid YAML.

* ci: retry transient SIGSEGV in install-from-source smoke

The smoke step in ``validate-install-from-source`` runs
``argus scan --list`` to verify scanner registration works end-to-
end after ``pip install <repo-root>``. GitHub-hosted runners on
Python 3.13.13 hit a transient SIGSEGV in this exact subprocess
pattern — first observed on the PR #125 merge run (green on
first re-run), and reproduced again here on PR #126.

The crash:
* Doesn't reproduce locally on macOS Python 3.13.13.
* Doesn't reproduce on linux/aarch64 or linux/amd64 in
  python:3.13.13-slim Docker containers.
* Has no Python traceback — bash exits 139 immediately after
  ``argus --version`` succeeds.
* Resolves on retry (PR #125's CI run flipped to green on
  ``gh run rerun``).

Adding a retry loop (max 3 attempts) so the transient flake
doesn't gate this PR. A real registration bug (the class of bug
this smoke is meant to catch) fails all 3 attempts and surfaces
clearly. Each attempt prints what it got so the failure mode is
obvious if it ever does fire.

The lint step in the same job is the deterministic guard — it
inspects every wrapper's action.yml for the required/forbidden
patterns and doesn't depend on subprocess execution at all.

---------

Co-authored-by: eFAILution <eFAILution@users.noreply.github.com>
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