Commit 651c10b
authored
fix(scanners): correct execution-failure signaling and OSV container entrypoint (#125)
* fix(scanners): correct execution-failure signaling and OSV container 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.
* fix(scanners): surface parse failures distinctly and use per-scanner 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.
* fix(windows): yamllint AppLocker fallback and UTF-8 encoding for container 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.
---------
Co-authored-by: eFAILution <eFAILution@users.noreply.github.com>1 parent 3ccf528 commit 651c10b
24 files changed
Lines changed: 1199 additions & 107 deletions
File tree
- .ai
- argus
- core
- linters
- reporters
- scanners
- tests
- core
- linters
- reporters
- scanners
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
374 | 374 | | |
375 | 375 | | |
376 | 376 | | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1693 | 1693 | | |
1694 | 1694 | | |
1695 | 1695 | | |
| 1696 | + | |
| 1697 | + | |
| 1698 | + | |
| 1699 | + | |
1696 | 1700 | | |
1697 | 1701 | | |
1698 | 1702 | | |
1699 | 1703 | | |
1700 | 1704 | | |
1701 | 1705 | | |
1702 | | - | |
| 1706 | + | |
1703 | 1707 | | |
1704 | | - | |
1705 | | - | |
1706 | | - | |
1707 | | - | |
1708 | | - | |
1709 | | - | |
| 1708 | + | |
| 1709 | + | |
| 1710 | + | |
| 1711 | + | |
| 1712 | + | |
| 1713 | + | |
| 1714 | + | |
| 1715 | + | |
| 1716 | + | |
| 1717 | + | |
| 1718 | + | |
| 1719 | + | |
| 1720 | + | |
| 1721 | + | |
| 1722 | + | |
| 1723 | + | |
| 1724 | + | |
| 1725 | + | |
| 1726 | + | |
1710 | 1727 | | |
1711 | 1728 | | |
1712 | 1729 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| 5 | + | |
5 | 6 | | |
6 | 7 | | |
7 | 8 | | |
| |||
674 | 675 | | |
675 | 676 | | |
676 | 677 | | |
677 | | - | |
| 678 | + | |
| 679 | + | |
| 680 | + | |
| 681 | + | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
| 685 | + | |
| 686 | + | |
678 | 687 | | |
679 | 688 | | |
680 | 689 | | |
| |||
738 | 747 | | |
739 | 748 | | |
740 | 749 | | |
| 750 | + | |
| 751 | + | |
| 752 | + | |
| 753 | + | |
| 754 | + | |
| 755 | + | |
| 756 | + | |
| 757 | + | |
| 758 | + | |
741 | 759 | | |
742 | 760 | | |
743 | 761 | | |
744 | 762 | | |
| 763 | + | |
| 764 | + | |
745 | 765 | | |
746 | 766 | | |
747 | 767 | | |
| |||
854 | 874 | | |
855 | 875 | | |
856 | 876 | | |
857 | | - | |
858 | | - | |
859 | | - | |
860 | | - | |
861 | | - | |
862 | | - | |
863 | | - | |
864 | | - | |
865 | | - | |
866 | | - | |
867 | | - | |
868 | | - | |
869 | | - | |
870 | | - | |
871 | | - | |
872 | | - | |
873 | | - | |
874 | | - | |
875 | | - | |
876 | | - | |
877 | | - | |
878 | | - | |
| 877 | + | |
| 878 | + | |
| 879 | + | |
| 880 | + | |
| 881 | + | |
| 882 | + | |
| 883 | + | |
| 884 | + | |
| 885 | + | |
| 886 | + | |
| 887 | + | |
| 888 | + | |
| 889 | + | |
| 890 | + | |
| 891 | + | |
| 892 | + | |
| 893 | + | |
| 894 | + | |
| 895 | + | |
| 896 | + | |
| 897 | + | |
| 898 | + | |
| 899 | + | |
| 900 | + | |
| 901 | + | |
| 902 | + | |
| 903 | + | |
| 904 | + | |
| 905 | + | |
| 906 | + | |
879 | 907 | | |
880 | | - | |
881 | | - | |
882 | | - | |
883 | | - | |
884 | | - | |
885 | | - | |
| 908 | + | |
| 909 | + | |
| 910 | + | |
| 911 | + | |
| 912 | + | |
| 913 | + | |
| 914 | + | |
| 915 | + | |
| 916 | + | |
| 917 | + | |
| 918 | + | |
| 919 | + | |
| 920 | + | |
| 921 | + | |
| 922 | + | |
| 923 | + | |
| 924 | + | |
| 925 | + | |
| 926 | + | |
| 927 | + | |
| 928 | + | |
| 929 | + | |
| 930 | + | |
| 931 | + | |
| 932 | + | |
| 933 | + | |
| 934 | + | |
| 935 | + | |
886 | 936 | | |
887 | 937 | | |
888 | 938 | | |
| |||
0 commit comments