From 75917493b0db6f60bfbd6ec395daca74aa53939b Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Sun, 23 Aug 2026 09:48:20 -0400 Subject: [PATCH 1/3] fix(analyzer): gate AE3 and AE4 on text content Signed-off-by: Rod Boev --- .../nodes/analyzers/artifact_integrity.py | 45 +++++++++-------- tests/nodes/test_security_remediation.py | 49 +++++++++++++++++++ 2 files changed, 74 insertions(+), 20 deletions(-) diff --git a/src/skillspector/nodes/analyzers/artifact_integrity.py b/src/skillspector/nodes/analyzers/artifact_integrity.py index 443167ab..bdc8ed87 100644 --- a/src/skillspector/nodes/analyzers/artifact_integrity.py +++ b/src/skillspector/nodes/analyzers/artifact_integrity.py @@ -9,6 +9,7 @@ import unicodedata from dataclasses import dataclass, field +from skillspector.artifacts import ContentKind from skillspector.inspection_ledger import ( InspectionLedgerEvent, LedgerOutcome, @@ -269,28 +270,32 @@ def node(state: SkillspectorState) -> AnalyzerNodeResponse: confidence=1.0, ) ) - format_density, mixed_script, first_nul_line = _text_signals(content, budget) - if artifact.get("contains_nul") and first_nul_line is not None: - budget.emit( - _finding( - "AE3", - "Text artifact contains embedded NUL bytes", - path, - severity="HIGH", - confidence=0.9, - line=first_nul_line, + if artifact.get("content_kind") not in { + ContentKind.BINARY, + ContentKind.OPAQUE, + }: + format_density, mixed_script, first_nul_line = _text_signals(content, budget) + if artifact.get("contains_nul") and first_nul_line is not None: + budget.emit( + _finding( + "AE3", + "Text artifact contains embedded NUL bytes", + path, + severity="HIGH", + confidence=0.9, + line=first_nul_line, + ) ) - ) - if format_density >= 0.01 or mixed_script: - budget.emit( - _finding( - "AE4", - "Suspicious Unicode normalization or mixed-script content", - path, - severity="MEDIUM", - confidence=0.8, + if format_density >= 0.01 or mixed_script: + budget.emit( + _finding( + "AE4", + "Suspicious Unicode normalization or mixed-script content", + path, + severity="MEDIUM", + confidence=0.8, + ) ) - ) except _ArtifactIntegrityResourceLimitError as exc: resource_limit = exc diff --git a/tests/nodes/test_security_remediation.py b/tests/nodes/test_security_remediation.py index 3bc350f8..717356b3 100644 --- a/tests/nodes/test_security_remediation.py +++ b/tests/nodes/test_security_remediation.py @@ -5,6 +5,7 @@ from __future__ import annotations +import base64 import io import time import tracemalloc @@ -65,6 +66,54 @@ def test_artifact_integrity_reports_misleading_extension() -> None: assert any(finding.rule_id == "AE2" for finding in response["findings"]) +def test_opaque_png_does_not_produce_decoded_text_findings(tmp_path: Path) -> None: + (tmp_path / "SKILL.md").write_text( + """--- +name: binary-repro +description: A skill that ships one small PNG as reference material. +--- + +# Binary repro + +Describe the diagram in assets/diagram.png to the user. +""", + encoding="utf-8", + ) + png = base64.b64decode( + "iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAHElEQVQoz2NgGAWjYBSMglEwCkbBKBgFo2AUAAAHkgABfXRPtQAAAABJRU5ErkJggg==" + ) + assets = tmp_path / "assets" + assets.mkdir() + (assets / "diagram.png").write_bytes(png) + + result = graph.invoke({"input_path": str(tmp_path), "output_format": "json", "use_llm": False}) + + artifact = next( + item for item in result["artifact_inventory"] if item["path"] == "assets/diagram.png" + ) + findings = [finding for finding in result["findings"] if finding.file == "assets/diagram.png"] + assert artifact["content_kind"] in {ContentKind.BINARY, ContentKind.OPAQUE} + assert not {finding.rule_id for finding in findings} & {"AE3", "AE4"} + assert any( + event.get("path") == "assets/diagram.png" and event.get("reason_code") == "opaque_content" + for event in result["inspection_ledger"] + ) + + +def test_text_artifact_remains_eligible_for_ae4() -> None: + response = artifact_integrity( + { + "components": ["notes"], + "local_file_cache": {"notes": "latin-а"}, + "artifact_inventory": [ + {"path": "notes", "content_kind": ContentKind.TEXT}, + ], + } + ) + + assert [finding.rule_id for finding in response["findings"]] == ["AE4"] + + def test_normalized_view_removes_ignorables_maps_offsets_and_confusables() -> None: source = "ig\u00adn\u03bfre" view = normalized_security_view(source) From 3fb0e6a766f3c227e14612d94ef97b178aea4a3d Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Sun, 23 Aug 2026 10:12:13 -0400 Subject: [PATCH 2/3] test(analyzer): cover opaque artifact preservation Signed-off-by: Rod Boev --- tests/nodes/test_security_remediation.py | 26 +++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/nodes/test_security_remediation.py b/tests/nodes/test_security_remediation.py index 717356b3..bcd368c8 100644 --- a/tests/nodes/test_security_remediation.py +++ b/tests/nodes/test_security_remediation.py @@ -95,7 +95,9 @@ def test_opaque_png_does_not_produce_decoded_text_findings(tmp_path: Path) -> No assert artifact["content_kind"] in {ContentKind.BINARY, ContentKind.OPAQUE} assert not {finding.rule_id for finding in findings} & {"AE3", "AE4"} assert any( - event.get("path") == "assets/diagram.png" and event.get("reason_code") == "opaque_content" + event.get("analyzer_id") == "artifact_integrity" + and event.get("path") == "assets/diagram.png" + and event.get("outcome") == "completed" for event in result["inspection_ledger"] ) @@ -114,6 +116,28 @@ def test_text_artifact_remains_eligible_for_ae4() -> None: assert [finding.rule_id for finding in response["findings"]] == ["AE4"] +def test_opaque_misleading_extension_keeps_ae2_without_ae3_or_ae4() -> None: + response = artifact_integrity( + { + "components": ["payload.md"], + "file_cache": {"payload.md": "\x00latin-а"}, + "artifact_inventory": [ + { + "path": "payload.md", + "content_kind": ContentKind.OPAQUE, + "misleading_extension": True, + "contains_nul": True, + } + ], + } + ) + + rule_ids = [finding.rule_id for finding in response["findings"]] + assert "AE2" in rule_ids + assert "AE3" not in rule_ids + assert "AE4" not in rule_ids + + def test_normalized_view_removes_ignorables_maps_offsets_and_confusables() -> None: source = "ig\u00adn\u03bfre" view = normalized_security_view(source) From 238221754828f149d50e23fcd99a850bc66d48b5 Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Sun, 23 Aug 2026 10:46:56 -0400 Subject: [PATCH 3/3] test(ae): preserve opaque ledger and reference evidence Signed-off-by: Rod Boev --- tests/nodes/test_security_remediation.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/nodes/test_security_remediation.py b/tests/nodes/test_security_remediation.py index bcd368c8..64ed77eb 100644 --- a/tests/nodes/test_security_remediation.py +++ b/tests/nodes/test_security_remediation.py @@ -80,7 +80,7 @@ def test_opaque_png_does_not_produce_decoded_text_findings(tmp_path: Path) -> No encoding="utf-8", ) png = base64.b64decode( - "iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAHElEQVQoz2NgGAWjYBSMglEwCkbBKBgFo2AUAAAHkgABfXRPtQAAAABJRU5ErkJggg==" + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" ) assets = tmp_path / "assets" assets.mkdir() @@ -94,12 +94,20 @@ def test_opaque_png_does_not_produce_decoded_text_findings(tmp_path: Path) -> No findings = [finding for finding in result["findings"] if finding.file == "assets/diagram.png"] assert artifact["content_kind"] in {ContentKind.BINARY, ContentKind.OPAQUE} assert not {finding.rule_id for finding in findings} & {"AE3", "AE4"} + assert any( + finding.rule_id == "AE1" and finding.file == "SKILL.md" for finding in result["findings"] + ) assert any( event.get("analyzer_id") == "artifact_integrity" and event.get("path") == "assets/diagram.png" and event.get("outcome") == "completed" for event in result["inspection_ledger"] ) + assert any( + event.get("path") == "assets/diagram.png" + and event.get("reason_code") == LedgerReason.OPAQUE_CONTENT + for event in result["inspection_ledger"] + ) def test_text_artifact_remains_eligible_for_ae4() -> None: