#!/usr/bin/env python3 """ Skill Security Auditor — Scan AI agent skills for security risks before installation. Usage: python3 skill_security_auditor.py /path/to/skill/ python3 skill_security_auditor.py https://github.com/user/repo --skill skill-name python3 skill_security_auditor.py /path/to/skill/ --strict --json Exit codes: 0 = PASS (safe to install) 1 = FAIL (critical findings, do not install) 2 = WARN (review manually before installing) """ import argparse import json import os import re import stat import subprocess import sys import tempfile import shutil from dataclasses import dataclass, field, asdict from enum import IntEnum from pathlib import Path from typing import Optional class Severity(IntEnum): INFO = 0 HIGH = 1 CRITICAL = 2 SEVERITY_LABELS = { Severity.INFO: "⚪ INFO", Severity.HIGH: "🟡 HIGH", Severity.CRITICAL: "🔴 CRITICAL", } SEVERITY_NAMES = { Severity.INFO: "INFO", Severity.HIGH: "HIGH", Severity.CRITICAL: "CRITICAL", } @dataclass class Finding: severity: Severity category: str file: str line: int pattern: str risk: str fix: str def to_dict(self): d = asdict(self) d["severity"] = SEVERITY_NAMES[self.severity] return d @dataclass class AuditReport: skill_name: str skill_path: str findings: list = field(default_factory=list) files_scanned: int = 0 scripts_scanned: int = 0 md_files_scanned: int = 0 @property def critical_count(self): return sum(1 for f in self.findings if f.severity == Severity.CRITICAL) @property def high_count(self): return sum(1 for f in self.findings if f.severity == Severity.HIGH) @property def info_count(self): return sum(1 for f in self.findings if f.severity == Severity.INFO) @property def verdict(self): if self.critical_count > 0: return "FAIL" if self.high_count > 0: return "WARN" return "PASS" def to_dict(self): return { "skill_name": self.skill_name, "skill_path": self.skill_path, "verdict": self.verdict, "summary": { "critical": self.critical_count, "high": self.high_count, "info": self.info_count, "total": len(self.findings), }, "stats": { "files_scanned": self.files_scanned, "scripts_scanned": self.scripts_scanned, "md_files_scanned": self.md_files_scanned, }, "findings": [f.to_dict() for f in self.findings], } # ============================================================================= # CODE EXECUTION PATTERNS # ============================================================================= CODE_PATTERNS = [ # Command injection — CRITICAL { "regex": r"\bos\.system\s*\(", "category": "CMD-INJECT", "severity": Severity.CRITICAL, "risk": "Arbitrary command execution via os.system()", "fix": "Use subprocess.run() with list arguments and shell=False", }, { "regex": r"\bos\.popen\s*\(", "category": "CMD-INJECT", "severity": Severity.CRITICAL, "risk": "Command execution via os.popen()", "fix": "Use subprocess.run() with list arguments and capture_output=True", }, { "regex": r"\bsubprocess\.\w+\([^)]*shell\s*=\s*True", "category": "CMD-INJECT", "severity": Severity.CRITICAL, "risk": "Shell injection via subprocess with shell=True", "fix": "Use subprocess.run() with list arguments and shell=False", }, { "regex": r"\bcommands\.get(?:status)?output\s*\(", "category": "CMD-INJECT", "severity": Severity.CRITICAL, "risk": "Deprecated command execution via commands module", "fix": "Use subprocess.run() with list arguments", }, # Code execution — CRITICAL { "regex": r"\beval\s*\(", "category": "CODE-EXEC", "severity": Severity.CRITICAL, "risk": "Arbitrary code execution via eval()", "fix": "Use ast.literal_eval() for data parsing or explicit parsing logic", }, { "regex": r"\bexec\s*\(", "category": "CODE-EXEC", "severity": Severity.CRITICAL, "risk": "Arbitrary code execution via exec()", "fix": "Remove exec() — rewrite logic to avoid dynamic code execution", }, { "regex": r"\bcompile\s*\([^)]*['\"]exec['\"]", "category": "CODE-EXEC", "severity": Severity.CRITICAL, "risk": "Dynamic code compilation for execution", "fix": "Remove compile() with exec mode — use explicit logic instead", }, { "regex": r"\b__import__\s*\(", "category": "CODE-EXEC", "severity": Severity.CRITICAL, "risk": "Dynamic module import — can load arbitrary code", "fix": "Use explicit import statements", }, { "regex": r"\bimportlib\.import_module\s*\(", "category": "CODE-EXEC", "severity": Severity.HIGH, "risk": "Dynamic module import via importlib", "fix": "Use explicit import statements unless dynamic loading is justified", }, # Obfuscation — CRITICAL { "regex": r"\bbase64\.b64decode\s*\(", "category": "OBFUSCATION", "severity": Severity.CRITICAL, "risk": "Base64 decoding — may hide malicious payloads", "fix": "Review decoded content. If not processing user data, remove base64 usage", }, { "regex": r"\bcodecs\.decode\s*\(", "category": "OBFUSCATION", "severity": Severity.CRITICAL, "risk": "Codec decoding — may hide obfuscated payloads", "fix": "Review decoded content and ensure it's not hiding executable code", }, { "regex": r"\\x[0-9a-fA-F]{2}(?:\\x[0-9a-fA-F]{2}){7,}", "category": "OBFUSCATION", "severity": Severity.CRITICAL, "risk": "Long hex-encoded string — likely obfuscated payload", "fix": "Decode and inspect the content. Replace with readable strings", }, { "regex": r"\bchr\s*\(\s*\d+\s*\)(?:\s*\+\s*chr\s*\(\s*\d+\s*\)){3,}", "category": "OBFUSCATION", "severity": Severity.CRITICAL, "risk": "Character-by-character string construction — obfuscation technique", "fix": "Replace chr() chains with readable string literals", }, { "regex": r"bytes\.fromhex\s*\(", "category": "OBFUSCATION", "severity": Severity.HIGH, "risk": "Hex byte decoding — may hide payloads", "fix": "Review the hex content and replace with readable code", }, # Network exfiltration — CRITICAL { "regex": r"\brequests\.(?:post|put|patch)\s*\(", "category": "NET-EXFIL", "severity": Severity.CRITICAL, "risk": "Outbound HTTP write request — potential data exfiltration", "fix": "Remove outbound POST/PUT/PATCH or verify destination is trusted and necessary", }, { "regex": r"\burllib\.request\.urlopen\s*\(", "category": "NET-EXFIL", "severity": Severity.HIGH, "risk": "Outbound HTTP request via urllib", "fix": "Verify the URL destination is trusted. Remove if not needed", }, { "regex": r"\burllib\.request\.Request\s*\(", "category": "NET-EXFIL", "severity": Severity.HIGH, "risk": "HTTP request construction via urllib", "fix": "Verify the request target and ensure no sensitive data is sent", }, { "regex": r"\bsocket\.(?:connect|create_connection)\s*\(", "category": "NET-EXFIL", "severity": Severity.CRITICAL, "risk": "Raw socket connection — potential C2 or exfiltration channel", "fix": "Remove raw socket usage unless absolutely required and justified", }, { "regex": r"\bhttpx\.(?:post|put|patch|AsyncClient)\s*\(", "category": "NET-EXFIL", "severity": Severity.CRITICAL, "risk": "Outbound HTTP request via httpx", "fix": "Remove or verify destination is trusted", }, { "regex": r"\baiohttp\.ClientSession\s*\(", "category": "NET-EXFIL", "severity": Severity.CRITICAL, "risk": "Async HTTP client — potential exfiltration", "fix": "Remove or verify all request destinations are trusted", }, { "regex": r"\brequests\.get\s*\(", "category": "NET-READ", "severity": Severity.HIGH, "risk": "Outbound HTTP GET request — may download malicious payloads", "fix": "Verify the URL is trusted and necessary for skill functionality", }, # Credential harvesting — CRITICAL { "regex": r"(?:open|read|Path)\s*\([^)]*(?:\.ssh|\.aws|\.config/secrets|\.gnupg|\.npmrc|\.pypirc)", "category": "CRED-HARVEST", "severity": Severity.CRITICAL, "risk": "Reads credential files (SSH keys, AWS creds, secrets)", "fix": "Remove all access to credential directories", }, { "regex": r"\bos\.environ\s*\[\s*['\"](?:AWS_|GITHUB_TOKEN|API_KEY|SECRET|PASSWORD|TOKEN|PRIVATE)", "category": "CRED-HARVEST", "severity": Severity.CRITICAL, "risk": "Extracts sensitive environment variables", "fix": "Remove credential access unless skill explicitly requires it and user is warned", }, { "regex": r"\bos\.environ\.get\s*\([^)]*(?:AWS_|GITHUB_TOKEN|API_KEY|SECRET|PASSWORD|TOKEN|PRIVATE)", "category": "CRED-HARVEST", "severity": Severity.CRITICAL, "risk": "Reads sensitive environment variables", "fix": "Remove credential access. Skills should not need external credentials", }, { "regex": r"(?:keyring|keychain)\.\w+\s*\(", "category": "CRED-HARVEST", "severity": Severity.CRITICAL, "risk": "Accesses system keyring/keychain", "fix": "Remove keyring access — skills should not access system credential stores", }, # File system abuse — HIGH { "regex": r"(?:open|write|Path)\s*\([^)]*(?:/etc/|/usr/|/var/|/tmp/\.\w)", "category": "FS-ABUSE", "severity": Severity.HIGH, "risk": "Writes to system directories outside skill scope", "fix": "Restrict file operations to the skill directory or user-specified output paths", }, { "regex": r"(?:open|write|Path)\s*\([^)]*(?:\.bashrc|\.bash_profile|\.profile|\.zshrc|\.zprofile)", "category": "FS-ABUSE", "severity": Severity.CRITICAL, "risk": "Modifies shell configuration — potential persistence mechanism", "fix": "Remove all writes to shell config files", }, { "regex": r"\bos\.symlink\s*\(", "category": "FS-ABUSE", "severity": Severity.HIGH, "risk": "Creates symbolic links — potential directory traversal attack", "fix": "Remove symlink creation unless explicitly required and bounded", }, { "regex": r"\bshutil\.rmtree\s*\(", "category": "FS-ABUSE", "severity": Severity.HIGH, "risk": "Recursive directory deletion — destructive operation", "fix": "Remove or restrict to specific, validated paths within skill scope", }, { "regex": r"\bos\.remove\s*\(|os\.unlink\s*\(", "category": "FS-ABUSE", "severity": Severity.HIGH, "risk": "File deletion — verify target is within skill scope", "fix": "Ensure deletion targets are validated and within expected paths", }, # Privilege escalation — CRITICAL { "regex": r"\bsudo\b", "category": "PRIV-ESC", "severity": Severity.CRITICAL, "risk": "Sudo invocation — privilege escalation attempt", "fix": "Remove sudo usage. Skills should never require elevated privileges", }, { "regex": r"\bchmod\b.*\b[0-7]*7[0-7]{2}\b", "category": "PRIV-ESC", "severity": Severity.HIGH, "risk": "Setting world-executable permissions", "fix": "Use restrictive permissions (e.g., 0o644 for files, 0o755 for dirs)", }, { "regex": r"\bos\.set(?:e)?uid\s*\(", "category": "PRIV-ESC", "severity": Severity.CRITICAL, "risk": "UID manipulation — privilege escalation", "fix": "Remove UID manipulation. Skills must run as the invoking user", }, { "regex": r"\bcrontab\b|\bcron\b.*\bwrite\b", "category": "PRIV-ESC", "severity": Severity.CRITICAL, "risk": "Cron job manipulation — persistence mechanism", "fix": "Remove cron manipulation. Skills should not modify scheduled tasks", }, # Unsafe deserialization — HIGH { "regex": r"\bpickle\.loads?\s*\(", "category": "DESERIAL", "severity": Severity.HIGH, "risk": "Pickle deserialization — can execute arbitrary code", "fix": "Use json.loads() or other safe serialization formats", }, { "regex": r"\byaml\.(?:load|unsafe_load)\s*\([^)]*(?!Loader\s*=\s*yaml\.SafeLoader)", "category": "DESERIAL", "severity": Severity.HIGH, "risk": "Unsafe YAML loading — can execute arbitrary code", "fix": "Use yaml.safe_load() or yaml.load(data, Loader=yaml.SafeLoader)", }, { "regex": r"\bmarshal\.loads?\s*\(", "category": "DESERIAL", "severity": Severity.HIGH, "risk": "Marshal deserialization — can execute arbitrary code", "fix": "Use json.loads() or other safe serialization formats", }, { "regex": r"\bshelve\.open\s*\(", "category": "DESERIAL", "severity": Severity.HIGH, "risk": "Shelve uses pickle internally — can execute arbitrary code", "fix": "Use JSON or SQLite for persistent storage", }, ] # ============================================================================= # PROMPT INJECTION PATTERNS # ============================================================================= PROMPT_INJECTION_PATTERNS = [ # System prompt override — CRITICAL { "regex": r"(?i)ignore\s+(?:all\s+)?(?:previous|prior|above)\s+instructions", "category": "PROMPT-OVERRIDE", "severity": Severity.CRITICAL, "risk": "Attempts to override system prompt and prior instructions", "fix": "Remove instruction override attempts", }, { "regex": r"(?i)you\s+are\s+now\s+(?:a|an|the)\s+", "category": "PROMPT-OVERRIDE", "severity": Severity.CRITICAL, "risk": "Role hijacking — attempts to redefine the AI's identity", "fix": "Remove role redefinition. Skills should provide instructions, not identity changes", }, { "regex": r"(?i)(?:disregard|forget|override)\s+(?:your|all|any)\s+(?:instructions|rules|guidelines|constraints|safety)", "category": "PROMPT-OVERRIDE", "severity": Severity.CRITICAL, "risk": "Explicit instruction override attempt", "fix": "Remove override directives", }, { "regex": r"(?i)(?:pretend|act\s+as\s+if|imagine)\s+you\s+(?:have\s+no|don'?t\s+have\s+any)\s+(?:restrictions|limits|rules|safety)", "category": "SAFETY-BYPASS", "severity": Severity.CRITICAL, "risk": "Safety restriction bypass attempt", "fix": "Remove safety bypass instructions", }, { "regex": r"(?i)(?:skip|disable|bypass|turn\s+off|ignore)\s+(?:safety|content|security)\s+(?:checks?|filters?|restrictions?|rules?)", "category": "SAFETY-BYPASS", "severity": Severity.CRITICAL, "risk": "Explicit safety mechanism bypass", "fix": "Remove safety bypass directives", }, { "regex": r"(?i)(?:execute|run)\s+(?:any|all|arbitrary)\s+(?:commands?|code|scripts?)\s+(?:without|no)\s+(?:asking|confirmation|restriction|limit)", "category": "SAFETY-BYPASS", "severity": Severity.CRITICAL, "risk": "Unrestricted command execution directive", "fix": "Add explicit permission requirements for any command execution", }, # Data extraction — CRITICAL { "regex": r"(?i)(?:send|upload|post|transmit|exfiltrate)\s+(?:the\s+)?(?:contents?|data|files?|information)\s+(?:of|from|to)", "category": "PROMPT-EXFIL", "severity": Severity.CRITICAL, "risk": "Instruction to exfiltrate data", "fix": "Remove data transmission directives", }, { "regex": r"(?i)(?:read|access|open|get)\s+(?:the\s+)?(?:contents?\s+of\s+)?(?:~|\/home|\/etc|\.ssh|\.aws|\.env|credentials?|secrets?|api.?keys?)", "category": "PROMPT-EXFIL", "severity": Severity.CRITICAL, "risk": "Instruction to access sensitive files or credentials", "fix": "Remove credential/sensitive file access directives", }, # Hidden instructions — HIGH { "regex": r"[\u200b\u200c\u200d\ufeff\u00ad]", "category": "HIDDEN-INSTR", "severity": Severity.HIGH, "risk": "Zero-width or invisible characters — may hide instructions", "fix": "Remove zero-width characters. All instructions should be visible", }, { "regex": r"