From ab2f21cf456d0b1290c4b0b9889630eb15caf1dc Mon Sep 17 00:00:00 2001 From: yashrajbasav Date: Thu, 5 Nov 2026 21:10:21 +0530 Subject: [PATCH 1/4] fix: preserve JSON grammar for decoded bundle views Signed-off-by: yashrajbasav --- .../nodes/analyzers/static_runner.py | 31 +++++++--- .../analyzers/test_oms_projected_file_type.py | 57 +++++++++++++++++++ 2 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 tests/nodes/analyzers/test_oms_projected_file_type.py diff --git a/src/skillspector/nodes/analyzers/static_runner.py b/src/skillspector/nodes/analyzers/static_runner.py index 269ea225e..8734e3237 100644 --- a/src/skillspector/nodes/analyzers/static_runner.py +++ b/src/skillspector/nodes/analyzers/static_runner.py @@ -753,11 +753,13 @@ def _uses_python_source_type(module: object) -> bool: ) -def _effective_module_file_type(path: str, module: object, *, python_source: bool) -> str: +def _effective_module_file_type( + path: str, module: object, *, python_source: bool, file_type: str | None = None +) -> str: """Resolve the file type consistently for every hook owned by one module.""" if python_source and _uses_python_source_type(module): return "python" - return _infer_file_type(path) + return file_type or _infer_file_type(path) def _requires_python_ast(pattern_modules: list) -> bool: @@ -1104,10 +1106,11 @@ def _scan_path( prepared_analyses: Mapping[int, object] | None = None, source_view: SecurityTextView | None = None, analysis_method: str = "analyze", + file_type: str | None = None, ) -> tuple[list[Finding], _StaticResourceLimitError | None]: """Run pattern modules with construction, emission, and runtime guards.""" findings: list[Finding] = [] - file_type = _infer_file_type(path) + file_type = file_type or _infer_file_type(path) if python_source is None: python_source = may_be_python_source(path, content) content_lines = content.splitlines() @@ -1127,6 +1130,7 @@ def _scan_path( path, module, python_source=python_source, + file_type=file_type, ) module_finding_start = len(findings) occurrence_columns = _OccurrenceColumnResolver(content, line_starts) @@ -1339,6 +1343,7 @@ def _scan_view_windows( source_view: SecurityTextView | None = None, evidence_source_view: SecurityTextView | None = None, analysis_method: str = "analyze", + file_type: str | None = None, ) -> tuple[list[Finding], _StaticResourceLimitError | None]: """Scan one already-bounded view.""" view_token = _ACTIVE_SECURITY_VIEW.set((view, source_text)) @@ -1353,6 +1358,7 @@ def _scan_view_windows( prepared_analyses=prepared_analyses, source_view=source_view, analysis_method=analysis_method, + file_type=file_type, ) finally: _ACTIVE_SECURITY_VIEW.reset(view_token) @@ -2221,6 +2227,7 @@ def _scan_declared_marker_views( prepared_analyses: Mapping[int, object] | None = None, parser_diagnostics: list[StaticParserDiagnostic] | None = None, python_import_bounds: PythonImportBounds | None = None, + file_type: str | None = None, ) -> tuple[list[Finding], bool, bool, _StaticResourceLimitError | None]: """Reconstruct marker payloads with directive-relative context windows.""" findings: list[Finding] = [] @@ -2236,6 +2243,7 @@ def check_runtime() -> None: ) from exc check_runtime() + file_type = file_type or _infer_file_type(path) projection_limited = False bounded_parse_limited = False seen_views: set[tuple[str, int, int]] = set() @@ -2274,7 +2282,7 @@ def defer_output_limit( # The proof covers the whole artifact, so it holds in every window. It is # computed only when a loose quoted directive header reaches a quote. python_strings = ( - PythonStringClosers(content, check_runtime) if _infer_file_type(path) == "python" else None + PythonStringClosers(content, check_runtime) if file_type == "python" else None ) for owned_start, raw_start in zip(owned_starts, raw_starts, strict=True): @@ -2354,6 +2362,7 @@ def defer_output_limit( view_budget, None, python_source=python_source, + file_type=file_type, source_text=raw_window, prepared_analyses=prepared_analyses, source_view=( @@ -2433,7 +2442,7 @@ def defer_output_limit( module, marker_view, check_runtime, - file_type=_infer_file_type(path), + file_type=file_type, complete_context=complete_context, source_start=raw_start, diagnostics=parser_diagnostics, @@ -2612,6 +2621,8 @@ def _scan_all_views_with_parser_diagnostics( if isinstance(diagnostic, StaticParserDiagnostic): parser_diagnostics.append(diagnostic.enclosed(0, len(content))) return findings, reason, metrics + # A DSSE projection is JSON even when its carrier is named skill.oms.sig. + file_type = "json" if _oms_projected else _infer_file_type(path) ast_modules = [module for module in pattern_modules if _uses_python_ast(module)] lexical_modules = [module for module in pattern_modules if not _uses_python_ast(module)] if python_source is None: @@ -2763,7 +2774,7 @@ def limited_result( if prepare is not None: prepared_analyses[id(module)] = prepare( content=content, - file_type=_infer_file_type(path), + file_type=file_type, check_runtime=finding_budget.check_runtime, ) finding_budget.check_runtime() @@ -2792,6 +2803,7 @@ def limited_result( prepared_analyses=prepared_analyses, parser_diagnostics=parser_diagnostics, python_import_bounds=python_import_bounds, + file_type=file_type, ) bounded_parse_limited = bounded_parse_limited or marker_bounded_parse_limited except _StaticResourceLimitError as exc: @@ -2832,7 +2844,7 @@ def limited_result( module, SecurityTextView("raw", content), finding_budget.check_runtime, - file_type=_infer_file_type(path), + file_type=file_type, complete_context=True, diagnostics=parser_diagnostics, path=path, @@ -2856,6 +2868,7 @@ def limited_result( python_ast_cache_key, python_ast, python_source=python_source, + file_type=file_type, ) except _StaticResourceLimitError as exc: return reconciled_prefix(), exc.reason, exc.metrics @@ -2958,6 +2971,7 @@ def limited_result( path, module, python_source=python_source, + file_type=file_type, ) bounded_parse_limited = bounded_parse_limited or bool( _bounded_parser_incomplete( @@ -2996,6 +3010,7 @@ def limited_result( view_budget, None, python_source=python_source, + file_type=file_type, source_text=raw_window, prepared_analyses=prepared_analyses, source_view=( @@ -3132,6 +3147,7 @@ def limited_result( view_budget, None, python_source=python_source, + file_type=file_type, source_text=projection.text, prepared_analyses=prepared_analyses, source_view=source_view, @@ -3198,6 +3214,7 @@ def limited_result( view_budget, None, python_source=python_source, + file_type=file_type, source_text=continuity.view.text, prepared_analyses=prepared_analyses, source_view=( diff --git a/tests/nodes/analyzers/test_oms_projected_file_type.py b/tests/nodes/analyzers/test_oms_projected_file_type.py new file mode 100644 index 000000000..8eefe1bf4 --- /dev/null +++ b/tests/nodes/analyzers/test_oms_projected_file_type.py @@ -0,0 +1,57 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Decoded bundle syntax is independent of the carrier filename.""" + +import json + +import pytest + +from skillspector.artifacts import SecurityTextView +from skillspector.nodes.analyzers import static_patterns_tool_misuse as tm +from skillspector.nodes.analyzers import static_runner +from skillspector.oms import OMSProjection + + +def _scan(monkeypatch, path, projected, *, complete=True): + carrier = '{"dsseEnvelope": "synthetic carrier"}' + monkeypatch.setattr( + static_runner, + "project_oms_content", + lambda _content: OMSProjection(SecurityTextView("oms-decoded", projected), complete), + ) + return static_runner._scan_all_views_detailed(path, carrier, [tm], None) + + +@pytest.mark.parametrize("path", ["skill.oms.sig", "bundle.json", "notes.md", "bundle.py"]) +def test_projected_json_owns_quotes_regardless_of_carrier_name(monkeypatch, path): + projected = json.dumps({"readable": ["label`with'apostrophe", "ordinary reference"]}) + findings, reason, metrics = _scan(monkeypatch, path, projected) + assert findings == [] + assert reason is None + assert getattr(metrics, "parser_diagnostic", None) is None + + +@pytest.mark.parametrize("complete", [False, True]) +@pytest.mark.parametrize("path", ["skill.oms.sig", "bundle.json", "bundle.py"]) +def test_projected_commands_keep_findings_and_original_carrier(monkeypatch, path, complete): + projected = json.dumps({"instruction": "rm -rf /"}) + findings, _, _ = _scan(monkeypatch, path, projected, complete=complete) + destructive = [finding for finding in findings if finding.rule_id == "TM1"] + assert destructive + assert all( + finding.file == path and finding.start_line == 1 and finding.evidence.get("oms_projection") + for finding in destructive + ) + + +def test_unrecognized_raw_bundle_keeps_runtime_diagnostic(monkeypatch): + monkeypatch.setattr( + static_runner, + "project_oms_content", + lambda content: OMSProjection(SecurityTextView("raw", content), False), + ) + _, _, metrics = static_runner._scan_all_views_detailed( + "skill.oms.sig", 'sh -c "$CMD -rf /"', [tm], None + ) + assert metrics.parser_diagnostic is not None From 46a7d7a681d7ef2b5456c745e84e868244821470 Mon Sep 17 00:00:00 2001 From: yashrajbasav Date: Thu, 5 Nov 2026 21:21:07 +0530 Subject: [PATCH 2/4] fix: keep security rule applicability independent of bundle grammar Signed-off-by: yashrajbasav --- .../nodes/analyzers/static_runner.py | 47 ++++++++----------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/src/skillspector/nodes/analyzers/static_runner.py b/src/skillspector/nodes/analyzers/static_runner.py index 8734e3237..5e8db229e 100644 --- a/src/skillspector/nodes/analyzers/static_runner.py +++ b/src/skillspector/nodes/analyzers/static_runner.py @@ -753,13 +753,11 @@ def _uses_python_source_type(module: object) -> bool: ) -def _effective_module_file_type( - path: str, module: object, *, python_source: bool, file_type: str | None = None -) -> str: +def _effective_module_file_type(path: str, module: object, *, python_source: bool) -> str: """Resolve the file type consistently for every hook owned by one module.""" if python_source and _uses_python_source_type(module): return "python" - return file_type or _infer_file_type(path) + return _infer_file_type(path) def _requires_python_ast(pattern_modules: list) -> bool: @@ -1106,11 +1104,10 @@ def _scan_path( prepared_analyses: Mapping[int, object] | None = None, source_view: SecurityTextView | None = None, analysis_method: str = "analyze", - file_type: str | None = None, ) -> tuple[list[Finding], _StaticResourceLimitError | None]: """Run pattern modules with construction, emission, and runtime guards.""" findings: list[Finding] = [] - file_type = file_type or _infer_file_type(path) + file_type = _infer_file_type(path) if python_source is None: python_source = may_be_python_source(path, content) content_lines = content.splitlines() @@ -1130,7 +1127,6 @@ def _scan_path( path, module, python_source=python_source, - file_type=file_type, ) module_finding_start = len(findings) occurrence_columns = _OccurrenceColumnResolver(content, line_starts) @@ -1343,7 +1339,6 @@ def _scan_view_windows( source_view: SecurityTextView | None = None, evidence_source_view: SecurityTextView | None = None, analysis_method: str = "analyze", - file_type: str | None = None, ) -> tuple[list[Finding], _StaticResourceLimitError | None]: """Scan one already-bounded view.""" view_token = _ACTIVE_SECURITY_VIEW.set((view, source_text)) @@ -1358,7 +1353,6 @@ def _scan_view_windows( prepared_analyses=prepared_analyses, source_view=source_view, analysis_method=analysis_method, - file_type=file_type, ) finally: _ACTIVE_SECURITY_VIEW.reset(view_token) @@ -2227,7 +2221,7 @@ def _scan_declared_marker_views( prepared_analyses: Mapping[int, object] | None = None, parser_diagnostics: list[StaticParserDiagnostic] | None = None, python_import_bounds: PythonImportBounds | None = None, - file_type: str | None = None, + parser_file_type: str | None = None, ) -> tuple[list[Finding], bool, bool, _StaticResourceLimitError | None]: """Reconstruct marker payloads with directive-relative context windows.""" findings: list[Finding] = [] @@ -2243,7 +2237,7 @@ def check_runtime() -> None: ) from exc check_runtime() - file_type = file_type or _infer_file_type(path) + parser_file_type = parser_file_type or _infer_file_type(path) projection_limited = False bounded_parse_limited = False seen_views: set[tuple[str, int, int]] = set() @@ -2282,7 +2276,7 @@ def defer_output_limit( # The proof covers the whole artifact, so it holds in every window. It is # computed only when a loose quoted directive header reaches a quote. python_strings = ( - PythonStringClosers(content, check_runtime) if file_type == "python" else None + PythonStringClosers(content, check_runtime) if parser_file_type == "python" else None ) for owned_start, raw_start in zip(owned_starts, raw_starts, strict=True): @@ -2362,7 +2356,6 @@ def defer_output_limit( view_budget, None, python_source=python_source, - file_type=file_type, source_text=raw_window, prepared_analyses=prepared_analyses, source_view=( @@ -2442,7 +2435,7 @@ def defer_output_limit( module, marker_view, check_runtime, - file_type=file_type, + file_type=parser_file_type, complete_context=complete_context, source_start=raw_start, diagnostics=parser_diagnostics, @@ -2621,8 +2614,9 @@ def _scan_all_views_with_parser_diagnostics( if isinstance(diagnostic, StaticParserDiagnostic): parser_diagnostics.append(diagnostic.enclosed(0, len(content))) return findings, reason, metrics - # A DSSE projection is JSON even when its carrier is named skill.oms.sig. - file_type = "json" if _oms_projected else _infer_file_type(path) + # Decode ownership uses JSON grammar; rule applicability retains the carrier + # type so opaque and unknown bundle fields keep their original security checks. + parser_file_type = "json" if _oms_projected else _infer_file_type(path) ast_modules = [module for module in pattern_modules if _uses_python_ast(module)] lexical_modules = [module for module in pattern_modules if not _uses_python_ast(module)] if python_source is None: @@ -2774,7 +2768,7 @@ def limited_result( if prepare is not None: prepared_analyses[id(module)] = prepare( content=content, - file_type=file_type, + file_type=_infer_file_type(path), check_runtime=finding_budget.check_runtime, ) finding_budget.check_runtime() @@ -2803,7 +2797,7 @@ def limited_result( prepared_analyses=prepared_analyses, parser_diagnostics=parser_diagnostics, python_import_bounds=python_import_bounds, - file_type=file_type, + parser_file_type=parser_file_type, ) bounded_parse_limited = bounded_parse_limited or marker_bounded_parse_limited except _StaticResourceLimitError as exc: @@ -2844,7 +2838,7 @@ def limited_result( module, SecurityTextView("raw", content), finding_budget.check_runtime, - file_type=file_type, + file_type=parser_file_type, complete_context=True, diagnostics=parser_diagnostics, path=path, @@ -2868,7 +2862,6 @@ def limited_result( python_ast_cache_key, python_ast, python_source=python_source, - file_type=file_type, ) except _StaticResourceLimitError as exc: return reconciled_prefix(), exc.reason, exc.metrics @@ -2967,11 +2960,12 @@ def limited_result( ) if callable(exhaustion_hook) and full_view.name != "raw": finding_budget.check_runtime() - module_file_type = _effective_module_file_type( - path, - module, - python_source=python_source, - file_type=file_type, + module_file_type = ( + parser_file_type + if _oms_projected + else _effective_module_file_type( + path, module, python_source=python_source + ) ) bounded_parse_limited = bounded_parse_limited or bool( _bounded_parser_incomplete( @@ -3010,7 +3004,6 @@ def limited_result( view_budget, None, python_source=python_source, - file_type=file_type, source_text=raw_window, prepared_analyses=prepared_analyses, source_view=( @@ -3147,7 +3140,6 @@ def limited_result( view_budget, None, python_source=python_source, - file_type=file_type, source_text=projection.text, prepared_analyses=prepared_analyses, source_view=source_view, @@ -3214,7 +3206,6 @@ def limited_result( view_budget, None, python_source=python_source, - file_type=file_type, source_text=continuity.view.text, prepared_analyses=prepared_analyses, source_view=( From 5542a69c2e4b22fdf45d84bd9cb51235b4455374 Mon Sep 17 00:00:00 2001 From: yashrajbasav Date: Thu, 5 Nov 2026 21:35:09 +0530 Subject: [PATCH 3/4] fix: preserve default marker grammar selection for ordinary files Signed-off-by: yashrajbasav --- src/skillspector/nodes/analyzers/static_runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/skillspector/nodes/analyzers/static_runner.py b/src/skillspector/nodes/analyzers/static_runner.py index 5e8db229e..cf3e1ae26 100644 --- a/src/skillspector/nodes/analyzers/static_runner.py +++ b/src/skillspector/nodes/analyzers/static_runner.py @@ -2797,7 +2797,7 @@ def limited_result( prepared_analyses=prepared_analyses, parser_diagnostics=parser_diagnostics, python_import_bounds=python_import_bounds, - parser_file_type=parser_file_type, + parser_file_type="json" if _oms_projected else None, ) bounded_parse_limited = bounded_parse_limited or marker_bounded_parse_limited except _StaticResourceLimitError as exc: From caf306b10eb6500cb6644aae7908d8bbf105e08d Mon Sep 17 00:00:00 2001 From: yashrajbasav Date: Thu, 5 Nov 2026 22:05:27 +0530 Subject: [PATCH 4/4] fix: preserve optional grammar context during marker composition Signed-off-by: yashrajbasav --- src/skillspector/nodes/analyzers/static_runner.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/skillspector/nodes/analyzers/static_runner.py b/src/skillspector/nodes/analyzers/static_runner.py index cf3e1ae26..74fa8de1a 100644 --- a/src/skillspector/nodes/analyzers/static_runner.py +++ b/src/skillspector/nodes/analyzers/static_runner.py @@ -2237,7 +2237,7 @@ def check_runtime() -> None: ) from exc check_runtime() - parser_file_type = parser_file_type or _infer_file_type(path) + bounded_file_type = parser_file_type or _infer_file_type(path) projection_limited = False bounded_parse_limited = False seen_views: set[tuple[str, int, int]] = set() @@ -2276,7 +2276,7 @@ def defer_output_limit( # The proof covers the whole artifact, so it holds in every window. It is # computed only when a loose quoted directive header reaches a quote. python_strings = ( - PythonStringClosers(content, check_runtime) if parser_file_type == "python" else None + PythonStringClosers(content, check_runtime) if bounded_file_type == "python" else None ) for owned_start, raw_start in zip(owned_starts, raw_starts, strict=True): @@ -2435,7 +2435,7 @@ def defer_output_limit( module, marker_view, check_runtime, - file_type=parser_file_type, + file_type=bounded_file_type, complete_context=complete_context, source_start=raw_start, diagnostics=parser_diagnostics,