fix(ea1): reject footnote-legend '*', bound pre-colon gap, detect YAML block-list and JSON wildcard grants - #447
Conversation
…ck-list and JSON wildcard grants Fixes #444. Fixes #445. Follow-up to #405/#417. Remaining false positives (#444): - A bare '*' now counts only when it ends the line (optionally ']' and/or a '#' comment), so footnote legends like 'Tools: * = requires auth' no longer fire. Quoted '*' stays unambiguous anywhere on the line. - The gap before the colon is bounded to the same line ([ \t]*:), so a blank line followed by a markdown definition-list ': *' can no longer bridge paragraphs. Detection gaps (#445): - The key may be quoted, catching JSON forms: "tools": ["*"] and "permissions": "*". - New block-list branch catches the idiomatic YAML form (tools: newline '- "*"'), bounded to a single newline with a standalone-star item so markdown lists of bold/italic names cannot collide. - A quoted '*' anywhere in a same-line bracket list now matches (tools: ["search", "*"]). Validated against a 46-case matrix (20 genuine grant forms, 11 new detections, 15 false-positive classes); full suite 2963 passed, ruff check/format clean.
rng1995
left a comment
There was a problem hiding this comment.
[SkillSpector Review]
Requesting changes: the new same-line list branch introduces an EA1 false positive when an explicit tool object has a nested argument value of "*"; the wildcard must be a top-level tool-list element. The focused suite passes, but this head has no hosted checks, and the feature commit also lacks the required DCO sign-off. Please add the nested-object regression, narrow the match, and rerun signed CI.
| # stays on one line so the match can never bridge paragraphs (#405, #444). | ||
| ( | ||
| r"['\"]?(?:tools?|permissions?)['\"]?[ \t]*:[ \t]*" | ||
| r"(?:\[[^\]\r\n]*['\"]\*['\"]" |
There was a problem hiding this comment.
[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.
Summary
Follow-up to #405 / #417 (and duplicate #438). #417 correctly bounded the EA1 wildcard-grant regex to a single line and a standalone
*, but an execution-verified edge-case audit of the merged pattern found two remaining false-positive paths and three detection gaps. This PR fixes all five.Fixes #444. Fixes #445. (Systemic paragraph-crossing across all 15 pattern files is tracked separately in #446 — out of scope here.)
Cases and how each is solved
False positives removed (#444)
Case 1 — footnote/legend text after a
Tools:label.The
(?!\*|\w)lookahead rejects**and*word, but a standalone*followed by a space passed — both lines fired EA1/MEDIUM on benign docs.Solution: a bare (unquoted)
*now only counts when it ends the line, optionally closed by]and/or a#comment. A real bare-scalar grant (tools: *,tools: [*],tools: * # allow all) has nothing else after the value; a footnote legend always does. Quoted forms ("*",'*') are unambiguous and keep matching anywhere on the line.Case 2 — blank-line gap before the colon.
#417 bounded the whitespace after the colon, but
\s*:before it still crossed newlines, so a markdown definition-list line two paragraphs later still bridged (matched texttools\n\n: *).Solution:
[ \t]*:— the key and colon must share a line. No real YAML/JSON/TOML syntax breaks a line between key and colon.Detection gaps closed (#445)
Case 3 — JSON quoted keys never matched.
The old prefix required the colon directly after the key word, so the closing quote of a JSON key broke the match — despite JSON being the most common encoding for MCP/agent configs.
Solution: the key may be wrapped in optional quotes:
['\"]?(?:tools?|permissions?)['\"]?.Case 4 — the idiomatic YAML block-list form never matched.
The block-sequence dash was never part of the pattern.
Solution: a second pattern matches a key followed by exactly one newline and a first list item that is a standalone wildcard. A blank line still breaks the match (the #405 cross-heading bridge cannot return), and the standalone-
*lookahead keeps markdown lists (- **Read**,- *note*) out. Later items are intentionally out of scope until seen in practice — matching arbitrary positions across lines widens the false-positive surface.Case 5 — wildcard not in first position of an inline list.
The old pattern required
*immediately after[.Solution: a quoted
*anywhere inside same-line brackets now matches (\[[^\]\r\n]*['\"]\*['\"]). Quoted-only on purpose: markdown link/bold text inside[...]cannot satisfy it, and an unquoted*mid-list in YAML is an alias, not a wildcard.Validation
analyze()flags (re.IGNORECASE | re.MULTILINE): 20 genuine grant forms still detected (quoted/bracketed/bare/no-space/tab/CRLF/uppercase/comments/allowed_toolssubstring), 11 new detections, 15 false-positive classes rejected (including all EA1 wildcard-tool-access pattern crosses blank lines and matches markdown bold syntax Affected rule: EA1 "Unrestricted Tool Access" Affected file: src/skillspector/nodes/analyzers/static_patterns_excessive_agency.py, line 45 #405/fix(ea1): bound wildcard-tool-access match to a single line and a sta… #417 regression cases).pytest tests/ -m "not integration and not provider"— 2,963 passed, 14 skipped, 4 xfailed, 0 failures.tests/nodes/analyzers/test_ea1_wildcard_line_boundary.py(footnote legend ×2, pre-colon gap, bare+comment, YAML block quoted/bare/zero-indent, JSON list/scalar key, inline not-first, markdown bold dash-list negative, blank-line-before-dash negative).ruff checkandruff format --checkclean.Risk
tools: [\n "*"\n]) and wildcards in later block-list items.