From 4c140bd5bcc58058a0b98b076890ed97166274b3 Mon Sep 17 00:00:00 2001 From: Narendran Raghavan Date: Tue, 11 Aug 2026 21:47:26 -0700 Subject: [PATCH 1/2] fix(yara): scope locality guard to agent namespace Signed-off-by: Narendran Raghavan --- src/skillspector/nodes/analyzers/static_yara.py | 4 +++- tests/nodes/analyzers/test_static_yara.py | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/skillspector/nodes/analyzers/static_yara.py b/src/skillspector/nodes/analyzers/static_yara.py index 361941c88..81056797b 100644 --- a/src/skillspector/nodes/analyzers/static_yara.py +++ b/src/skillspector/nodes/analyzers/static_yara.py @@ -62,6 +62,7 @@ _DEFAULT_RULE_ID = "YR4" _DEFAULT_SEVERITY = Severity.MEDIUM _DEFAULT_CONFIDENCE = 0.7 +_DESTRUCTIVE_AUTONOMY_NAMESPACE = "agent_skills" _DESTRUCTIVE_AUTONOMY_RULE = "agent_skill_destructive_autonomous_actions" _MAX_DESTRUCTIVE_AUTONOMY_LINE_DISTANCE = 3 @@ -278,7 +279,8 @@ def _match_file(rules: yara.Rules, content: str, file_path: str) -> list[Analyze findings: list[AnalyzerFinding] = [] for match in matches: if ( - match.rule == _DESTRUCTIVE_AUTONOMY_RULE + match.namespace == _DESTRUCTIVE_AUTONOMY_NAMESPACE + and match.rule == _DESTRUCTIVE_AUTONOMY_RULE and not _has_local_destructive_autonomy_evidence(match, data) ): logger.debug( diff --git a/tests/nodes/analyzers/test_static_yara.py b/tests/nodes/analyzers/test_static_yara.py index ec6fc168a..671832f52 100644 --- a/tests/nodes/analyzers/test_static_yara.py +++ b/tests/nodes/analyzers/test_static_yara.py @@ -491,6 +491,19 @@ def test_destructive_root_delete_remains_blocking_without_autonomy_phrase(self): findings = _run_builtin("rm -rf /\n", "setup.sh") assert _has_rule(findings, "agent_skill_destructive_autonomous_actions") + def test_user_rule_with_destructive_rule_name_is_not_post_filtered(self, tmp_path): + _write_rule( + tmp_path, + "agent_skill_destructive_autonomous_actions", + category="hack_tool", + severity="MEDIUM", + strings={"custom": "CUSTOM_DESTRUCTIVE_MARKER"}, + ) + + findings = _run("CUSTOM_DESTRUCTIVE_MARKER", "custom.txt", str(tmp_path)) + + assert _has_rule(findings, "agent_skill_destructive_autonomous_actions") + def test_credential_webhook_requires_collection_and_transmission(self): content = """ # Document how to rotate OPENAI_API_KEY. From c3c437daf61a665728bf88f42a1a55d886ed8c9b Mon Sep 17 00:00:00 2001 From: Narendran Raghavan Date: Tue, 11 Aug 2026 23:04:16 -0700 Subject: [PATCH 2/2] Preserve built-in YARA namespace precedence Signed-off-by: Narendran Raghavan --- .../nodes/analyzers/static_yara.py | 14 +++++-- tests/nodes/analyzers/test_static_yara.py | 39 +++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/skillspector/nodes/analyzers/static_yara.py b/src/skillspector/nodes/analyzers/static_yara.py index 81056797b..23cf8e485 100644 --- a/src/skillspector/nodes/analyzers/static_yara.py +++ b/src/skillspector/nodes/analyzers/static_yara.py @@ -72,14 +72,20 @@ def _collect_rule_files(*dirs: Path) -> list[Path]: - """Collect all YARA rule files under one or more directories, sorted for determinism.""" - files: set[Path] = set() + """Collect YARA files deterministically while preserving directory precedence.""" + files: list[Path] = [] + seen: set[Path] = set() for d in dirs: if not d.is_dir(): continue + directory_files: set[Path] = set() for ext in _RULE_EXTENSIONS: - files.update(d.rglob(ext)) - return sorted(files) + directory_files.update(d.rglob(ext)) + for rule_file in sorted(directory_files): + if rule_file not in seen: + seen.add(rule_file) + files.append(rule_file) + return files def _content_hash(rule_files: list[Path]) -> str: diff --git a/tests/nodes/analyzers/test_static_yara.py b/tests/nodes/analyzers/test_static_yara.py index 671832f52..d4b12ab9c 100644 --- a/tests/nodes/analyzers/test_static_yara.py +++ b/tests/nodes/analyzers/test_static_yara.py @@ -504,6 +504,45 @@ def test_user_rule_with_destructive_rule_name_is_not_post_filtered(self, tmp_pat assert _has_rule(findings, "agent_skill_destructive_autonomous_actions") + def test_user_agent_skills_file_cannot_claim_builtin_namespace(self, tmp_path, monkeypatch): + builtin_dir = tmp_path / "z_builtin" + user_dir = tmp_path / "a_user" + builtin_dir.mkdir() + user_dir.mkdir() + (builtin_dir / "agent_skills.yar").write_text( + """ +rule agent_skill_destructive_autonomous_actions { + strings: + $destructive_action = "DELETE_MARKER" + $autonomy_action = "AUTONOMY_MARKER" + condition: + all of them +} +""" + ) + (user_dir / "agent_skills.yar").write_text( + """ +rule agent_skill_destructive_autonomous_actions { + strings: + $custom = "CUSTOM_DESTRUCTIVE_MARKER" + condition: + $custom +} +""" + ) + monkeypatch.setattr(static_yara, "_BUILTIN_RULES_DIR", builtin_dir) + + intervening_lines = "\n".join(f"review step {index}" for index in range(6)) + content = ( + f"DELETE_MARKER\n{intervening_lines}\nAUTONOMY_MARKER\nCUSTOM_DESTRUCTIVE_MARKER\n" + ) + + findings = _run(content, "custom.txt", str(user_dir)) + + assert len(findings) == 1 + assert _has_rule(findings, "agent_skill_destructive_autonomous_actions") + assert "[a_user/agent_skills]" in findings[0].message + def test_credential_webhook_requires_collection_and_transmission(self): content = """ # Document how to rotate OPENAI_API_KEY.