Skip to content

fix(analyzer): detect bound shell truthiness - #577

Merged
rng1995 merged 40 commits into
NVIDIA:mainfrom
chrisknvidia:fix/christopherk/issue-475-shell-truthiness-core
Oct 8, 2026
Merged

rng1995 merged 40 commits into
NVIDIA:mainfrom
chrisknvidia:fix/christopherk/issue-475-shell-truthiness-core

Conversation

@chrisknvidia

@chrisknvidia chrisknvidia commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator

Ordinary Python functions, main guards, compound statements and formatted commands can lose a HIGH TM1 finding when the bound-shell companion abstains. Preserve the lexical finding unless a retained companion row replaces it or a scoped, affirmative replacement proves the candidate no longer applies. Fixes #475.

Review fixes keep uninvoked global/nonlocal stores, annotation-only declarations, unrelated attribute stores and same-slot assignments from suppressing lexical HIGH. Explicit called-slot replacement has a bounded lifetime: cached-module changes are tracked across ordinary imports, while unknown eager effects discard replacement certainty. Native callable captures, cross-API assignments and native values wrapped in RHS expressions cannot establish replacement proof. The legacy first-statement direct shadow control remains bounded to a single completed store with no native receiver/Popen reference and no later eager effect. Receiver binding, report ownership and cached slot state remain separate.

Validation of this revision: 602 affected analyzer/graph/input cases passed; 51 fresh installed-wheel CLI scans passed across positive/negative source cases, real reports and strict exits. Ruff lint/format for src/tests and diff checks passed. Static verification uses real native analyzer/graph execution and an installed wheel with --no-llm; remote provider inference, production deployment and other operating systems were not exercised. Earlier broad-suite results were from older heads and do not establish a green full suite for this update. Hosted CI and human re-review remain required.

Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Signed-off-by: Christopher Kevin <christopherk@nvidia.com>

@rng1995 rng1995 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed exact head bd09251621e1edabcfb4615b04d420dbf83d02b6. Requesting changes because the issue #475 literal-versus-bound bypass remains when a later argument is effectful.

For enabled = True; subprocess.run(command, shell=enabled, env=build_env()), Python resolves the receiver and captures shell=True before evaluating the later env expression. The runtime shell value is therefore definitely true, but this head emits zero TM1 findings; the equivalent shell=True call emits one. Moving env=build_env() before shell=enabled correctly makes the value uncertain, so the two orders must not be treated identically.

_call_arguments_are_passive currently rejects the whole call when any argument is impure, including expressions evaluated after shell. Please make invalidation evaluation-order-aware and add before/after-shell parity regressions.

@rng1995 rng1995 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[SkillSpector Review]

Reviewed exact head bd09251621e1edabcfb4615b04d420dbf83d02b6 and the complete bound-shell dataflow implementation.

The existing blocking review remains valid: _call_arguments_are_passive() rejects a call when any later argument is effectful, even though Python has already evaluated and captured an earlier shell=enabled value. This leaves the issue #475 bypass for shell=enabled, env=build_env() while the equivalent literal is detected. Make invalidation evaluation-order-aware and add before/after-shell parity regressions.

No duplicate inline comment was added.

…e-475-shell-truthiness-core

Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
@chrisknvidia

chrisknvidia commented Sep 21, 2026 •

Copy link
Copy Markdown
Collaborator Author

@rng1995 I addressed the receiver-invalidation finding in 995d746: the current subprocess call is still reported, then effectful arguments clear receiver trust so later proxy calls are not misclassified. PTAL.

@rng1995 rng1995 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Invalidate trusted receivers after effectful arguments (src/skillspector/nodes/analyzers/static_python_shell_truthiness.py:696-703, with analogous AnnAssign/Expr paths at 826-830 and 846-849). The original argument-order blocker is fixed, but an imported callback evaluated after shell= can mutate the caller module’s subprocess binding. The current call is correctly flagged because its receiver and shell value were captured first; however, trusted_names survives, so a subsequent Proxy.run call is falsely reported as subprocess. Exact runtime repro gives first=real, second=proxy, while the analyzer emits TM1 on both lines. Preserve the current finding, then clear trusted receiver names for later statements after any non-passive argument call, and add a regression.

Signed-off-by: Christopher Kevin <christopherk@nvidia.com>

@rng1995 rng1995 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[SkillSpector Review]

Re-reviewed exact head 995d746cbc1c615a7bf17c04ed4943c917ce9beb. The original later-argument ordering defect is fixed, and the latest commit correctly preserves the current direct subprocess finding before clearing receiver trust after effectful arguments. All six hosted checks pass and the prior review thread is resolved.

One required receiver-invalidation case remains. An ordinary effectful call can rebind the caller's module-level subprocess name, but the non-direct call path clears value facts while retaining receiver trust unless the call AST itself contains a direct store/mutation. A subsequent proxy .run(..., shell=enabled) is therefore still misreported as subprocess TM1. The inline comment includes a concrete straight-line case. Invalidate trusted receivers after calls that are not proven receiver-safe, while preserving already-captured findings, and add a regression. This false positive blocks approval.

Comment thread src/skillspector/nodes/analyzers/static_python_shell_truthiness.py Outdated
Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Signed-off-by: Christopher Kevin <christopherk@nvidia.com>

@rng1995 rng1995 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[SkillSpector Review]

