#!/usr/bin/env python3
"""Pattern scan of a skill, plugin or MCP server directory before install.
Usage: python3 audit_extension.py
[--json]
Prints one finding per line as "severity rule path:line: message" and exits 1 if any finding is high.
Standard library only. It reads files; it never runs them. A clean result is not approval.
"""
import json
import re
import sys
from pathlib import Path
# Some patterns are assembled from pieces so this file does not match its own rules.
SH = r"(?:ba|z|da|k)?sh\b"
FETCH_ALT = r"(?:cu" + r"rl|wg" + r"et)"
FETCH = r"\b" + FETCH_ALT + r"\b"
AGENT_WORDS = r"\b(?:ign" + r"ore|disre" + r"gard|forget)\s+(?:all\s+|any\s+)?(?:previous|prior|above|earlier|system)\s+(?:instructions|prompts?|rules)\b"
KEEP_QUIET = r"\b(?:do\s+not|don't|never)\s+(?:tell|inform|show|mention\s+(?:this\s+)?to)\s+the\s+user\b|\bwithout\s+(?:asking|telling)\s+the\s+user\b"
RULES = [
("hidden-unicode", "high", re.compile("[\u200b-\u200f\u202a-\u202e\u2060-\u2064\u2066-\u2069\ufeff\U000e0000-\U000e007f]"),
"Invisible or bidirectional characters can hide text from reviewers."),
("pipe-to-shell", "high", re.compile(FETCH + r"[^\n|]*\|\s*(?:sudo\s+)?" + SH, re.I), "Pipes a download into a shell."),
("eval-remote", "high", re.compile(r"(?:\beval|\bsource|\b(?:ba)?sh\s+-c)\s*[\"']?\$\(\s*" + FETCH), "Runs downloaded text."),
("base64-exec", "high", re.compile(r"base64\s+(?:-d|--decode)\b[^\n]*\|\s*(?:sudo\s+)?" + SH + r"|b64decode\([^\n]*\)[^\n]*\b(?:exec|eval)\b|\b(?:exec|eval)\([^\n]*b64decode", re.I),
"Decodes and runs hidden code."),
("credential-access", "high", re.compile(r"(?:~|\$HOME|expanduser\([\"']~)[\\/]\." + r"(?:ssh|aws|gnupg|kube|docker|netrc)\b|\.aws/cred" + r"entials|\bid_(?:rsa|ed25519)\b|Login" + r" Data|\.config/g" + r"cloud", re.I),
"Touches credential files or browser profiles."),
("env-exfiltration", "high", re.compile(r"\b(?:env|printenv)\b\s*\|\s*(?:" + FETCH_ALT[3:-1] + r"|nc)\b|os\.environ[^\n]*(?:requests\.|urlopen|http)|process\.env[^\n]*fetch\(", re.I),
"Sends environment variables over the network."),
("agent-directed-wording", "high", re.compile(AGENT_WORDS + "|" + KEEP_QUIET, re.I), "Wording aimed at steering the agent or hiding actions from the user."),
("auto-approve", "medium", re.compile(r"--dangerously-skip-" + r"permissions|--yo" + r"lo\b|\"(?:autoApprove|alwaysAllow)\"\s*:\s*\[", re.I), "Turns off permission prompts."),
("unpinned-npx", "medium", re.compile(r"\bnpx\s+(?:-y\s+|--yes\s+)?(@?[\w./-]+?)(?:@latest)?(?=[\s\"',\]]|$)"), "Unpinned package (npx); use an exact version."),
("unpinned-uvx", "medium", re.compile(r"\buvx\s+(?!--from\b)([A-Za-z][\w.-]*)(?=[\s\"',\]]|$)"), "Unpinned package (uvx); use ==version."),
("plain-http-url", "medium", re.compile(r"\bhttp://(?!localhost|127\.0\.0\.1|\[::1\])[^\s\"'<>)]+"), "Plain HTTP URL; traffic can be altered in transit."),
("raw-ip-url", "medium", re.compile(r"\bhttps?://(?:\d{1,3}\.){3}\d{1,3}"), "Network call to a bare IP address."),
("encoded-blob", "medium", re.compile(r"[A-Za-z0-9+/]{200,}={0,2}"), "Long encoded string nobody can review."),
("html-comment", "medium", re.compile(r"