Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,34 @@

# EA1: Unrestricted Tool Access
EA1_PATTERNS = [
(r"(?:tools?|permissions?)\s*:[ \t]*\[?[ \t]*['\"]?\*(?!\*|\w)['\"]?[ \t]*\]?", 0.85),
# Same-line wildcard grant. The key may be quoted (JSON). A quoted '*' is
# unambiguous as a scalar or a top-level list element — the list branch
# excludes '{', '}', and '[' so it never descends into a nested object or
# array: in tools: [{name: grep, pattern: "*"}] or
# tools: ["search", ["grep", "*"]] the '*' is not a top-level tool-list
# element and is not a grant. (Ceiling: a quoted element containing one
# of those delimiters before a genuine top-level "*" also stops the scan;
# walking quoted strings needs a parser, not a pattern.) A bare '*'
# counts only when it ends the line (optionally closed by ']' and/or a
# '#' comment), so a footnote legend like "Tools: * = requires auth" is
# not a grant. The gap around the colon stays on one line so the match
# can never bridge paragraphs (#405, #444).
(
r"['\"]?(?:tools?|permissions?)['\"]?[ \t]*:[ \t]*"
r"(?:\[[^\][{}\r\n]*['\"]\*['\"]"
r"|\[?[ \t]*['\"]\*['\"]"
r"|\[?[ \t]*\*(?![\*\w])[ \t]*\]?[ \t]*(?:#[^\r\n]*)?\r?$)",
0.85,
),
# YAML block list whose first item is the wildcard (#445). Bounded to a
# single newline — a blank line still breaks the match — and the item's
# '*' must be standalone, so markdown lists ("- **Read**", "- *note*")
# do not collide. Later items are out of scope until seen in practice.
(
r"['\"]?(?:tools?|permissions?)['\"]?[ \t]*:[ \t]*(?:#[^\r\n]*)?\r?\n"
r"[ \t]*-[ \t]+['\"]?\*(?![\*\w])['\"]?[ \t]*(?:#[^\r\n]*)?\r?$",
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?)",
Expand Down
162 changes: 162 additions & 0 deletions tests/nodes/analyzers/test_ea1_wildcard_line_boundary.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,165 @@ def test_unquoted_wildcard_flagged(self) -> None:
"markdown",
)
assert any(f.rule_id == "EA1" for f in findings)


class TestEA1FootnoteAndPreColonNotFlagged:
"""Remaining false-positive paths from #444: a footnote legend after a
'Tools:' label, and a blank-line gap before the colon."""

def test_footnote_legend_after_tools_label_not_flagged(self) -> None:
findings = ea_module.analyze(
"Tools: * = requires authentication\n",
"SKILL.md",
"markdown",
)
assert not any(f.rule_id == "EA1" for f in findings)

def test_footnote_prose_after_tools_label_not_flagged(self) -> None:
findings = ea_module.analyze(
"Tools: * marks optional parameters\n",
"SKILL.md",
"markdown",
)
assert not any(f.rule_id == "EA1" for f in findings)

def test_blank_line_before_definition_list_colon_not_flagged(self) -> None:
"""The gap before the colon must stay on one line too — a markdown
definition list two paragraphs later is not a grant."""
findings = ea_module.analyze(
"several tools\n\n: * item\n",
"SKILL.md",
"markdown",
)
assert not any(f.rule_id == "EA1" for f in findings)

def test_bare_wildcard_with_trailing_comment_still_flagged(self) -> None:
"""A '#' comment after the bare wildcard is still a grant, not prose."""
findings = ea_module.analyze(
"tools: * # allow everything\n",
"SKILL.md",
"markdown",
)
assert any(f.rule_id == "EA1" for f in findings)


class TestEA1BlockListAndJsonFormsFlagged:
"""Detection gaps from #445: the idiomatic YAML block-list and JSON
quoted-key encodings of a wildcard grant."""

