diff --git a/src/skillspector/nodes/analyzers/static_patterns_excessive_agency.py b/src/skillspector/nodes/analyzers/static_patterns_excessive_agency.py index 04ba47f7..87d56f12 100644 --- a/src/skillspector/nodes/analyzers/static_patterns_excessive_agency.py +++ b/src/skillspector/nodes/analyzers/static_patterns_excessive_agency.py @@ -42,7 +42,11 @@ # EA1: Unrestricted Tool Access EA1_PATTERNS = [ - (r"(?:tools?|permissions?)\s*:\s*\[?\s*['\"]?\*['\"]?\s*\]?", 0.85), + # EA1 wildcard grant: keep the value on the same line as the colon (the + # previous \s* spanned blank lines and bridged unrelated headings) and + # require the '*' to be a standalone token so markdown bold (**) or a + # word-adjacent '*' (e.g. a YAML anchor) is not treated as a wildcard. + (r"(?:tools?|permissions?)\s*:[ \t]*\[?[ \t]*['\"]?\*(?!\*|\w)['\"]?[ \t]*\]?", 0.85), (r"(?:allow|grant|enable)\s+(?:access\s+to\s+)?(?:all|any|every)\s+tools?", 0.8), ( r"(?:no|without)\s+(?:tool|permission|access|capability)\s+(?:restrictions?|constraints?|limitations?)", diff --git a/tests/unit/test_patterns_new.py b/tests/unit/test_patterns_new.py index d96e5f58..fb7fed8d 100644 --- a/tests/unit/test_patterns_new.py +++ b/tests/unit/test_patterns_new.py @@ -88,6 +88,8 @@ class TestExcessiveAgency: "content", [ pytest.param('tools: ["*"]', id="wildcard_tools"), + pytest.param("tools: *", id="wildcard_tools_bare"), + pytest.param("permissions: '*'", id="wildcard_permissions"), pytest.param("Allow access to all tools without restriction.", id="grant_all_tools"), pytest.param("This skill can execute arbitrary commands.", id="arbitrary_commands"), pytest.param( @@ -104,6 +106,21 @@ def test_ea1_read_only_docs_not_flagged(self) -> None: ) assert not any(f.rule_id == "EA1" for f in findings) + @pytest.mark.parametrize( + "content", + [ + pytest.param( + "For each tool:\n\n**Input Schema:**", + id="no_cross_heading_bridge", + ), + pytest.param("**API Coverage vs. Workflow Tools:**", id="no_bold_markdown_collision"), + pytest.param("Tools: **Read**, **Write**", id="no_bolded_named_tool_list"), + ], + ) + def test_ea1_no_false_positive_on_markdown_bold_or_heading(self, content: str) -> None: + findings = ea_mod.analyze(content, "SKILL.md", "markdown") + assert not any(f.rule_id == "EA1" for f in findings) + @pytest.mark.parametrize( "content,filename,filetype", [