Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,28 @@

# 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 anywhere on the line (scalar, first list item, or later list
# item); 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]*['\"]\*['\"]"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Restrict the wildcard to a top-level tool-list element. The unrestricted [^\]\r\n]* scan treats any nested quoted star as the wildcard tool itself: valid JSON such as "tools": [{"name": "search", "arguments": {"glob": "*"}}] now emits EA1 even though the tool list explicitly contains only search. Match scalar list elements without descending into nested objects/arrays, and add this JSON case as a negative regression.

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
113 changes: 113 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,116 @@ 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)