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,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)
Loading