Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 13 additions & 5 deletions src/skillspector/nodes/analyzers/static_yara.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
_DEFAULT_RULE_ID = "YR4"
_DEFAULT_SEVERITY = Severity.MEDIUM
_DEFAULT_CONFIDENCE = 0.7
_DESTRUCTIVE_AUTONOMY_NAMESPACE = "agent_skills"
Comment thread
rng1995 marked this conversation as resolved.
_DESTRUCTIVE_AUTONOMY_RULE = "agent_skill_destructive_autonomous_actions"
_MAX_DESTRUCTIVE_AUTONOMY_LINE_DISTANCE = 3

Expand All @@ -71,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:
Expand Down Expand Up @@ -278,7 +285,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(
Expand Down
52 changes: 52 additions & 0 deletions tests/nodes/analyzers/test_static_yara.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,58 @@ 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_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.
Expand Down
Loading