def test_yaml_block_list_quoted_wildcard_flagged(self) -> None:
findings = ea_module.analyze(
'tools:\n - "*"\n',
"SKILL.md",
"markdown",
)
assert any(f.rule_id == "EA1" for f in findings)

def test_yaml_block_list_bare_wildcard_flagged(self) -> None:
findings = ea_module.analyze(
"tools:\n - *\n",
"SKILL.md",
"markdown",
)
assert any(f.rule_id == "EA1" for f in findings)

def test_yaml_block_list_zero_indent_flagged(self) -> None:
findings = ea_module.analyze(
'permissions:\n- "*"\n',
"SKILL.md",
"markdown",
)
assert any(f.rule_id == "EA1" for f in findings)

def test_json_quoted_key_list_wildcard_flagged(self) -> None:
findings = ea_module.analyze(
'"tools": ["*"]\n',
"config.json",
"json",
)
assert any(f.rule_id == "EA1" for f in findings)

def test_json_quoted_key_scalar_wildcard_flagged(self) -> None:
findings = ea_module.analyze(
'"permissions": "*"\n',
"config.json",
"json",
)
assert any(f.rule_id == "EA1" for f in findings)

def test_inline_list_wildcard_not_first_flagged(self) -> None:
findings = ea_module.analyze(
'tools: ["search", "*"]\n',
"SKILL.md",
"markdown",
)
assert any(f.rule_id == "EA1" for f in findings)

def test_markdown_dash_list_of_bold_tools_not_flagged(self) -> None:
"""The block-list branch must not collide with a markdown list of
specific bolded tool names."""
findings = ea_module.analyze(
"Tools:\n- **Read**\n- **Write**\n",
"SKILL.md",
"markdown",
)
assert not any(f.rule_id == "EA1" for f in findings)

def test_blank_line_before_dash_item_not_flagged(self) -> None:
"""A blank line between the key and a dash item breaks the block-list
association — the cross-paragraph bridge from #405 must not return."""
findings = ea_module.analyze(
'tools:\n\n - "*"\n',
"SKILL.md",
"markdown",
)
assert not any(f.rule_id == "EA1" for f in findings)


class TestEA1NestedObjectArgumentNotFlagged:
"""A '*' nested inside an explicit tool object is an argument value for a
specifically named tool, not a wildcard grant — the wildcard must be a
top-level list element."""

def test_json_tool_object_with_wildcard_argument_not_flagged(self) -> None:
findings = ea_module.analyze(
'tools: [{"name": "grep", "pattern": "*"}]\n',
"config.json",
"json",
)
assert not any(f.rule_id == "EA1" for f in findings)

def test_yaml_flow_tool_object_with_wildcard_argument_not_flagged(self) -> None:
findings = ea_module.analyze(
'tools: [{name: search, glob: "*"}]\n',
"SKILL.md",
"markdown",
)
assert not any(f.rule_id == "EA1" for f in findings)

def test_top_level_wildcard_after_named_element_still_flagged(self) -> None:
"""Narrowing to top-level elements must not lose the plain mixed list."""
findings = ea_module.analyze(
'tools: ["search", "*"]\n',
"SKILL.md",
"markdown",
)
assert any(f.rule_id == "EA1" for f in findings)

def test_nested_array_wildcard_not_flagged(self) -> None:
"""A '*' inside a nested array is not a top-level tool-list element."""
findings = ea_module.analyze(
'tools: ["search", ["grep", "*"]]\n',
"SKILL.md",
"markdown",
)
assert not any(f.rule_id == "EA1" for f in findings)

def test_json_tool_object_with_nested_arguments_not_flagged(self) -> None:
"""A '*' buried in a named tool's argument object is not a grant."""
findings = ea_module.analyze(
'"tools": [{"name": "search", "arguments": {"glob": "*"}}]\n',
"config.json",
"json",
)
assert not any(f.rule_id == "EA1" for f in findings)