Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package skillspector for openSUSE:Factory checked in at 2026-09-10 15:37:32 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/skillspector (Old) and /work/SRC/openSUSE:Factory/.skillspector.new.1265 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "skillspector" Thu Sep 10 15:37:32 2026 rev:8 rq:1376832 version:2.11.2 Changes: -------- --- /work/SRC/openSUSE:Factory/skillspector/skillspector.changes 2026-09-08 17:07:35.487749726 +0200 +++ /work/SRC/openSUSE:Factory/.skillspector.new.1265/skillspector.changes 2026-09-10 15:37:35.846474732 +0200 @@ -1,0 +2,20 @@ +Thu Sep 10 05:43:00 UTC 2026 - Martin Pluskal <[email protected]> + +- Update to version 2.11.2: + * Deduplicate reference-coverage records so duplicate Markdown + references no longer abort with fatal unaccounted_work errors + * Account distinct reference targets sharing one source line + without conflicting completion records + * Stop false static-parser limits on simple runtime parameters, + inline skill invocations, PowerShell member access and long + quoted prose + * Keep runtime-selected printf/command/builtin/env paths marked + partially inspected instead of claiming exact reconstruction +- Deselect two more aarch64-speed tests, both green in upstream CI: + test_cross_window_separator_pair_across_public_surfaces (only the + is_complete projection fails at the 30 s per-artifact budget; every + detection assertion passes) and + test_pe3_repeated_nouns_have_bounded_qualifier_cost (1.28 s against + a 1.0 s wall-clock bound) + +------------------------------------------------------------------- Old: ---- skillspector-2.11.1.tar.gz New: ---- skillspector-2.11.2.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ skillspector.spec ++++++ --- /var/tmp/diff_new_pack.Mh6Jeo/_old 2026-09-10 15:37:36.653508357 +0200 +++ /var/tmp/diff_new_pack.Mh6Jeo/_new 2026-09-10 15:37:36.656508482 +0200 @@ -21,7 +21,7 @@ # %%{primary_python} so it stays correct as the primary interpreter moves. %define pythons %{primary_python} Name: skillspector -Version: 2.11.1 +Version: 2.11.2 Release: 0 Summary: Security scanner for AI agent skills License: Apache-2.0 @@ -164,11 +164,20 @@ # 2.1 MiB against its own 64 MiB ceiling); only "completed" fails. # * test_rd04_... 31.55 s and test_nine_case_... 31.70 s (~1.26 MB fixture) # -- the same truncation. -# Note test_cross_window_separator_pair_across_public_surfaces (~512 KB, 84 s) -# is NOT deselected: it asserts no completeness bound and passes here. -# test_dense_directory_discovery_... is no longer deselected either -- 2.11.0's -# O_PATH ancestor traversal took it from 19.73 s to 1.68 s against its own -# 5.0 s ceiling. +# Note test_dense_directory_discovery_... is no longer deselected either -- +# 2.11.0's O_PATH ancestor traversal took it from 19.73 s to 1.68 s against +# its own 5.0 s ceiling. +# +# 2.11.2 adds two more aarch64-speed failures of the same fail-closed family +# (upstream's own test-ci is green: 4013 passed, 0 failed on x86_64): +# * test_cross_window_separator_pair_across_public_surfaces is byte-identical +# to 2.11.1 and every detection assertion in it still passes here (P1/P9 +# found, severities/confidences/scores equal) -- only the is_complete +# projection fails, with the test taking ~35 s in this chroot at the +# same 30 s per-artifact budget. Truncation, not missed detection. +# * test_pe3_repeated_nouns_have_bounded_qualifier_cost asserts a 5000-noun +# scan finishes in under 1.0 s; aarch64 needs 1.28 s. Pure wall-clock +# bound with no detection content (its zero-PE3-finding assertion holds). # # Deselecting keeps the build gate meaningful without hiding a detection # failure: the same resource bounds are asserted deterministically by the @@ -176,7 +185,7 @@ # test_static_runtime_limit_is_reported_as_partial, # test_static_output_limit_is_reported_as_partial, and the build_context # deadline tests), and those do run. -%pytest --deselect tests/unit/test_mcp_server.py::test_mcp_stdio_initialize_registers_scan_skill --deselect tests/unit/test_input_handler_ssrf.py::TestGitCloneSSRF::test_github_url_allowed --deselect tests/unit/test_input_handler_ssrf.py::TestGitCloneSSRF::test_gitlab_url_allowed --deselect tests/unit/test_input_handler_ssrf.py::TestDownloadSSRF::test_raw_githubusercontent_allowed --deselect tests/unit/test_input_handler_ssrf.py::TestDownloadSSRF::test_download_does_not_follow_redirects --deselect tests/unit/test_input_handler.py::test_validate_url_host_scp_extracts_github --deselect tests/unit/test_input_handler.py::test_scp_valid_host_clones --deselect tests/unit/test_input_handler.py::test_https_url_unchanged --deselect "tests/nodes/analyzers/test_bundled_execution_surface.py::test_discovery_parser_bounds_and_ledger_table[malformed_schema]" --deselect tests/test_bundled_execution_surface_acceptance.py::test_public_cli_exit_contract --deselect tests/test_bundled_execution_surface_ acceptance.py::test_recursive_single_child_routes_execution_surfaces_from_child_root --deselect tests/test_bundled_execution_surface_acceptance.py::test_cli_fail_on_incomplete_is_opt_in --deselect tests/nodes/test_security_end_to_end.py::test_rd04_large_file_pair_detects_start_boundary_and_end --deselect tests/nodes/test_security_end_to_end.py::test_nine_case_contract_across_public_surfaces --deselect tests/nodes/test_security_remediation.py::test_five_megabyte_normalized_static_scan_stays_below_memory_ceiling +%pytest --deselect tests/unit/test_mcp_server.py::test_mcp_stdio_initialize_registers_scan_skill --deselect tests/unit/test_input_handler_ssrf.py::TestGitCloneSSRF::test_github_url_allowed --deselect tests/unit/test_input_handler_ssrf.py::TestGitCloneSSRF::test_gitlab_url_allowed --deselect tests/unit/test_input_handler_ssrf.py::TestDownloadSSRF::test_raw_githubusercontent_allowed --deselect tests/unit/test_input_handler_ssrf.py::TestDownloadSSRF::test_download_does_not_follow_redirects --deselect tests/unit/test_input_handler.py::test_validate_url_host_scp_extracts_github --deselect tests/unit/test_input_handler.py::test_scp_valid_host_clones --deselect tests/unit/test_input_handler.py::test_https_url_unchanged --deselect "tests/nodes/analyzers/test_bundled_execution_surface.py::test_discovery_parser_bounds_and_ledger_table[malformed_schema]" --deselect tests/test_bundled_execution_surface_acceptance.py::test_public_cli_exit_contract --deselect tests/test_bundled_execution_surface_ acceptance.py::test_recursive_single_child_routes_execution_surfaces_from_child_root --deselect tests/test_bundled_execution_surface_acceptance.py::test_cli_fail_on_incomplete_is_opt_in --deselect tests/nodes/test_security_end_to_end.py::test_rd04_large_file_pair_detects_start_boundary_and_end --deselect tests/nodes/test_security_end_to_end.py::test_nine_case_contract_across_public_surfaces --deselect tests/nodes/test_security_remediation.py::test_five_megabyte_normalized_static_scan_stays_below_memory_ceiling --deselect tests/nodes/test_security_end_to_end.py::test_cross_window_separator_pair_across_public_surfaces --deselect tests/unit/test_patterns.py::TestPrivilegeEscalation::test_pe3_repeated_nouns_have_bounded_qualifier_cost %files %license LICENSE ++++++ skillspector-2.11.1.tar.gz -> skillspector-2.11.2.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/skillspector-2.11.1/CHANGELOG.md new/skillspector-2.11.2/CHANGELOG.md --- old/skillspector-2.11.1/CHANGELOG.md 2020-02-02 01:00:00.000000000 +0100 +++ new/skillspector-2.11.2/CHANGELOG.md 2020-02-02 01:00:00.000000000 +0100 @@ -1,3 +1,9 @@ +### 2.11.2 (Thursday, September 10, 2026) +### Features/Bug Fixes +* fix: prevent duplicate reference coverage from causing fatal scan-accounting errors (#507) +* fix: avoid false shell-parser limits on ordinary documentation (#507) +* fix: preserve partial coverage for runtime-selected printf and wrapper paths (#508) +--- ### 2.11.1 (Monday, September 07, 2026) ### Features/Bug Fixes * fix: parse space-separated allowed-tools strings (fixes #327) (#330) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/skillspector-2.11.1/PKG-INFO new/skillspector-2.11.2/PKG-INFO --- old/skillspector-2.11.1/PKG-INFO 2020-02-02 01:00:00.000000000 +0100 +++ new/skillspector-2.11.2/PKG-INFO 2020-02-02 01:00:00.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: skillspector -Version: 2.11.1 +Version: 2.11.2 Summary: SkillSpector: Security scanner for AI agent skills (Claude Code, Cursor, and similar). Scans skills for vulnerabilities, malicious patterns, and security risks before installation. Supports Git repos, URLs, zips, and local directories; runs static pattern checks and optional LLM semantic analysis; outputs terminal, JSON, and Markdown reports with risk scoring. Project-URL: Homepage, https://github.com/NVIDIA/skillspector Project-URL: Documentation, https://github.com/NVIDIA/skillspector#readme diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/skillspector-2.11.1/docs/release/skillspector-2.11.2.md new/skillspector-2.11.2/docs/release/skillspector-2.11.2.md --- old/skillspector-2.11.1/docs/release/skillspector-2.11.2.md 1970-01-01 01:00:00.000000000 +0100 +++ new/skillspector-2.11.2/docs/release/skillspector-2.11.2.md 2020-02-02 01:00:00.000000000 +0100 @@ -0,0 +1,63 @@ +# SkillSpector v2.11.2 + +Released: 2026-09-10 + +## Summary + +SkillSpector 2.11.2 fixes fatal reference-accounting errors and several false static-parser limits triggered by ordinary documentation. This patch also preserves incomplete-analysis reporting when a runtime-selected executable prevents exact command reconstruction. + +## Highlights + +- Complete reference accounting when Markdown labels and destinations identify the same artifact, or when several referenced artifacts appear on one source line. +- Avoid false parser limits for simple runtime parameters, inline skill invocations, PowerShell member access, and long quoted prose. +- Keep runtime-selected `printf` and wrapper paths marked as partially inspected. + +## Added + +- None. + +## Changed + +- Record reference-coverage completion once per source line. + +## Fixed + +- Deduplicate reference-coverage records for the same source file, line, and target, preventing fatal `unaccounted_work` errors from duplicate Markdown references. +- Account for distinct reference targets on the same source line without creating conflicting completion records. +- Distinguish simple runtime parameters from command substitutions and complex parameter expansions in bounded shell reconstruction, including inline `$ARGUMENTS` documentation ([#464](https://github.com/NVIDIA/SkillSpector/issues/464)). +- Count unquoted characters separately from already-consumed quoted spans so long quoted prose does not cause a false command-word span limit. +- Preserve partial coverage when runtime parameters select a `printf`, `command`, `builtin`, or `env` executable path; a recognized basename alone cannot establish which executable will run. + +## Security + +- Fixed security findings. + +## Breaking Changes and Migration + +- None. No new configuration is required. + +## Deprecations + +- None. + +## Validation + +Validated locally with Python 3.12 and uv 0.10.10: + +- `uv lock --check` — passed; third-party dependency versions are unchanged. +- `uv run --no-sync make test-ci` — 4,013 passed, 14 skipped, 38 deselected, and 4 expected failures. +- `uv run --no-sync make lint` and `uv run --no-sync make format-check` — passed. +- Built wheel and source distributions; `twine check` passed for both artifacts. +- `skillspector --version` — reported `SkillSpector v2.11.2`. +- The GitHub release helper dry run resolved `v2.11.2` and the matching versioned release notes. +- Docker image build and repository smoke tests passed on Linux/arm64, including the local safe fixture and public GitHub repository scans. +- `git diff --check` — passed. + +## Known Limitations + +- Full LLM analysis and downstream CI behavior require validation in the deployment that uses the release. + +## References + +- [GitHub PR #507](https://github.com/NVIDIA/SkillSpector/pull/507) +- [GitHub PR #508](https://github.com/NVIDIA/SkillSpector/pull/508) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/skillspector-2.11.1/pyproject.toml new/skillspector-2.11.2/pyproject.toml --- old/skillspector-2.11.1/pyproject.toml 2020-02-02 01:00:00.000000000 +0100 +++ new/skillspector-2.11.2/pyproject.toml 2020-02-02 01:00:00.000000000 +0100 @@ -4,7 +4,7 @@ [project] name = "skillspector" -version = "2.11.1" +version = "2.11.2" description = "SkillSpector: Security scanner for AI agent skills (Claude Code, Cursor, and similar). Scans skills for vulnerabilities, malicious patterns, and security risks before installation. Supports Git repos, URLs, zips, and local directories; runs static pattern checks and optional LLM semantic analysis; outputs terminal, JSON, and Markdown reports with risk scoring." readme = "README.md" license = "Apache-2.0" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/skillspector-2.11.1/src/skillspector/nodes/analyzers/static_patterns_tool_misuse.py new/skillspector-2.11.2/src/skillspector/nodes/analyzers/static_patterns_tool_misuse.py --- old/skillspector-2.11.1/src/skillspector/nodes/analyzers/static_patterns_tool_misuse.py 2020-02-02 01:00:00.000000000 +0100 +++ new/skillspector-2.11.2/src/skillspector/nodes/analyzers/static_patterns_tool_misuse.py 2020-02-02 01:00:00.000000000 +0100 @@ -53,6 +53,8 @@ _DESTRUCTIVE_COMMAND_BASENAMES = frozenset({"rm", "del", "erase"}) _QUOTED_GLOB_SENTINEL = "\ue000" _DYNAMIC_SHELL_WORD_SENTINEL = "\ue001" +_RUNTIME_SHELL_PARAMETER_SENTINEL = "\ue002" +_SIMPLE_BRACED_PARAMETER_RE = re.compile(r"\$\{(?:[A-Za-z_][A-Za-z0-9_]*|[0-9]+|[@*#?$!-])\}") _ROOT_GLOB_DOCUMENTATION_LINE_RE = re.compile( r"[ \t]*(?:(?:[-*+]|#{1,6})[ \t]+)?" r"(?:(?:(?:documentation|note|example)[ \t]*:[ \t]*)" @@ -702,10 +704,18 @@ if word is None: return False, False if _DYNAMIC_SHELL_WORD_SENTINEL in word: - # A runtime expansion participates in the invocation or wrapper - # command word. Its executable basename is not deterministic. + # A command substitution or complex expansion participates in the + # invocation or wrapper word. Its basename is not deterministic. return True, False command = word.casefold().rsplit("/", 1)[-1] + if _RUNTIME_SHELL_PARAMETER_SENTINEL in word and command in { + "printf", + "command", + "builtin", + "env", + }: + # A known basename does not make a runtime-selected executable exact. + return True, False if command == "printf": return True, True if command == "command": @@ -789,6 +799,17 @@ return True, arguments +def _invocation_expansion_marker(content: str, start: int, end: int) -> str: + """Distinguish runtime-only parameters from possible command reconstruction.""" + if content.startswith("$(", start) or ( + content.startswith("${", start) + and _SIMPLE_BRACED_PARAMETER_RE.fullmatch(content, start, end) is None + ): + # Complex parameter expansions may contain nested command substitutions. + return _DYNAMIC_SHELL_WORD_SENTINEL + return _RUNTIME_SHELL_PARAMETER_SENTINEL + + def _next_shell_invocation_word( content: str, start: int, @@ -820,6 +841,7 @@ quote: str | None = None ansi_c_quote = False word_started = False + unquoted_characters = 0 while cursor < limit: if cursor % 4096 == 0: check_runtime() @@ -884,7 +906,7 @@ word_started = True cursor += 1 continue - output.append(_DYNAMIC_SHELL_WORD_SENTINEL) + output.append(_invocation_expansion_marker(content, cursor, parameter_end)) word_started = True cursor = parameter_end if inherited_quote_closed[0]: @@ -959,7 +981,8 @@ word_started = True cursor += 1 continue - output.append(_DYNAMIC_SHELL_WORD_SENTINEL) + # A simple runtime parameter does not invoke the printf evaluator. + output.append(_invocation_expansion_marker(content, cursor, parameter_end)) word_started = True cursor = parameter_end continue @@ -1002,7 +1025,8 @@ else: output.append(character) word_started = True - if len(output) > _SHELL_COMMAND_WORD_CHARS: + unquoted_characters += 1 + if unquoted_characters > _SHELL_COMMAND_WORD_CHARS: return None, cursor, True cursor += 1 if quote is not None: @@ -1151,6 +1175,7 @@ ansi_c_quote = False dynamic = False limited = False + unquoted_characters = 0 cursor = start limit = len(content) while cursor < limit: @@ -1354,7 +1379,11 @@ break else: output.append(character) - if len(output) > _SHELL_COMMAND_WORD_CHARS: + # Quoted spans have already been consumed in full. Their decoded + # length must not exhaust the budget for the following literal + # suffix, which can resolve the candidate as an ordinary word. + unquoted_characters += 1 + if unquoted_characters > _SHELL_COMMAND_WORD_CHARS: return _ShellCommandWord("".join(output), cursor, dynamic, limited=True) cursor += 1 if quote is not None: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/skillspector-2.11.1/src/skillspector/nodes/finalize_inspection_ledger.py new/skillspector-2.11.2/src/skillspector/nodes/finalize_inspection_ledger.py --- old/skillspector-2.11.1/src/skillspector/nodes/finalize_inspection_ledger.py 2020-02-02 01:00:00.000000000 +0100 +++ new/skillspector-2.11.2/src/skillspector/nodes/finalize_inspection_ledger.py 2020-02-02 01:00:00.000000000 +0100 @@ -39,6 +39,7 @@ if outcome in {"partial", "failed", "out_of_scope"}: exceptional_outcomes.setdefault(str(event.get("path", "")), set()).add(outcome) findings: list[Finding] = [] + seen_locations: set[tuple[str, int, str]] = set() for reference in raw_references: if not isinstance(reference, dict): continue @@ -62,6 +63,14 @@ if final_disposition not in {"partial", "failed", "out_of_scope"}: continue line_value = reference.get("line", 1) + source_path = str(reference.get("source_path", "SKILL.md")) + line = line_value if isinstance(line_value, int) else 1 + # A Markdown label and destination may resolve to the same artifact. + # Findings identify source lines, so emit that coverage gap only once. + location = (source_path, line, target_path) + if location in seen_locations: + continue + seen_locations.add(location) evidence = str(reference.get("evidence", ""))[:160] findings.append( Finding( @@ -69,8 +78,8 @@ message="Referenced artifact was not completely inspected", severity="HIGH", confidence=1.0, - file=str(reference.get("source_path", "SKILL.md")), - start_line=line_value if isinstance(line_value, int) else 1, + file=source_path, + start_line=line, category="analysis-evasion", tags=["coverage", "reference", f"target-disposition:{final_disposition}"], finding=f"{target_path} ({final_disposition})"[:200], @@ -88,17 +97,24 @@ def finalize_inspection_ledger(state: SkillspectorState) -> dict[str, object]: """Validate full internal facts and derive the public completeness projection.""" reference_findings = _reference_coverage_findings(state) + # Work IDs are scoped to analyzer, source file and line range. Distinct + # targets on one line must share a terminal row with all emitted findings. + reference_ids_by_line: dict[tuple[str, int | None], list[str]] = {} + for finding in reference_findings: + reference_ids_by_line.setdefault((finding.file, finding.start_line), []).append( + finding.finding_id + ) reference_events: list[InspectionLedgerEvent] = [ ledger_event( outcome=LedgerOutcome.COMPLETED, phase="reference", analyzer_id="reference_coverage", - path=finding.file, - start_line=finding.start_line, - end_line=finding.start_line, - emitted_finding_ids=[finding.finding_id], + path=path, + start_line=line, + end_line=line, + emitted_finding_ids=finding_ids, ) - for finding in reference_findings + for (path, line), finding_ids in reference_ids_by_line.items() ] merged_state = dict(state) all_findings = [*(state.get("findings") or []), *reference_findings] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/skillspector-2.11.1/tests/nodes/analyzers/test_security_reconstruction.py new/skillspector-2.11.2/tests/nodes/analyzers/test_security_reconstruction.py --- old/skillspector-2.11.1/tests/nodes/analyzers/test_security_reconstruction.py 2020-02-02 01:00:00.000000000 +0100 +++ new/skillspector-2.11.2/tests/nodes/analyzers/test_security_reconstruction.py 2020-02-02 01:00:00.000000000 +0100 @@ -1619,6 +1619,81 @@ @pytest.mark.parametrize( + "content", + [ + "Interpret `$ARGUMENTS` as the user's input.", + "Interpret `${ARGUMENTS}` as the user's input.", + "Use `$example:task FILE_PATH|--all` to invoke the skill.", + "Read the token from `$SERVICE_TOKEN` or a configured file.", + 'Test-Path "$($_.FullName)\\cli-path"', + r"Render `$$\int_0^1 x \, dx$$` as math.", + "The reader's " + "ordinary documentation\n" * 220 + " author's guide.\n", + '"""Reader documentation.\n' + "ordinary documentation\n" * 220 + '"""\nreturn None\n', + ], + ids=[ + "parameter", + "braced-parameter", + "skill-invocation", + "token", + "powershell", + "math", + "apostrophe", + "docstring", + ], +) +def test_documentation_is_not_a_bounded_shell_reconstruction(content: str) -> None: + result = static_runner.run_static_patterns_with_ledger( + {"components": ["SKILL.md"], "file_cache": {"SKILL.md": content}}, [tm_module] + ) + + assert result["inspection_ledger"][0]["outcome"] is LedgerOutcome.COMPLETED + assert not any(finding.rule_id == "TM1" for finding in result["findings"]) + + +def test_long_quoted_command_path_still_detects_destructive_basename() -> None: + content = 'r"' + "directory/" * 500 + '"rm -rf /' + result = static_runner.run_static_patterns_with_ledger( + {"components": ["SKILL.md"], "file_cache": {"SKILL.md": content}}, [tm_module] + ) + + assert any(finding.rule_id == "TM1" for finding in result["findings"]) + + [email protected]( + "content", + [ + "$(printf $FORMAT) -rf /", + "$(printf ${FORMAT}) -rf /", + "$($BIN/printf echo) -rf /", + "$(${BIN}/printf echo) -rf /", + '$("$BIN/printf" echo) -rf /', + "$($BIN/env printf echo) -rf /", + "$($BIN/command printf echo) -rf /", + "$($BIN/builtin printf echo) -rf /", + '`"${TOOL:-$(printf rm ' + " " * 300 + ')}"` -rf /', + ], + ids=[ + "runtime-format", + "braced-runtime-format", + "runtime-path", + "braced-runtime-path", + "quoted-runtime-path", + "runtime-env-path", + "runtime-command-path", + "runtime-builtin-path", + "nested-parameter-reconstruction", + ], +) +def test_runtime_printf_arguments_and_nested_reconstruction_stay_partial(content: str) -> None: + result = static_runner.run_static_patterns_with_ledger( + {"components": ["SKILL.md"], "file_cache": {"SKILL.md": content}}, [tm_module] + ) + + assert result["inspection_ledger"][0]["outcome"] is LedgerOutcome.PARTIAL + assert result["inspection_ledger"][0]["reason_code"] is LedgerReason.STATIC_PARSE_LIMIT + + [email protected]( "printf_command", ["printf", 'p"rintf"', "p'rintf'", '"pri"ntf', r"p\rintf", "env printf"], ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/skillspector-2.11.1/tests/nodes/test_finalize_inspection_ledger.py new/skillspector-2.11.2/tests/nodes/test_finalize_inspection_ledger.py --- old/skillspector-2.11.1/tests/nodes/test_finalize_inspection_ledger.py 2020-02-02 01:00:00.000000000 +0100 +++ new/skillspector-2.11.2/tests/nodes/test_finalize_inspection_ledger.py 2020-02-02 01:00:00.000000000 +0100 @@ -610,6 +610,76 @@ assert completeness["is_complete"] is False [email protected]("failed", [False, True]) [email protected]( + "locations", + [ + [("a.md", 4, 8), ("a.md", 4, 24)], + [("a.md", 4, 8), ("b.md", 4, 24)], + [("a.md", 4, 8), ("a.md", 7, 8)], + [("a.md", 4, 8), ("a.md", 4, 24), ("b.md", 4, 40), ("a.md", 7, 8)], + ], +) +def test_reference_findings_share_one_terminal_event_per_source_line( + locations: list[tuple[str, int, int]], failed: bool +) -> None: + paths = sorted({path for path, _, _ in locations}) + result = finalize_inspection_ledger( + { + "components": ["SKILL.md", *paths], + "findings": [], + "effective_finding_ids": [], + "artifact_inventory": [ + {"path": path, "disposition": "analyzed", "content_kind": "text"} for path in paths + ], + "artifact_references": [ + { + "source_path": "SKILL.md", + "line": line, + "column": column, + "evidence": f"Read [{path}]({path}).", + "target_path": path, + "status": "resolved", + "disposition": "analyzed", + } + for path, line, column in locations + ], + "inspection_ledger": [ + ledger_event( + outcome=LedgerOutcome.FAILED if failed else LedgerOutcome.PARTIAL, + record_type=LedgerRecordType.SYSTEM, + phase="static", + path=path, + reason=LedgerReason.READ_ERROR if failed else LedgerReason.STATIC_PARSE_LIMIT, + ) + for path in paths + ], + "analyzer_status_events": [], + } + ) + + findings = result["findings"] + expected_locations = {(line, path) for path, line, _ in locations} + assert len(findings) == len(expected_locations) + assert { + (finding.start_line, finding.matched_text) for finding in findings + } == expected_locations + events = result["inspection_ledger"] + assert len(events) == len({line for _, line, _ in locations}) + for event in events: + assert set(event["emitted_finding_ids"]) == { + finding.finding_id for finding in findings if finding.start_line == event["start_line"] + } + completeness = result["analysis_completeness"] + assert completeness["status"] == ("failed" if failed else "partial") + assert result["execution_successful"] is (not failed) + assert len(result["effective_finding_ids"]) == len(expected_locations) + assert not any( + row["reason_code"] in {"unaccounted_work", "finding_accounting_error"} + for row in completeness["ledger_exceptions"] + ) + + @pytest.mark.parametrize("use_llm", [False, True]) @pytest.mark.parametrize( ("disposition", "outcome", "reason", "expected_ae1"), diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/skillspector-2.11.1/tests/nodes/test_security_end_to_end.py new/skillspector-2.11.2/tests/nodes/test_security_end_to_end.py --- old/skillspector-2.11.1/tests/nodes/test_security_end_to_end.py 2020-02-02 01:00:00.000000000 +0100 +++ new/skillspector-2.11.2/tests/nodes/test_security_end_to_end.py 2020-02-02 01:00:00.000000000 +0100 @@ -1005,6 +1005,59 @@ await _assert_incomplete_across_public_surfaces(tmp_path, result) +def test_markdown_reference_to_parser_limited_target_keeps_cli_execution_successful( + tmp_path: Path, +) -> None: + _write_bundle( + tmp_path, + { + "SKILL.md": ( + "---\nname: reference-coverage\ndescription: Reference coverage regression\n---\n" + "Read [references/commands.md](references/commands.md).\n" + ), + "references/commands.md": "$(env env env env printf rm) -rf /\n", + }, + ) + + report = _scan_cli(tmp_path) + + assert report["execution_successful"] is True + completeness = report["analysis_completeness"] + assert completeness["status"] == "partial" + assert completeness["is_complete"] is False + assert any( + row["reason_code"] == "static_parse_limit" for row in completeness["ledger_exceptions"] + ) + assert not any(row["fatal"] for row in completeness["ledger_exceptions"]) + ae1 = [issue for issue in report["issues"] if issue["id"] == "AE1"] + assert len(ae1) == 1 + assert ae1[0]["location"]["file"] == "SKILL.md" + assert ae1[0]["location"]["start_line"] == 5 + + +def test_referenced_variable_documentation_does_not_create_coverage_gaps(tmp_path: Path) -> None: + _write_bundle( + tmp_path, + { + "SKILL.md": ( + "---\nname: reference-variables\ndescription: Variable documentation\n---\n" + "Read [references/usage.md](references/usage.md).\n" + ), + "references/usage.md": ( + "Interpret `$ARGUMENTS` as the requested input.\n" + 'In PowerShell, use `Test-Path "$($_.FullName)\\cli-path"`.\n' + ), + }, + ) + + report = _scan_cli(tmp_path) + + assert report["execution_successful"] is True + assert report["analysis_completeness"]["status"] == "complete" + assert report["analysis_completeness"]["ledger_exceptions"] == [] + assert not any(issue["id"] == "AE1" for issue in report["issues"]) + + @pytest.mark.asyncio async def test_changed_rule_family_negative_controls(tmp_path: Path) -> None: _write_bundle( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/skillspector-2.11.1/uv.lock new/skillspector-2.11.2/uv.lock --- old/skillspector-2.11.1/uv.lock 2020-02-02 01:00:00.000000000 +0100 +++ new/skillspector-2.11.2/uv.lock 2020-02-02 01:00:00.000000000 +0100 @@ -2687,7 +2687,7 @@ [[package]] name = "skillspector" -version = "2.11.1" +version = "2.11.2" source = { editable = "." } dependencies = [ { name = "boto3" },