Re-reviewed exact head 256a7ea61b56dc80f6bf218deae68570b2f77c70, including the complete six-file diff, the companion's forward scan and receiver-trust prepasses, lexical/AST reconciliation, prior reviews and replies, regression tests, and all six passing hosted checks.

Resolved: the earlier shell=enabled, env=build_env() evaluation-order finding; receiver invalidation after effectful direct-call arguments; and the exact standalone generic-call forms reported last time (expression, assignment, and annotated assignment). The new tests meaningfully cover those fixes.

The broader receiver-safety correction remains partially resolved. A generic call nested in a tuple/list RHS is still evaluated, but the new invalidation tests only whether the outer RHS is an ast.Call. Such an expression clears shell-value facts without clearing trusted receivers; a fresh true assignment then causes a later proxy call to receive HIGH TM1. The inline continuation gives a self-contained inert fixture and the needed forward/prepass regression coverage.

Changes are still required. This finding is source-traced; no contributor code or tests were executed locally. No merge was performed.

Comment thread src/skillspector/nodes/analyzers/static_python_shell_truthiness.py Outdated
Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Signed-off-by: Christopher Kevin <christopherk@nvidia.com>

@rng1995 rng1995 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[SkillSpector Review]

Hi @chrisknvidia, thank you for working through every receiver-trust case from the earlier rounds on #475!

