Skip to content

fix(scanner): scan single-file targets; fail on unusable ones - #90

Merged
lab700xdev merged 1 commit into
mainfrom
fix/125-local-target-silent-pass
Aug 16, 2026
Merged

fix(scanner): scan single-file targets; fail on unusable ones#90
lab700xdev merged 1 commit into
mainfrom
fix/125-local-target-silent-pass

Conversation

@lab700xdev

Copy link
Copy Markdown
Contributor

The bug

Local target discovery used Path.rglob as its only mechanism. rglob yields the contents of a directory and nothing else, so a target that was a regular file walked nothing, recorded no error, and exited 0:

$ aisbom scan ./mock_malware.pt     # RCE via posix.system
No AI models found.
exit 0                              # ← silently clean

$ aisbom scan ./fixtures/           # same file, parent directory
mock_malware.pt  CRITICAL (RCE Detected: posix.system)
exit 2                              # ← correct

Same file, opposite verdicts. Reproduced in all three modes — default, --strict, --lint.

The README documents exactly that invocation for both flagship security features: aisbom scan model.pkl --strict and aisbom scan model.pt --lint. The two examples a security-conscious user is most likely to copy were the two that silently did nothing.

A nonexistent path failed the same way — empty-but-valid SBOM, exit 0, indistinguishable from a genuinely clean repo. A typo'd path in CI turned the gate green permanently.

The fix

Each local target shape is now handled explicitly:

  • A single file is a first-class target. Dispatched through the same extension ladder as the walk, extracted into _dispatch_local_file so the two paths cannot drift apart again. Works for every supported format, not just PyTorch.
  • An unusable target is an error, not an empty result. Missing paths, broken symlinks, and files no scanner claims record a structured target_error and exit 1. --no-fail-on-risk does not suppress it — that flag governs risk findings, not a broken target.
  • No AI models found is withheld when nothing was examined. It is a claim about the target's contents, so it must not appear when the target was never opened.

Deliberately unchanged: an empty directory is still a clean scan at exit 0, and the directory walk still skips non-model files silently. Skipping a README.md inside a tree is correct; being handed one as the target is a failed instruction.

Exit-code contract

Exit Meaning
0 Scan completed, no CRITICAL risk
1 Scan could not be completed — target missing/unreadable, parse failure, or remote fetch failure
2 CRITICAL risk found (suppress with --no-fail-on-risk)

Only the 1 row is new for local targets; it matches what the remote fetch-failure path already does.

Verification

  • 694 passed, coverage 91.25% (was 679 / 91.17% — 15 new tests, no existing test changed).
  • The new tests were run against the unfixed code: 12 of 15 fail. The 3 that pass are the deliberate guard-against-over-correction cases (empty directory, directory walk ignoring non-model files, --no-fail-on-risk still clearing a real CRITICAL), which must pass both before and after.
  • Full CLI snapshot diff against main shows exactly one change — the missing-target case going exit 0exit 1 with its new message. Every other captured surface is byte-identical: --help/--version/info, scan in terminal/markdown/JSON/SPDX form, --strict, --lint, --no-fail-on-risk, diff, the unknown-flag error path, and the emitted CycloneDX and SPDX documents.
  • Bypass scorecard unchanged at 8/11, gate passes. cve-2025-1889-nonstandard-extension shares the No AI models found symptom but is an extension-matching gap, not this bug, and its verdict did not move.
  • SBOM output valid on the new paths: single-file scan emits a 1-component CycloneDX document (was 0) and a 1-package SPDX document; the target-error case still emits a valid 0-component document.
  • Frozen binary rebuilt and smoke-tested (macOS arm64), since binaries.yml never runs on PRs: single malicious file → 2, nonexistent → 1, empty dir → 0, fixtures dir → 2, and posix.system still detected inside the bundle.

Behavior change to be aware of

A path that previously exited 0 while scanning nothing now exits 1. Any pipeline passing a file path was getting no scanning at all before this change, so nothing that genuinely worked is affected — but a pipeline that was silently green on a bad path will now fail, which is the point.

Closes the local-target side of this gap; the remote fetch-failure equivalent was fixed previously.

Local discovery used Path.rglob as its only mechanism, which yields the
contents of a directory and nothing else. A target that was a regular file
walked nothing, recorded no error, and exited 0 — so a malicious .pt named
directly on the command line reported 'No AI models found' and passed clean,
while the same file scanned via its parent directory was correctly CRITICAL.
The README documents that exact form for both --strict and --lint.

A nonexistent path failed the same way: empty SBOM, exit 0, indistinguishable
from a genuinely clean repo.

Handle each local target shape explicitly. A single file is now dispatched
through the same extension ladder as the walk, extracted into
_dispatch_local_file so the two cannot drift. Missing paths, broken symlinks,
and files no scanner claims record a structured target_error and exit 1;
--no-fail-on-risk does not suppress it, since that flag governs risk findings
rather than a broken target. An empty directory is still a clean scan at
exit 0, and the directory walk still skips non-model files silently.

'No AI models found' is now withheld when nothing was examined — it is a claim
about the target's contents.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4a4c85653b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread aisbom/scanner.py
Comment on lines +289 to +292
if ext in PYTORCH_EXTENSIONS:
self.artifacts.append(self._inspect_pytorch(full_path))
elif ext == SAFETENSORS_EXTENSION:
self.artifacts.append(self._inspect_safetensors(full_path))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Propagate named-file inspector failures

When a supported single-file target cannot be read or parsed, each inspector catches the exception and returns an artifact containing error, but this dispatcher appends that artifact without adding anything to self.errors. Consequently the CLI still exits 0—for example, an unreadable unreadable.pt produces an UNKNOWN artifact and a malformed SafeTensors header can produce a LOW artifact—so the new unusable-target gate remains fail-open for precisely the supported files it is intended to protect. Record returned inspector errors in self.errors (or let them propagate into a target/parse error) before considering the file successfully dispatched.

Useful? React with 👍 / 👎.

@lab700xdev
lab700xdev merged commit 6eb9281 into main Aug 16, 2026
2 checks passed
@lab700xdev
lab700xdev deleted the fix/125-local-target-silent-pass branch August 16, 2026 13:58
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