Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
26e97ec
fix: confine failed shell words to proven Python values
yashrajp22 Nov 5, 2026
f73de04
test: exercise guarded Python value boundaries with executable operands
yashrajp22 Nov 5, 2026
701a20f
test: retain evidenced later command and word-limit controls
yashrajp22 Nov 5, 2026
cc4fc75
Merge branch 'main' into yashraj/python-host-word-boundaries-followup
github-actions[bot] Nov 6, 2026
3ece632
Merge branch 'main' into yashraj/python-host-word-boundaries-followup
github-actions[bot] Nov 6, 2026
1d02e8b
Merge branch 'main' into yashraj/python-host-word-boundaries-followup
github-actions[bot] Nov 6, 2026
59283a9
Merge branch 'main' into yashraj/python-host-word-boundaries-followup
github-actions[bot] Nov 6, 2026
d598cc4
Merge branch 'main' into yashraj/python-host-word-boundaries-followup
github-actions[bot] Nov 6, 2026
ebe5e3d
Merge branch 'main' into yashraj/python-host-word-boundaries-followup
github-actions[bot] Nov 6, 2026
7f892b2
Merge branch 'main' into yashraj/python-host-word-boundaries-followup
github-actions[bot] Nov 6, 2026
cf9007b
Merge branch 'main' into yashraj/python-host-word-boundaries-followup
github-actions[bot] Nov 6, 2026
5d0c569
Merge branch 'main' into yashraj/python-host-word-boundaries-followup
github-actions[bot] Nov 9, 2026
9e6a339
Merge branch 'main' into yashraj/python-host-word-boundaries-followup
github-actions[bot] Nov 9, 2026
1dfb3c3
Merge branch 'main' into yashraj/python-host-word-boundaries-followup
github-actions[bot] Nov 9, 2026
f44d002
Merge branch 'main' into yashraj/python-host-word-boundaries-followup
github-actions[bot] Nov 9, 2026
eb2d58d
Merge branch 'main' into yashraj/python-host-word-boundaries-followup
github-actions[bot] Nov 9, 2026
1fccc9c
Merge branch 'main' into yashraj/python-host-word-boundaries-followup
github-actions[bot] Nov 9, 2026
c54d275
Merge branch 'main' into yashraj/python-host-word-boundaries-followup
github-actions[bot] Nov 9, 2026
af8baf3
Merge branch 'main' into yashraj/python-host-word-boundaries-followup
github-actions[bot] Nov 9, 2026
ddff665
Merge branch 'main' into yashraj/python-host-word-boundaries-followup
github-actions[bot] Nov 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/skillspector/nodes/analyzers/static_patterns_tool_misuse.py
Original file line number Diff line number Diff line change
Expand Up @@ -3724,6 +3724,23 @@ def _has_shell_command_word_exhaustion(
# spans and runtime-selected operands keep their existing
# conservative treatment.
continue
if python_word_source is not None and ownership_boundary < parsed.end:
# A raw shell word may pair Python delimiters across unrelated
# tokens. Recheck only the complete value the native owner proves.
value_source_range: list[int] = []
value = python_word_source.string_value(
start, parsed.end, source_range=value_source_range
)
if value is not None:
value_diagnostic = _has_shell_command_word_exhaustion(
value[0],
check_runtime,
_command_string_depth=_command_string_depth,
_quoted_printf_allowed=quoted_printf_allowed,
)
if value_diagnostic is not None:
return value_diagnostic.enclosed(*value_source_range)
continue
if not _quoted_candidate_is_complete(
content, start, quote_limits, check_runtime, quoted_printf_allowed
):
Expand Down
105 changes: 105 additions & 0 deletions tests/nodes/analyzers/test_python_host_word_bounds.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""A failed shell word cannot borrow unrelated Python string delimiters."""

from __future__ import annotations

import pytest

from skillspector.inspection_ledger import LedgerOutcome, LedgerReason, ParserCause
from skillspector.nodes.analyzers import static_patterns_tool_misuse as tm
from skillspector.nodes.analyzers import static_runner

# Alternating Python delimiters make a raw shell word charge later token bodies
# to the first value. Each native value remains short and independent.
_TAIL = "".join(f'FIELD_{index} = "ordinary_value_{index}"\n' for index in range(600))


def _diagnostic(source: str):
return tm.bounded_parse_diagnostic(source, lambda: None, file_type="python")


@pytest.mark.parametrize("newline", ["\n", "\r\n"])
@pytest.mark.parametrize(
"statement",
[
'def read(access: str = "read"):\n return access\n',
'RECORD = {"status": "diagnostic_only"}\n',
'STATES = {"resource_limited", "completed"}\n',
'PREFIXES = ("run_recomputed_reference_", "run_default_")\n',
'MESSAGE = "café 🧪 literal"\n',
],
)
def test_short_python_values_do_not_consume_later_token_bodies(
statement: str, newline: str
) -> None:
source = (statement + _TAIL).replace("\n", newline)

assert _diagnostic(source) is None


@pytest.mark.parametrize(
"extension", [" + unknown", " % unknown", ".format(unknown)", " * count", "[part:]"]
)
def test_unproved_value_extensions_keep_the_original_failure(extension: str) -> None:
source = 'def read(access: str = "read"' + extension + "):\n return access\n" + _TAIL
owner = tm._PythonSourceOwnership(source, lambda: None)
start = source.index('read"')
assert owner.string_value(start, start + len("read")) is None

assert _diagnostic(source) is not None


def test_later_runtime_command_remains_independently_partial() -> None:
source = 'FIRST = "ordinary"\n' + _TAIL + 'COMMAND = "$TOOL -rf /"\n'

diagnostic = _diagnostic(source)
assert diagnostic is not None
assert diagnostic.cause is ParserCause.RUNTIME_DEPENDENT


@pytest.mark.parametrize("prefix", ["", 'FIRST = "ordinary"\n' + _TAIL], ids=["first", "later"])
def test_a_genuinely_over_budget_python_value_keeps_the_limit(prefix: str) -> None:
source = prefix + 'VALUE = "' + "r" * 4097 + '"\n' + _TAIL

diagnostic = _diagnostic(source)
assert diagnostic is not None
assert diagnostic.cause is ParserCause.RESOURCE_LIMIT
assert diagnostic.measurements["observed_characters"] == 4097
assert diagnostic.measurements["limit_characters"] == 4096


def test_native_ownership_requires_complete_valid_python() -> None:
source = 'FENCE = "\\n```\\n"\n' + _TAIL + "def broken(:\n"
assert tm._python_literal_spans(source, lambda: None) is None

assert _diagnostic(source) is not None
assert (
tm.bounded_parse_diagnostic(
source, lambda: None, file_type="python", complete_context=False
)
is not None
)


def test_later_raw_high_findings_are_preserved_after_value_reassessment() -> None:
source = 'FIRST = "ordinary"\n' + _TAIL + 'DANGEROUS = "rm -rf /"\n'
result = static_runner.run_static_patterns_with_ledger(
{"components": ["script.py"], "file_cache": {"script.py": source}}, [tm]
)

assert any(f.rule_id == "TM1" and f.severity == "HIGH" for f in result["findings"])
assert result["inspection_ledger"][0]["outcome"] is LedgerOutcome.COMPLETED


def test_later_runtime_limit_reaches_the_ledger() -> None:
source = 'FIRST = "ordinary"\n' + _TAIL + 'COMMAND = "$TOOL -rf /"\n'
result = static_runner.run_static_patterns_with_ledger(
{"components": ["script.py"], "file_cache": {"script.py": source}}, [tm]
)

assert any(
event.get("reason_code") is LedgerReason.STATIC_PARSE_LIMIT
for event in result["inspection_ledger"]
)
Loading