Value and readiness: The nested-RHS finding from the last round is fixed. Receiver trust now uses the same value-safety check in the forward scan, the annotated, expression and assert branches, and both prepasses. However, the current head loses TM1 coverage that main already has. postprocess_path_findings drops every same-scope lexical shell=<name> finding (the #560 detection that closed #475) and hands it to the companion. The companion now abstains after almost any ordinary statement. So common forms of the exact #475 bypass get zero TM1 at this head, while main reports HIGH TM1. Not ready: one blocker.

Previous findings:

  • shell=enabled, env=build_env() evaluation order (reviews at bd09251): Resolved. _shell_argument_is_captured_before_effects is unchanged. It is still covered by test_later_keyword_effect_preserves_captured_shell_value and test_earlier_argument_effect_keeps_shell_value_uncertain.
  • Receiver trust after effectful arguments of the direct call (c3b0ff9): Resolved, unchanged since.
  • Receiver trust after standalone generic calls (995d746, inline): Resolved, unchanged since.
  • Receiver trust after effects nested in a tuple/list RHS (256a7ea, inline): Resolved. _value_preserves_receiver_trust (static_python_shell_truthiness.py:725-780) now recurses through tuple, list and comprehension values. _scan_assignment, the AnnAssign/Expr/Assert branches and _advance_trusted_names all use it. Tuple, list, annotated, assert and bare-expression variants are tested for both the direct path and called functions.

Material findings

  1. [Blocker] src/skillspector/nodes/analyzers/static_patterns_tool_misuse.py:3916 (with static_python_shell_truthiness.py:1078-1081 and :1191-1193): when the companion abstains, main's lexical TM1 is suppressed. For a same-scope _VARIABLE_SHELL_FLAG_RE match, reconciliation discards the regex finding unconditionally. For true-prefixed names (:3899), it discards the finding whenever ownership is False. Since c6f6b28/fce761c/08a1caa, the companion clears receiver trust after any non-passive RHS (attribute, subscript, interpolated f-string, any call). The prepass also clears it after every unsupported statement (if, for, with, try, return, ...), which removes trust from function bodies defined earlier. The result is that "not proven" is treated as "not a finding". I traced each of these through the source. Each gets HIGH TM1 on main (the first two also on 256a7ea) and zero TM1 at 08a1caa:

    import subprocess
    import sys
    
    def run(command):
        use_shell = True
        result = subprocess.run(command, shell=use_shell, capture_output=True, text=True)
        return result.stdout
    
    if __name__ == "__main__":
        run(sys.argv[1])
    import subprocess
    import sys
    
    command = f"python {sys.argv[1]}"
    enabled = True
    result = subprocess.run(command, shell=enabled, capture_output=True, text=True)
    import subprocess
    if True:
        enabled = True
        subprocess.run(cmd, shell=enabled)

    The third form is a one-line wrapper an attacker can add to any payload. It has been dropped since 75d1827, and earlier rounds should have flagged it. My previous request to widen invalidation is what made the first two regress, because of how it combines with exclusive same-scope ownership. Several tests encode the loss instead of catching it. test_calls_inside_compound_statements_are_out_of_scope, test_compound_statement_conservatively_clears_truth_facts and test_unsupported_assignment_clears_existing_facts assert zero TM1 for inputs main flags. 08a1caa moves payload = input() / os.environ.copy() below the call in test_shared_python_ast.py. It also replaces command with a literal in test_function_local_binding_and_outer_fact_are_independent, which hides the same effect. Main's #560 tests in tests/unit/test_patterns_new.py call analyze() directly, without deferral, so they never exercise the pipeline path.

    Expected fix: keep the companion's strict invalidation, but make reconciliation fail closed. Drop the lexical candidate only when the companion emitted TM1 for that call, or when it has affirmative counter-evidence: an observed rebinding of the flag, or of the receiver to a non-subprocess value. When trust was merely lost, or the construct is unsupported, keep the lexical finding. Apply the same rule to the true-prefixed direct path at :3899. Add node()-level regressions for the three forms above, each asserting exactly one HIGH TM1. Restore the original fixtures in test_shared_python_ast.py and test_function_local_binding_and_outer_fact_are_independent.

PIC tradeoffs: The PIC should confirm that main's #560 lexical TM1 is the minimum this companion may never remove. Its precision gains (proxy receivers, rebinding) should only apply where it has positive evidence.

Verification and gaps: I compared the PR's own change at 256a7ea with 08a1caa. The two main merges (f7d9398, 1b9db61) left it intact. The only difference is that the defer_variable_reconciliation hook in static_runner.py was re-indented into main's new non-prepared branch. static_patterns_tool_misuse has no prepare_analysis hook, so it still takes that branch. I reviewed all three author commits in full. I traced the forward scan, both prepasses and postprocess_path_findings for the examples above against main's _VARIABLE_SHELL_FLAG_PATTERN and _variable_shell_flag_same_scope. All six hosted checks pass on 08a1caa, and ruff check is clean on the changed files. Per policy I did not execute contributor code or tests, so the examples are traced through the source, not run. The branch is behind main but mergeable; no rebase is needed.


Decision: Changes Requested (reviewed head 08a1caa49d7cdbff158fb0e8c4aa2e23082583b8)

Comment thread src/skillspector/nodes/analyzers/static_patterns_tool_misuse.py Outdated
Comment thread tests/nodes/analyzers/test_tool_misuse_python_ast.py
chrisknvidia and others added 5 commits October 5, 2026 14:24
Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>
Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>
Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

@rng1995 rng1995 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-reviewed exact head 64f3453514a38bdc93ad941b07862ab2aadba4e0 against all earlier reviews and the full current diff. The reported original lexical-abstention cases now retain HIGH TM1, and focused analyzer/runner/end-to-end tests pass. Independent base-to-head reproductions confirm three P1 groups: untaken expression stores, identity-preserving receiver/flag stores, and an earlier compound-statement invocation hidden by a future module store. Each still executes the native subprocess call with shell=True but loses its HIGH finding on this head. Details and minimal reproductions are inline. The green hosted checks do not cover these regressions. Changes requested; do not merge until they are fixed. The test-specific earlier thread is resolved; the broader affirmative-proof/fail-closed thread remains open. Public CLI reproductions change from 38/CAUTION on the base to 9/SAFE on this head.

Comment thread src/skillspector/nodes/analyzers/static_patterns_tool_misuse.py
Comment thread src/skillspector/nodes/analyzers/static_patterns_tool_misuse.py
Comment thread src/skillspector/nodes/analyzers/static_patterns_tool_misuse.py Outdated
rng1995 and others added 3 commits October 8, 2026 12:16
Resolve the static_patterns_tool_misuse.py import conflict by keeping both
the PR's ParsedPythonFile/Finding imports and main's python_tokens imports.

Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Address the three remaining review findings on lexical TM1 reconciliation,
where a bound shell=True call still ran natively but lost its HIGH finding:

- Untaken expression stores: a walrus in a short-circuited BoolOp operand,
  an untaken conditional-expression arm, or a comprehension/generator body
  is no longer recorded as replacement evidence. Only constant operands
  and tests prove that the store executed.
- Identity-preserving stores: unpacked targets are paired with their RHS
  elements. Self-stores are transparent, truthy constants keep the flag,
  and native receiver aliases (saved = subprocess, import subprocess as
  saved, Popen = subprocess.Popen) keep the native receiver.
- Deferred bodies: an outer store only proves replacement when every
  invocation the owner scope can reach sees it. Any reference to the
  function, method or class (compound statements, main guards, callbacks,
  other bodies) can invoke it from that point on, and decorators or
  lambdas can run once defined. This also covers a re-import before a
  later invocation.

The release-trigger test for a called function now matches its
module-level twin: the companion abstains and the lexical HIGH remains.

Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's NVIDIA#784 test expects the tool-misuse ledger event for an invalid
Python file to carry obfuscated_instruction_text. This PR makes tool
misuse parse Python for TM1 reconciliation, so the runner's
higher-precedence syntax_error now names that partial event. Keep the
fail-closed contract: assert the marker reading through the lexical-only
anti-refusal analyzer and keep tool misuse partial with syntax_error.

Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@rng1995 rng1995 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[SkillSpector Review]

Hi @chrisknvidia, thank you for the long effort on #475 and for the lexical-abstention matrix, which made the remaining gaps easy to pin down!

Value and readiness: Ready to merge. Bound shell=<name> calls now get the same TM1 as the literal form when the binding is provably true, and the companion abstains instead of guessing. With these last fixes, lexical TM1 is revoked only by a replacement proven to execute before the call. That closes the evasion class from #475 without losing any detection main has today.

Previous findings (review at 64f3453)

  • [P1] Untaken short-circuit and conditional-expression stores: Resolved in fae399c. record_binding now treats BoolOp operands, IfExp arms and comprehension or generator bodies as conditional unless constants prove they execute. False and (enabled := False) and False and (subprocess := None) keep one HIGH again, and taken arms still remove the candidate.

  • [P1] Identity-preserving receiver and flag stores: Resolved in fae399c. Tuple and list targets are paired with their RHS elements:

    • self-stores are transparent;
    • truthy constants keep the flag;
    • a name counts as a native alias only when every binding of it captures the native receiver.

    saved = subprocess; subprocess = saved, subprocess, saved = subprocess, 1 and enabled, ignored = True, 1 all keep one HIGH.

  • [P1] Future module store hiding an earlier invocation: Resolved in fae399c. For a deferred body, an outer store counts only when every invocation the module can reach sees a replacement. A reference anywhere outside the body can invoke it, whether in a compound statement, a main guard, a callback or another body. Decorated functions and lambdas can run as soon as they are defined. if True: run(...) followed by subprocess = None keeps one HIGH again.

  • Broad fail-closed thread (review at 08a1caa): Resolved. Together with the earlier 9d430d1 follow-ups, ownership uncertainty never drops main's lexical TM1.

What I changed on the branch (all signed off)

  • 4c41790 merges current main. It keeps both sides' imports in static_patterns_tool_misuse.py.
  • fae399c is the fix above, with 34 new node-level cases (they all fail on 64f3453) and 4 graph-level cases. test_import_binding_release_invalidates_called_function_trust now expects the lexical HIGH, like its module-level twin. The __del__ release is an unknown effect, and the previous zero came only from source order.
  • 2c8e480 adapts main's #784 marker-ownership test. Tool misuse now parses Python, so an invalid .py file's partial event carries the higher-precedence syntax_error. The test checks the marker reading through the lexical-only anti-refusal analyzer and keeps tool misuse PARTIAL.

Verification:

  • A 20-layout probe matches main on every positive and keeps every proven-replacement control suppressed.
  • The full non-integration suite passes locally: 10,265 in tests/nodes, tests/unit and tests/test_python_ast.py, plus 668 in the rest of tests/.
  • ruff check and ruff format --check are clean, and all six CI checks pass on 2c8e480.

Stack note: #578 and #579 carry the same reconciliation code. I applied the identical fix there, so they stay consistent after this merges.


Decision: Ready to merge. A human maintainer needs to approve (reviewed head 2c8e4808b8ce4017290152fa14278f186cb9ace5). This bot pushed commits to the branch, so it does not approve the PR itself.

@rng1995
rng1995 merged commit 5ee48ef into NVIDIA:main Oct 8, 2026
6 checks passed
rng1995 added a commit to chrisknvidia/SkillSpector that referenced this pull request Oct 8, 2026
… proof

Port the six true-prefixed TM1 regression tests from NVIDIA#577. This branch
already carries test_true_direct_calls_on_one_line_keep_distinct_locations
as test_true_direct_calls_around_safe_comprehension_keep_distinct_locations,
so only the other five are added. Two of them reported the same call twice
on this branch:

- A variable window for a name spelled `true` (in any case) duplicated
  the case-insensitive direct `shell=True` owner at the same call. AST
  reconciliation removes that window only when the dataflow companion
  takes the call, so a file that does not parse, or a call after an
  unknown receiver effect, kept both findings. coalesce_path_findings
  now drops such a window when a direct lexical owner starts at the
  same call. A window for a call whose direct candidate was folded into
  an identical same-line call still reports that call.
- A call-anchored window for a longer true-prefixed name (`true_value`)
  used its raw text as identity, so the raw and normalized security
  views of one call did not collapse, and reconciliation then moved both
  to the call. The window now takes its identity from the normalized
  view text, as the direct owner already does.

Also cover both root causes with a test for a nested `true` call after
an unknown receiver effect and a normalized `true_value` call in a file
that does not parse.

Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rng1995 added a commit to chrisknvidia/SkillSpector that referenced this pull request Oct 8, 2026
…eports

The `\b`-bounded direct pattern does not match `shell=true_value`, so on
this branch such a call was reported only through a variable window. That
window pairs an assignment with a single call, skips an assignment
inside an earlier window, and was dropped outside Python. Compared with
NVIDIA#577 this lost:

- calls in JavaScript and Markdown files, fenced or plain;
- a later call that the AST companion abstains on, for example after an
  unknown receiver effect;
- all but the last same-line call in a file that does not parse, and the
  second of two identical same-line `shell=true` calls outside Python,
  which the direct candidate key folds into one;
- a call whose assignment sits inside the window of an earlier one.

_variable_shell_matches now pairs every assignment of a true-prefixed
name with each call its bounded window reaches, and the nearest
assignment owns each call. Candidate generation, analysis and AST
reconciliation share that list, and a window's candidate identity uses
its full text, so windows to different calls do not collapse on a shared
200-character preview. Outside Python, call-anchored windows for
true-prefixed names are kept, and coalescing still drops one when a
direct owner reports the same call. With an AST, each added window
passes the same visibility, counterevidence, cached replacement and
companion checks as before. Windows for other names are unchanged.

Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rng1995 added a commit to chrisknvidia/SkillSpector that referenced this pull request Oct 8, 2026
…-identity

NVIDIA#577 landed on main as 5ee48ef. This branch carries the superset of its TM1
work: the same lexical-counterevidence fix, the window-identity changes, the
five NVIDIA#577-only tests ported in 7b643ff, and a differential check showing every
call NVIDIA#577 reports is still reported here. For the four files that conflict
(static_patterns_tool_misuse.py, static_python_shell_truthiness.py,
test_shared_python_ast.py, test_tool_misuse_python_ast.py) keep this branch's
version. No other main commit touched them since the previous merge. The
shared-AST fixture keeps the input()-before-import subprocess order the review
asked for. The other NVIDIA#577 files merged to this branch's content unchanged.

Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
rng1995 added a commit to chrisknvidia/SkillSpector that referenced this pull request Oct 8, 2026
… proof

Port the six true-prefixed TM1 regression tests from NVIDIA#577. This branch
already carries test_true_direct_calls_on_one_line_keep_distinct_locations
unchanged, so only the other five are added. Two of them reported the
same call twice on this branch:

- A variable window for a name spelled `true` (in any case) duplicated
  the case-insensitive direct `shell=True` owner at the same call. AST
  reconciliation removes that window only when the dataflow companion
  takes the call, so a file that does not parse, or a call after an
  unknown receiver effect, kept both findings. coalesce_path_findings
  now drops such a window when a direct lexical owner starts at the
  same call. A window for a call whose direct candidate was folded into
  an identical same-line call still reports that call.
- A call-anchored window for a longer true-prefixed name (`true_value`)
  used its raw text as identity, so the raw and normalized security
  views of one call did not collapse, and reconciliation then moved both
  to the call. The window now takes its identity from the normalized
  view text, as the direct owner already does.

Also cover both root causes with a test for a nested `true` call after
an unknown receiver effect and a normalized `true_value` call in a file
that does not parse.

Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rng1995 added a commit to chrisknvidia/SkillSpector that referenced this pull request Oct 8, 2026
…eports

The `\b`-bounded direct pattern does not match `shell=true_value`, so on
this branch such a call was reported only through a variable window. That
window pairs an assignment with a single call, skips an assignment
inside an earlier window, and was dropped outside Python. Compared with
NVIDIA#577 this lost:

- calls in JavaScript and Markdown files, fenced or plain;
- a later call that the AST companion abstains on, for example after an
  unknown receiver effect;
- all but the last same-line call in a file that does not parse, and the
  second of two identical same-line `shell=true` calls outside Python,
  which the direct candidate key folds into one;
- a call whose assignment sits inside the window of an earlier one.

_variable_shell_matches now pairs every assignment of a true-prefixed
name with each call its bounded window reaches, and the nearest
assignment owns each call. Candidate generation, analysis and AST
reconciliation share that list, and a window's candidate identity uses
its full text, so windows to different calls do not collapse on a shared
200-character preview. Outside Python, call-anchored windows for
true-prefixed names are kept, and coalescing still drops one when a
direct owner reports the same call. With an AST, each added window
passes the same visibility, counterevidence, cached replacement and
companion checks as before. Windows for other names are unchanged.

Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rng1995 added a commit to chrisknvidia/SkillSpector that referenced this pull request Oct 8, 2026
…ecution-surface-classification

NVIDIA#577 landed on main as 5ee48ef. This branch carries the superset of its TM1
work: the same lexical-counterevidence fix, the window-identity and
execution-surface changes, the NVIDIA#577-only tests ported in 2ba149d, and a
differential check showing every call NVIDIA#577 reports is still reported here.
For the four files that conflict (static_patterns_tool_misuse.py,
static_python_shell_truthiness.py, test_shared_python_ast.py,
test_tool_misuse_python_ast.py) keep this branch's version. No other main
commit touched them since the previous merge. The shared-AST fixture keeps
the input()-before-import subprocess order the review asked for. The other
NVIDIA#577 files merged to this branch's content unchanged.

Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
rng1995 added a commit that referenced this pull request Oct 9, 2026
* fix(analyzer): stabilize TM1 window identity

Signed-off-by: Christopher Kevin <christopherk@nvidia.com>

* fix(analyzer): track eager receiver side effects

Signed-off-by: Christopher Kevin <christopherk@nvidia.com>

* fix(analyzer): preserve lexical TM1 when dataflow abstains

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix(analyzer): require a retained companion owner

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix(analyzer): preserve execution scope in binding evidence

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* style(analyzer): simplify binding event deduplication

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix: preserve runtime and retained shell evidence

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix(analyzer): preserve called-slot identity and cached method evidence

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix: preserve cached subprocess slot state across scoped imports

Signed-off-by: Christopher Kevin <christopherk@nvidia.com>

* fix(analyzer): reconcile explicit cached-slot evidence across scopes

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix: abstain from cached slot proof after eager effects

Signed-off-by: Christopher Kevin <christopherk@nvidia.com>

* test: retain lexical findings after unknown cached-slot effects

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix: bound direct called-slot evidence to known execution order

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix: limit direct slot proof to single assignment targets

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix: invalidate slot evidence on protocols and unsafe releases

Signed-off-by: Christopher Kevin <christopherk@nvidia.com>

* fix: keep legacy shadow proof isolated from later effects

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix: abstain from cached replacement proof after unknown effects

Signed-off-by: Christopher Kevin <christopherk@nvidia.com>

* fix: retain native callable detection across cached slot stores

Signed-off-by: Christopher Kevin <christopherk@nvidia.com>

* fix: retain shell findings for native receiver assignments

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix: preserve native Popen replacement warnings

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix(analyzer): require proven replacement before revoking lexical TM1

Address the three remaining review findings on lexical TM1 reconciliation,
where a bound shell=True call still ran natively but lost its HIGH finding:

- Untaken expression stores: a walrus in a short-circuited BoolOp operand,
  an untaken conditional-expression arm, or a comprehension/generator body
  is no longer recorded as replacement evidence. Only constant operands
  and tests prove that the store executed.
- Identity-preserving stores: unpacked targets are paired with their RHS
  elements. Self-stores are transparent, truthy constants keep the flag,
  and native receiver aliases (saved = subprocess, import subprocess as
  saved, Popen = subprocess.Popen) keep the native receiver.
- Deferred bodies: an outer store only proves replacement when every
  invocation the owner scope can reach sees it. Any reference to the
  function, method or class (compound statements, main guards, callbacks,
  other bodies) can invoke it from that point on, and decorators or
  lambdas can run once defined. This also covers a re-import before a
  later invocation.

The release-trigger test for a called function now matches its
module-level twin: the companion abstains and the lexical HIGH remains.

Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: account for TM1 Python parsing in marker ownership ledger

Main's #784 test expects the tool-misuse ledger event for an invalid
Python file to carry obfuscated_instruction_text. This PR makes tool
misuse parse Python for TM1 reconciliation, so the runner's
higher-precedence syntax_error now names that partial event. Keep the
fail-closed contract: assert the marker reading through the lexical-only
anti-refusal analyzer and keep tool misuse partial with syntax_error.

Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(analyzer): keep one TM1 owner for true-prefixed names without AST proof

Port the six true-prefixed TM1 regression tests from #577. This branch
already carries test_true_direct_calls_on_one_line_keep_distinct_locations
as test_true_direct_calls_around_safe_comprehension_keep_distinct_locations,
so only the other five are added. Two of them reported the same call twice
on this branch:

- A variable window for a name spelled `true` (in any case) duplicated
  the case-insensitive direct `shell=True` owner at the same call. AST
  reconciliation removes that window only when the dataflow companion
  takes the call, so a file that does not parse, or a call after an
  unknown receiver effect, kept both findings. coalesce_path_findings
  now drops such a window when a direct lexical owner starts at the
  same call. A window for a call whose direct candidate was folded into
  an identical same-line call still reports that call.
- A call-anchored window for a longer true-prefixed name (`true_value`)
  used its raw text as identity, so the raw and normalized security
  views of one call did not collapse, and reconciliation then moved both
  to the call. The window now takes its identity from the normalized
  view text, as the direct owner already does.

Also cover both root causes with a test for a nested `true` call after
an unknown receiver effect and a normalized `true_value` call in a file
that does not parse.

Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(analyzer): report each true-prefixed shell call that #577 reports

The `\b`-bounded direct pattern does not match `shell=true_value`, so on
this branch such a call was reported only through a variable window. That
window pairs an assignment with a single call, skips an assignment
inside an earlier window, and was dropped outside Python. Compared with
#577 this lost:

- calls in JavaScript and Markdown files, fenced or plain;
- a later call that the AST companion abstains on, for example after an
  unknown receiver effect;
- all but the last same-line call in a file that does not parse, and the
  second of two identical same-line `shell=true` calls outside Python,
  which the direct candidate key folds into one;
- a call whose assignment sits inside the window of an earlier one.

_variable_shell_matches now pairs every assignment of a true-prefixed
name with each call its bounded window reaches, and the nearest
assignment owns each call. Candidate generation, analysis and AST
reconciliation share that list, and a window's candidate identity uses
its full text, so windows to different calls do not collapse on a shared
200-character preview. Outside Python, call-anchored windows for
true-prefixed names are kept, and coalescing still drops one when a
direct owner reports the same call. With an AST, each added window
passes the same visibility, counterevidence, cached replacement and
companion checks as before. Windows for other names are unchanged.

Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Co-authored-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>
Co-authored-by: Narendran Raghavan <nraghavan@nvidia.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
rng1995 added a commit that referenced this pull request Oct 9, 2026
* fix(analyzer): stabilize TM1 window identity

Signed-off-by: Christopher Kevin <christopherk@nvidia.com>

* fix(analyzer): track eager receiver side effects

Signed-off-by: Christopher Kevin <christopherk@nvidia.com>

* feat(python): refresh execution surfaces on current main

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix(analyzer): preserve lexical TM1 when dataflow abstains

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix(analyzer): require a retained companion owner

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix(analyzer): preserve execution scope in binding evidence

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix(analyzer): preserve lexical TM1 across conservative Python analysis

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* style(analyzer): simplify binding event deduplication

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix: preserve runtime and retained shell evidence

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix: preserve runtime and retained shell evidence

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix(analyzer): preserve called-slot identity and cached method evidence

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix(analyzer): preserve called-slot identity and cached method evidence

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix: preserve cached subprocess slot state across scoped imports

Signed-off-by: Christopher Kevin <christopherk@nvidia.com>

* fix: preserve cached subprocess slot state across scoped imports

Signed-off-by: Christopher Kevin <christopherk@nvidia.com>

* fix(analyzer): reconcile explicit cached-slot evidence across scopes

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix(analyzer): reconcile explicit cached-slot evidence across scopes

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix: abstain from cached slot proof after eager effects

Signed-off-by: Christopher Kevin <christopherk@nvidia.com>

* fix: abstain from cached slot proof after eager effects

Signed-off-by: Christopher Kevin <christopherk@nvidia.com>

* test: retain lexical findings after unknown cached-slot effects

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* test: retain lexical findings after unknown cached-slot effects

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix: bound direct called-slot evidence to known execution order

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix: bound direct called-slot evidence to known execution order

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix: limit direct slot proof to single assignment targets

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix: limit direct slot proof to single assignment targets

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix: invalidate slot evidence on protocols and unsafe releases

Signed-off-by: Christopher Kevin <christopherk@nvidia.com>

* fix: invalidate slot evidence on protocols and unsafe releases

Signed-off-by: Christopher Kevin <christopherk@nvidia.com>

* fix: keep legacy shadow proof isolated from later effects

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix: keep legacy shadow proof isolated from later effects

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix: abstain from cached replacement proof after unknown effects

Signed-off-by: Christopher Kevin <christopherk@nvidia.com>

* fix: abstain from cached replacement proof after unknown effects

Signed-off-by: Christopher Kevin <christopherk@nvidia.com>

* fix: retain native callable detection across cached slot stores

Signed-off-by: Christopher Kevin <christopherk@nvidia.com>

* fix: retain native callable detection across cached slot stores

Signed-off-by: Christopher Kevin <christopherk@nvidia.com>

* fix: retain shell findings for native receiver assignments

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix: retain shell findings for native receiver assignments

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix: preserve native Popen replacement warnings

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* fix: preserve native Popen replacement warnings

Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>

* test: retain occurrence metadata in prose shell reports

Signed-off-by: Christopher Kevin <christopherk@nvidia.com>

* fix(analyzer): require proven replacement before revoking lexical TM1

Address the three remaining review findings on lexical TM1 reconciliation,
where a bound shell=True call still ran natively but lost its HIGH finding:

- Untaken expression stores: a walrus in a short-circuited BoolOp operand,
  an untaken conditional-expression arm, or a comprehension/generator body
  is no longer recorded as replacement evidence. Only constant operands
  and tests prove that the store executed.
- Identity-preserving stores: unpacked targets are paired with their RHS
  elements. Self-stores are transparent, truthy constants keep the flag,
  and native receiver aliases (saved = subprocess, import subprocess as
  saved, Popen = subprocess.Popen) keep the native receiver.
- Deferred bodies: an outer store only proves replacement when every
  invocation the owner scope can reach sees it. Any reference to the
  function, method or class (compound statements, main guards, callbacks,
  other bodies) can invoke it from that point on, and decorators or
  lambdas can run once defined. This also covers a re-import before a
  later invocation.

The release-trigger test for a called function now matches its
module-level twin: the companion abstains and the lexical HIGH remains.

Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(analyzer): require proven replacement before revoking lexical TM1

Address the three remaining review findings on lexical TM1 reconciliation,
where a bound shell=True call still ran natively but lost its HIGH finding:

- Untaken expression stores: a walrus in a short-circuited BoolOp operand,
  an untaken conditional-expression arm, or a comprehension/generator body
  is no longer recorded as replacement evidence. Only constant operands
  and tests prove that the store executed.
- Identity-preserving stores: unpacked targets are paired with their RHS
  elements. Self-stores are transparent, truthy constants keep the flag,
  and native receiver aliases (saved = subprocess, import subprocess as
  saved, Popen = subprocess.Popen) keep the native receiver.
- Deferred bodies: an outer store only proves replacement when every
  invocation the owner scope can reach sees it. Any reference to the
  function, method or class (compound statements, main guards, callbacks,
  other bodies) can invoke it from that point on, and decorators or
  lambdas can run once defined. This also covers a re-import before a
  later invocation.

The release-trigger test for a called function now matches its
module-level twin: the companion abstains and the lexical HIGH remains.

Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: account for TM1 Python parsing in marker ownership ledger

Main's #784 test expects the tool-misuse ledger event for an invalid
Python file to carry obfuscated_instruction_text. This PR makes tool
misuse parse Python for TM1 reconciliation, so the runner's
higher-precedence syntax_error now names that partial event. Keep the
fail-closed contract: assert the marker reading through the lexical-only
anti-refusal analyzer and keep tool misuse partial with syntax_error.

Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: account for TM1 Python parsing in marker ownership ledger

Main's #784 test expects the tool-misuse ledger event for an invalid
Python file to carry obfuscated_instruction_text. This PR makes tool
misuse parse Python for TM1 reconciliation, so the runner's
higher-precedence syntax_error now names that partial event. Keep the
fail-closed contract: assert the marker reading through the lexical-only
anti-refusal analyzer and keep tool misuse partial with syntax_error.

Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(analyzer): keep one TM1 owner for true-prefixed names without AST proof

Port the six true-prefixed TM1 regression tests from #577. This branch
already carries test_true_direct_calls_on_one_line_keep_distinct_locations
unchanged, so only the other five are added. Two of them reported the
same call twice on this branch:

- A variable window for a name spelled `true` (in any case) duplicated
  the case-insensitive direct `shell=True` owner at the same call. AST
  reconciliation removes that window only when the dataflow companion
  takes the call, so a file that does not parse, or a call after an
  unknown receiver effect, kept both findings. coalesce_path_findings
  now drops such a window when a direct lexical owner starts at the
  same call. A window for a call whose direct candidate was folded into
  an identical same-line call still reports that call.
- A call-anchored window for a longer true-prefixed name (`true_value`)
  used its raw text as identity, so the raw and normalized security
  views of one call did not collapse, and reconciliation then moved both
  to the call. The window now takes its identity from the normalized
  view text, as the direct owner already does.

Also cover both root causes with a test for a nested `true` call after
an unknown receiver effect and a normalized `true_value` call in a file
that does not parse.

Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(analyzer): keep one TM1 owner for true-prefixed names without AST proof

Port the six true-prefixed TM1 regression tests from #577. This branch
already carries test_true_direct_calls_on_one_line_keep_distinct_locations
as test_true_direct_calls_around_safe_comprehension_keep_distinct_locations,
so only the other five are added. Two of them reported the same call twice
on this branch:

- A variable window for a name spelled `true` (in any case) duplicated
  the case-insensitive direct `shell=True` owner at the same call. AST
  reconciliation removes that window only when the dataflow companion
  takes the call, so a file that does not parse, or a call after an
  unknown receiver effect, kept both findings. coalesce_path_findings
  now drops such a window when a direct lexical owner starts at the
  same call. A window for a call whose direct candidate was folded into
  an identical same-line call still reports that call.
- A call-anchored window for a longer true-prefixed name (`true_value`)
  used its raw text as identity, so the raw and normalized security
  views of one call did not collapse, and reconciliation then moved both
  to the call. The window now takes its identity from the normalized
  view text, as the direct owner already does.

Also cover both root causes with a test for a nested `true` call after
an unknown receiver effect and a normalized `true_value` call in a file
that does not parse.

Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(analyzer): report each true-prefixed shell call that #577 reports

The `\b`-bounded direct pattern does not match `shell=true_value`, so on
this branch such a call was reported only through a variable window. That
window pairs an assignment with a single call, skips an assignment
inside an earlier window, and was dropped outside Python. Compared with
#577 this lost:

- calls in JavaScript and Markdown files, fenced or plain;
- a later call that the AST companion abstains on, for example after an
  unknown receiver effect;
- all but the last same-line call in a file that does not parse, and the
  second of two identical same-line `shell=true` calls outside Python,
  which the direct candidate key folds into one;
- a call whose assignment sits inside the window of an earlier one.

_variable_shell_matches now pairs every assignment of a true-prefixed
name with each call its bounded window reaches, and the nearest
assignment owns each call. Candidate generation, analysis and AST
reconciliation share that list, and a window's candidate identity uses
its full text, so windows to different calls do not collapse on a shared
200-character preview. Outside Python, call-anchored windows for
true-prefixed names are kept, and coalescing still drops one when a
direct owner reports the same call. With an AST, each added window
passes the same visibility, counterevidence, cached replacement and
companion checks as before. Windows for other names are unchanged.

Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(analyzer): report each true-prefixed shell call that #577 reports

The `\b`-bounded direct pattern does not match `shell=true_value`, so on
this branch such a call was reported only through a variable window. That
window pairs an assignment with a single call, skips an assignment
inside an earlier window, and was dropped outside Python. Compared with
#577 this lost:

- calls in JavaScript and Markdown files, fenced or plain;
- a later call that the AST companion abstains on, for example after an
  unknown receiver effect;
- all but the last same-line call in a file that does not parse, and the
  second of two identical same-line `shell=true` calls outside Python,
  which the direct candidate key folds into one;
- a call whose assignment sits inside the window of an earlier one.

_variable_shell_matches now pairs every assignment of a true-prefixed
name with each call its bounded window reaches, and the nearest
assignment owns each call. Candidate generation, analysis and AST
reconciliation share that list, and a window's candidate identity uses
its full text, so windows to different calls do not collapse on a shared
200-character preview. Outside Python, call-anchored windows for
true-prefixed names are kept, and coalescing still drops one when a
direct owner reports the same call. With an AST, each added window
passes the same visibility, counterevidence, cached replacement and
companion checks as before. Windows for other names are unchanged.

Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(supply-chain): opt into Python source-type propagation

SC2's literal-XOR decoding and SC3 branch on file_type, but the
supply-chain analyzer was not in the USES_PYTHON_SOURCE_TYPE set, so
Python executed through an extensionless shebang file or a shebang
Markdown file received its suffix type and silently lost the decoded
SC2 HIGH while completeness stayed true. Opt the module in and add
graph-level parity tests for .py, .pyw, extensionless and .md surfaces.

Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(build-context): decode PEP 263 primary Python before UTF-8 rejection

A selected primary file (direct .py/.pyw or extensionless Python
shebang) declaring a supported PEP 263 encoding such as latin-1 was
rejected as unsupported_primary_content because the generic primary
check only accepts UTF-8. The later source-decoding pass then decoded
it correctly, but the artifact stayed FAILED with a fatal event and 0%
coverage. Waive the generic UTF-8 rejection when the bytes classify as
Python and decode under their declared encoding; an unknown codec or
bytes invalid for the declared codec remain fatal, and format checks
(BOMs, archives, NUL density) are unchanged. The waiver is evaluated
lazily and not after the shared deadline, so no post-cache Python work
starts once the budget is spent.

Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Co-authored-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>
Co-authored-by: Narendran Raghavan <nraghavan@nvidia.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
rng1995 added a commit that referenced this pull request Oct 9, 2026
Resolve the conflicts with #577 (5ee48ef) in
static_patterns_tool_misuse.py.

#577 replaced the per-match reparse with a single AST index
(_build_variable_shell_ast_index) and moved variable reconciliation
into postprocess_path_findings. That supersedes this branch's
_VariableShellScopeIndex, so the resolution keeps main's variable-shell
design and drops the index class, its _index_scope method and the
deque import.

Kept from this branch:
- USES_RUNTIME_CHECK and the check_runtime keyword, alongside main's
  defer_variable_reconciliation keyword.
- runtime_check() calls in the TM1-TM4 loops, the Perl helper and the
  non-deferred reconciliation block.
- SourceLocationIndex and get_context_from_lines for TM1-TM4 line and
  context lookups.

Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.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.

shell=True detection can be bypassed through variable assignment (malicious skill example that bypasses this static detection model)

2 participants