Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
a6b1d91
fix(ea1): reject footnote-legend '*', bound pre-colon gap, detect blo…
yashrajp22 Aug 27, 2026
78ec14b
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Aug 31, 2026
d44a3f4
fix(ea1): keep the list scan out of nested arrays, not just objects
yashrajp22 Aug 31, 2026
0de6f6f
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 6, 2026
022ef9d
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 7, 2026
55381cc
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 9, 2026
7ed9256
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 9, 2026
375eafe
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 12, 2026
46aa053
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 12, 2026
8f54c9f
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 12, 2026
3f6f63a
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 12, 2026
f66907a
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 12, 2026
6ce0e12
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 12, 2026
cac4f8a
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 12, 2026
e92374e
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 14, 2026
4ec4ed5
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 14, 2026
3e0b58a
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 15, 2026
2d1eaa7
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 15, 2026
61560f9
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 16, 2026
ffa7a94
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 16, 2026
2e3f043
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 16, 2026
36d6083
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 16, 2026
f3a2c2a
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 16, 2026
0f4668c
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 16, 2026
b4399ab
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 16, 2026
3a0488b
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 16, 2026
eb3713d
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 16, 2026
19b6a2c
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 16, 2026
ddbbbfc
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 16, 2026
7f2839b
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 16, 2026
17e2e52
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 16, 2026
6124e9b
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 16, 2026
d761c1c
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 16, 2026
f44d4b4
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 16, 2026
a76e028
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 16, 2026
ec694a4
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 16, 2026
ef0d65f
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 16, 2026
cecc5e5
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 17, 2026
b0d4b34
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 17, 2026
8cd98dd
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 17, 2026
aeb4a66
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 17, 2026
9382189
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 18, 2026
af99a9f
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Sep 18, 2026
b610278
Merge remote-tracking branch 'origin/main' into yashrajp22/ea1-wildca…
yashrajp22 Oct 14, 2026
d2d1d94
fix(ea1): match wildcard grants as complete list elements
yashrajp22 Oct 14, 2026
5330b98
style(tests): format EA1 quoted-string cases
yashrajp22 Oct 14, 2026
c3c6d5f
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Oct 14, 2026
d998bbc
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Oct 14, 2026
f95461e
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Oct 14, 2026
f74aeca
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Oct 14, 2026
610610b
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Oct 15, 2026
d4c751d
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Oct 15, 2026
5b5ea7b
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Oct 16, 2026
d60d84f
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Oct 16, 2026
a915aae
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Oct 16, 2026
a351aff
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Oct 16, 2026
b478491
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Oct 16, 2026
2a4e8e1
Merge branch 'main' into yashrajp22/ea1-wildcard-remaining-gaps
github-actions[bot] Oct 16, 2026
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,9 +44,28 @@
ANALYZER_ID = "static_patterns_excessive_agency"

# EA1: Unrestricted Tool Access
# Configuration syntax can span blank lines; prose instructions cannot.
EA1_CODE_PATTERNS = [
(r"(?:tools?|permissions?)\s*:[ \t]*\[?[ \t]*['\"]?\*(?!\*|\w)['\"]?[ \t]*\]?", 0.85),
# Consume complete scalar elements so quoted tool arguments cannot look
# like wildcard grants. Possessive token bodies avoid repartitioning long
# near-misses; unquoted nested arrays/objects remain outside this grammar.
(
r"['\"]?(?:tools?|permissions?)['\"]?[ \t]*:[ \t]*"
r"(?:\[(?:[ \t]*+(?:\"(?:[^\"\\\r\n]++|\\[^\r\n])*+\""
r"|'(?:[^'\r\n]++|'')*+'|[^\][{},'\"\r\n]++)[ \t]*+,)*"
r"[ \t]*(?:\"\*\"|'\*')(?=[ \t]*[,\]])"
r"|(?:\"\*\"|'\*')(?=[ \t]*(?:[,}#]|\r?$))"
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"tools?\s*:\s*\[\s*['\"]shell['\"].*?['\"](?:file_write|network|http)['\"]", 0.7),
]
EA1_PROSE_PATTERNS = [
Expand Down
208 changes: 208 additions & 0 deletions tests/nodes/analyzers/test_ea1_wildcard_line_boundary.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@

from __future__ import annotations

import pytest

from skillspector.nodes.analyzers import (
static_patterns_excessive_agency as ea_module,
)
Expand Down Expand Up @@ -114,3 +116,209 @@ 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)


@pytest.mark.parametrize(
"value",
[
"[\"grep --include='*'\"]",
"['grep --include=\"*\"']",
'["search", "grep --include=\'*\'"]',
r'["grep --include=\"*\""]',
"['grep --include=''*''']",
'["*" + "suffix"]',
'["search" "*"]',
'["search", "*name"]',
'["search", "name*"]',
'["search", ["grep", "*"]]',
'[{"name": "search", "arguments": {"glob": "*"}}]',
'["search", "*\']',
'["search", \'*" ]',
'["search", "unterminated \'*\']',
],
)
def test_only_complete_top_level_wildcard_elements_are_flagged(value: str) -> None:
findings = ea_module.analyze(f"tools: {value}\n", "SKILL.md", "markdown")
assert not any(f.rule_id == "EA1" for f in findings)


@pytest.mark.parametrize(
"value",
[
'["*"]',
'["*", "search"]',
'["search", "*"]',
'[search, "*"]',
'["search {x}", "*"]',
'["search [x]", "*"]',
'["grep --include=\'*\'", "*"]',
r'["grep --include=\"*\"", "*"]',
"['grep --include=''*''', '*']",
'["search", "*", "grep"]',
],
)
def test_complete_wildcard_elements_after_quoted_scalars_are_flagged(value: str) -> None:
findings = ea_module.analyze(f"tools: {value}\n", "SKILL.md", "markdown")
assert any(f.rule_id == "EA1" for f in findings)