Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
127 KiB
JSON
1 line
127 KiB
JSON
{"repo": "btr4k/bugbounty-agent", "files": {"config.yaml": {"b25b64ac6467e86a8b25a1a44557a4640ad6cfcf": {"t": 1785745200.6771472, "n": 4812, "h": "08e42a33f86b1e25", "c": "# Bug Bounty AI Agent Configuration\n\n# ═══════════════════════════════════════════════════════\n# AI Provider Configuration\n# Supported: claude | deepseek | openai | openrouter | custom\n# ═══════════════════════════════════════════════════════\nai:\n provider: \"auto\" # auto = detect from .env (claude/deepseek/openai/openrouter)\n api_key: \"\" # Leave empty — key is read from .env automatically\n model: \"\" # Leave empty for provider default\n max_tokens: 2000\n base_url: \"\" # Only needed for openrouter/custom\n timeout: 300 # Per-request HTTP timeout (seconds). Raise for slow providers/large JS batches\n\n# Provider presets (just change 'provider' and 'api_key'):\n# ┌─────────────┬──────────────────────────┬────────────────────────────┬──────────────┐\n# │ Provider │ Model (default) │ Env Variable │ Cost/1M tok │\n# ├─────────────┼──────────────────────────┼────────────────────────────┼──────────────┤\n# │ claude │ claude-sonnet-4-20250514 │ ANTHROPIC_API_KEY │ $3/$15 │\n# │ deepseek │ deepseek-chat │ DEEPSEEK_API_KEY │ $0.28/$0.42 │\n# │ openai │ gpt-4o-mini │ OPENAI_API_KEY │ $0.15/$0.60 │\n# │ openrouter │ deepseek/deepseek-chat │ OPENROUTER_API_KEY │ varies │\n# │ custom │ (must specify) │ AI_API_KEY │ varies │\n# └─────────────┴──────────────────────────┴────────────────────────────┴──────────────┘\n\n# Legacy Claude config (backward compatibility - used if ai.api_key is empty)\nclaude:\n api_key: \"${ANTHROPIC_API_KEY}\"\n model: \"claude-sonnet-4-20250514\"\n max_tokens: 2000\n\n# C99.nl API Intelligence\nc99:\n api_key: \"${C99_API_KEY}\"\n enabled: true\n\n# Target Configuration\ntarget:\n domains: []\n excluded_subdomains: []\n\n# Optional authenticated testing. Use only accounts and sessions you own.\n# Values are expanded from environment variables and redacted from AI prompts\n# and reports. Avoid placing literal credentials in this file.\nauthentication:\n # Required when headers or cookies are configured. Credentials are injected\n # only when every target in a tool invocation matches this allowlist.\n allowed_hosts: []\n # Example:\n # - \"app.example.com\"\n # - \"*.api.example.com\"\n headers: {}\n # Example:\n # Authorization: \"Bearer ${TARGET_TOKEN}\"\n cookies: {}\n # Example:\n # session: \"${TARGET_SESSION}\"\n\n# Reconnaissance Settings\nrecon:\n enabled: true\n timeout: 300 # seconds\n max_subdomains: 1000\n max_wayback_urls: 10000 # prevent OOM for large domains\n tools:\n subfinder: true\n assetfinder: true\n wayback: true\n katana: true # JS file extraction crawler\n cert_transparency: true\n\n# Scanning Settings\n#\n# Active scanners run SEQUENTIALLY (nuclei → dalfox), never in parallel. Running\n# several heavy scanners at once on a modest host exhausts memory and the OOM\n# killer terminates them mid-scan. One scanner at a time finishes reliably.\nscanning:\n enabled: true\n threads: 40 # Balanced default for reliable scans on modest servers\n timeout: 1200 # 20-minute budget for the nuclei high-value scan (runs alone)\n # Requests/sec ceiling. At 50 a full scan of ~1300 templates × dozens of hosts\n # could not finish inside the budget (≈57 min of requests). 150 is Nuclei's own\n # default and completes the scan with margin. Lower it for strict programs.\n rate_limit: 150\n tools:\n nuclei:\n enabled: true\n severity: [] # Empty runs actionable severities: critical, high, medium, low\n # High-signal profile. \"misconfig\" (≈800 templates) was dropped: it nearly\n # doubled request volume without producing confirmed findings here. Add it\n # back if your host and the program's rate limits allow a longer scan.\n tags: [\"exposure\", \"takeover\", \"default-login\"]\n templates_path: \"\" # Empty uses Nuclei's installed templates\n httpx:\n enabled: true\n follow_redirects: true\n status_code: true\n dalfox:\n enabled: true\n blind_url: \"\" # Optional: blind XSS callback URL (e.g. https://your-burp-collaborator)\n max_urls: 75 # Parameterized URLs to fuzz for reflected XSS\n nmap:\n enabled: false # Open ports alone are reconnaissance, not confirmed vulnerabilities\n ports: \"21,22,25,53,80,110,143,443,445,993,995,3000,3306,5432,6379,8000,8080,8443,8888,9090,9200,27017\"\n fast_scan: true\n# AI Analysis\nanalysis:\n min_confidence: 0.85 # Findings below this require manual confirmation and are not reported\n js_analysis: true # AI-powered JS file analysis\n\n# Reporting\nreporting:\n include_poc: true\n severity_filter: [] # Optional actionable severities: critical, high, medium, low\n output_dir: \"./reports\"\n"}, "bad05863b51bea22ca612fd10fa2588e7059e51a": {"t": 1785748579.658009, "n": 4812, "h": "08e42a33f86b1e25", "c": "# Bug Bounty AI Agent Configuration\n\n# ═══════════════════════════════════════════════════════\n# AI Provider Configuration\n# Supported: claude | deepseek | openai | openrouter | custom\n# ═══════════════════════════════════════════════════════\nai:\n provider: \"auto\" # auto = detect from .env (claude/deepseek/openai/openrouter)\n api_key: \"\" # Leave empty — key is read from .env automatically\n model: \"\" # Leave empty for provider default\n max_tokens: 2000\n base_url: \"\" # Only needed for openrouter/custom\n timeout: 300 # Per-request HTTP timeout (seconds). Raise for slow providers/large JS batches\n\n# Provider presets (just change 'provider' and 'api_key'):\n# ┌─────────────┬──────────────────────────┬────────────────────────────┬──────────────┐\n# │ Provider │ Model (default) │ Env Variable │ Cost/1M tok │\n# ├─────────────┼──────────────────────────┼────────────────────────────┼──────────────┤\n# │ claude │ claude-sonnet-4-20250514 │ ANTHROPIC_API_KEY │ $3/$15 │\n# │ deepseek │ deepseek-chat │ DEEPSEEK_API_KEY │ $0.28/$0.42 │\n# │ openai │ gpt-4o-mini │ OPENAI_API_KEY │ $0.15/$0.60 │\n# │ openrouter │ deepseek/deepseek-chat │ OPENROUTER_API_KEY │ varies │\n# │ custom │ (must specify) │ AI_API_KEY │ varies │\n# └─────────────┴──────────────────────────┴────────────────────────────┴──────────────┘\n\n# Legacy Claude config (backward compatibility - used if ai.api_key is empty)\nclaude:\n api_key: \"${ANTHROPIC_API_KEY}\"\n model: \"claude-sonnet-4-20250514\"\n max_tokens: 2000\n\n# C99.nl API Intelligence\nc99:\n api_key: \"${C99_API_KEY}\"\n enabled: true\n\n# Target Configuration\ntarget:\n domains: []\n excluded_subdomains: []\n\n# Optional authenticated testing. Use only accounts and sessions you own.\n# Values are expanded from environment variables and redacted from AI prompts\n# and reports. Avoid placing literal credentials in this file.\nauthentication:\n # Required when headers or cookies are configured. Credentials are injected\n # only when every target in a tool invocation matches this allowlist.\n allowed_hosts: []\n # Example:\n # - \"app.example.com\"\n # - \"*.api.example.com\"\n headers: {}\n # Example:\n # Authorization: \"Bearer ${TARGET_TOKEN}\"\n cookies: {}\n # Example:\n # session: \"${TARGET_SESSION}\"\n\n# Reconnaissance Settings\nrecon:\n enabled: true\n timeout: 300 # seconds\n max_subdomains: 1000\n max_wayback_urls: 10000 # prevent OOM for large domains\n tools:\n subfinder: true\n assetfinder: true\n wayback: true\n katana: true # JS file extraction crawler\n cert_transparency: true\n\n# Scanning Settings\n#\n# Active scanners run SEQUENTIALLY (nuclei → dalfox), never in parallel. Running\n# several heavy scanners at once on a modest host exhausts memory and the OOM\n# killer terminates them mid-scan. One scanner at a time finishes reliably.\nscanning:\n enabled: true\n threads: 40 # Balanced default for reliable scans on modest servers\n timeout: 1200 # 20-minute budget for the nuclei high-value scan (runs alone)\n # Requests/sec ceiling. At 50 a full scan of ~1300 templates × dozens of hosts\n # could not finish inside the budget (≈57 min of requests). 150 is Nuclei's own\n # default and completes the scan with margin. Lower it for strict programs.\n rate_limit: 150\n tools:\n nuclei:\n enabled: true\n severity: [] # Empty runs actionable severities: critical, high, medium, low\n # High-signal profile. \"misconfig\" (≈800 templates) was dropped: it nearly\n # doubled request volume without producing confirmed findings here. Add it\n # back if your host and the program's rate limits allow a longer scan.\n tags: [\"exposure\", \"takeover\", \"default-login\"]\n templates_path: \"\" # Empty uses Nuclei's installed templates\n httpx:\n enabled: true\n follow_redirects: true\n status_code: true\n dalfox:\n enabled: true\n blind_url: \"\" # Optional: blind XSS callback URL (e.g. https://your-burp-collaborator)\n max_urls: 75 # Parameterized URLs to fuzz for reflected XSS\n nmap:\n enabled: false # Open ports alone are reconnaissance, not confirmed vulnerabilities\n ports: \"21,22,25,53,80,110,143,443,445,993,995,3000,3306,5432,6379,8000,8080,8443,8888,9090,9200,27017\"\n fast_scan: true\n# AI Analysis\nanalysis:\n min_confidence: 0.85 # Findings below this require manual confirmation and are not reported\n js_analysis: true # AI-powered JS file analysis\n\n# Reporting\nreporting:\n include_poc: true\n severity_filter: [] # Optional actionable severities: critical, high, medium, low\n output_dir: \"./reports\"\n"}, "50ae59b7a87199dc974a3d1dfc63b3e7ef9c66be": {"t": 1785748580.7584336, "n": 5095, "h": "46f485ad1b209075", "c": "# Bug Bounty AI Agent Configuration\n\n# ═══════════════════════════════════════════════════════\n# AI Provider Configuration\n# Supported: claude | deepseek | openai | openrouter | custom\n# ═══════════════════════════════════════════════════════\nai:\n provider: \"auto\" # auto = detect from .env (claude/deepseek/openai/openrouter)\n api_key: \"\" # Leave empty — key is read from .env automatically\n model: \"\" # Leave empty for provider default\n max_tokens: 2000\n base_url: \"\" # Only needed for openrouter/custom\n timeout: 300 # Per-request HTTP timeout (seconds). Raise for slow providers/large JS batches\n\n# Provider presets (just change 'provider' and 'api_key'):\n# ┌─────────────┬──────────────────────────┬────────────────────────────┬──────────────┐\n# │ Provider │ Model (default) │ Env Variable │ Cost/1M tok │\n# ├─────────────┼──────────────────────────┼────────────────────────────┼──────────────┤\n# │ claude │ claude-sonnet-4-20250514 │ ANTHROPIC_API_KEY │ $3/$15 │\n# │ deepseek │ deepseek-chat │ DEEPSEEK_API_KEY │ $0.28/$0.42 │\n# │ openai │ gpt-4o-mini │ OPENAI_API_KEY │ $0.15/$0.60 │\n# │ openrouter │ deepseek/deepseek-chat │ OPENROUTER_API_KEY │ varies │\n# │ custom │ (must specify) │ AI_API_KEY │ varies │\n# └─────────────┴──────────────────────────┴────────────────────────────┴──────────────┘\n\n# Legacy Claude config (backward compatibility - used if ai.api_key is empty)\nclaude:\n api_key: \"${ANTHROPIC_API_KEY}\"\n model: \"claude-sonnet-4-20250514\"\n max_tokens: 2000\n\n# C99.nl API Intelligence\nc99:\n api_key: \"${C99_API_KEY}\"\n enabled: true\n\n# Target Configuration\ntarget:\n domains: []\n excluded_subdomains: []\n\n# Optional authenticated testing. Use only accounts and sessions you own.\n# Values are expanded from environment variables and redacted from AI prompts\n# and reports. Avoid placing literal credentials in this file.\nauthentication:\n # Required when headers or cookies are configured. Credentials are injected\n # only when every target in a tool invocation matches this allowlist.\n allowed_hosts: []\n # Example:\n # - \"app.example.com\"\n # - \"*.api.example.com\"\n headers: {}\n # Example:\n # Authorization: \"Bearer ${TARGET_TOKEN}\"\n cookies: {}\n # Example:\n # session: \"${TARGET_SESSION}\"\n\n# Reconnaissance Settings\nrecon:\n enabled: true\n timeout: 300 # seconds\n max_subdomains: 1000\n max_wayback_urls: 10000 # prevent OOM for large domains\n tools:\n subfinder: true\n assetfinder: true\n wayback: true\n katana: true # JS file extraction crawler\n cert_transparency: true\n\n# Scanning Settings\nscanning:\n enabled: true\n threads: 40 # Balanced default for reliable scans on modest servers\n timeout: 1200 # 20 minutes for high-priority templates\n rate_limit: 50 # Per-tool ceiling; lower this to match program rules\n tools:\n nuclei:\n enabled: true\n severity: [] # Empty runs actionable severities: critical, high, medium, low\n tags: [\"exposure\", \"misconfig\", \"takeover\", \"default-login\"] # High-signal default profile\n templates_path: \"\" # Empty uses Nuclei's installed templates\n httpx:\n enabled: true\n follow_redirects: true\n status_code: true\n nmap:\n enabled: false # Open ports alone are reconnaissance, not confirmed vulnerabilities\n ports: \"21,22,25,53,80,110,143,443,445,993,995,3000,3306,5432,6379,8000,8080,8443,8888,9090,9200,27017\"\n fast_scan: true\n dalfox:\n enabled: true\n blind_url: \"\" # Optional: blind XSS callback URL (e.g. https://your-burp-collaborator)\n max_urls: 100 # Increased for better XSS coverage\n ffuf:\n enabled: true\n # Wordlist search order (first found wins):\n # 1. This value (if set)\n # 2. /usr/share/seclists/Discovery/Web-Content/common.txt\n # 3. /usr/share/wordlists/dirb/common.txt\n # 4. /opt/SecLists/Discovery/Web-Content/common.txt\n # 5. Built-in ~130 high-value paths (fallback — limited coverage)\n # Recommended: apt install seclists OR set full path below\n wordlist_path: \"\"\n max_targets: 20 # Top N hosts to path-bruteforce (scored by importance)\n vhost_fuzzing: false # Discovery-only; enable manually when needed\n sqli:\n enabled: true # Uses focused Nuclei SQLi templates and parameter filtering\n max_urls: 50 # Max gf-filtered param URLs to test\n arjun:\n enabled: true # Hidden parameter discovery — feeds discovered params into dalfox\n max_targets: 15 # Top N hosts to discover hidden params on\n# AI Analysis\nanalysis:\n min_confidence: 0.85 # Findings below this require manual confirmation and are not reported\n js_analysis: true # AI-powered JS file analysis\n\n# Reporting\nreporting:\n include_poc: true\n severity_filter: [] # Optional actionable severities: critical, high, medium, low\n output_dir: \"./reports\"\n"}, "75aa7cb5a117cd4a2c2a5864086b4fc3bd0aee5f": {"t": 1785748581.8288908, "n": 4998, "h": "f3732ae3b54f189a", "c": "# Bug Bounty AI Agent Configuration\n\n# ═══════════════════════════════════════════════════════\n# AI Provider Configuration\n# Supported: claude | deepseek | openai | openrouter | custom\n# ═══════════════════════════════════════════════════════\nai:\n provider: \"auto\" # auto = detect from .env (claude/deepseek/openai/openrouter)\n api_key: \"\" # Leave empty — key is read from .env automatically\n model: \"\" # Leave empty for provider default\n max_tokens: 2000\n base_url: \"\" # Only needed for openrouter/custom\n timeout: 300 # Per-request HTTP timeout (seconds). Raise for slow providers/large JS batches\n\n# Provider presets (just change 'provider' and 'api_key'):\n# ┌─────────────┬──────────────────────────┬────────────────────────────┬──────────────┐\n# │ Provider │ Model (default) │ Env Variable │ Cost/1M tok │\n# ├─────────────┼──────────────────────────┼────────────────────────────┼──────────────┤\n# │ claude │ claude-sonnet-4-20250514 │ ANTHROPIC_API_KEY │ $3/$15 │\n# │ deepseek │ deepseek-chat │ DEEPSEEK_API_KEY │ $0.28/$0.42 │\n# │ openai │ gpt-4o-mini │ OPENAI_API_KEY │ $0.15/$0.60 │\n# │ openrouter │ deepseek/deepseek-chat │ OPENROUTER_API_KEY │ varies │\n# │ custom │ (must specify) │ AI_API_KEY │ varies │\n# └─────────────┴──────────────────────────┴────────────────────────────┴──────────────┘\n\n# Legacy Claude config (backward compatibility - used if ai.api_key is empty)\nclaude:\n api_key: \"${ANTHROPIC_API_KEY}\"\n model: \"claude-sonnet-4-20250514\"\n max_tokens: 2000\n\n# C99.nl API Intelligence\nc99:\n api_key: \"${C99_API_KEY}\"\n enabled: true\n\n# Target Configuration\ntarget:\n domains: []\n excluded_subdomains: []\n\n# Optional authenticated testing. Use only accounts and sessions you own.\n# Values are expanded from environment variables and redacted from AI prompts\n# and reports. Avoid placing literal credentials in this file.\nauthentication:\n # Required when headers or cookies are configured. Credentials are injected\n # only when every target in a tool invocation matches this allowlist.\n allowed_hosts: []\n # Example:\n # - \"app.example.com\"\n # - \"*.api.example.com\"\n headers: {}\n # Example:\n # Authorization: \"Bearer ${TARGET_TOKEN}\"\n cookies: {}\n # Example:\n # session: \"${TARGET_SESSION}\"\n\n# Reconnaissance Settings\nrecon:\n enabled: true\n timeout: 300 # seconds\n max_subdomains: 1000\n max_wayback_urls: 10000 # prevent OOM for large domains\n tools:\n subfinder: true\n assetfinder: true\n wayback: true\n katana: true # JS file extraction crawler\n cert_transparency: true\n\n# Scanning Settings\nscanning:\n enabled: true\n threads: 40 # Balanced default for reliable scans on modest servers\n timeout: 1200 # 20 minutes for high-priority templates\n rate_limit: 50 # Per-tool ceiling; lower this to match program rules\n tools:\n nuclei:\n enabled: true\n severity: [] # Empty runs actionable severities: critical, high, medium, low\n templates_path: \"\" # Empty uses Nuclei's installed templates\n httpx:\n enabled: true\n follow_redirects: true\n status_code: true\n nmap:\n enabled: false # Open ports alone are reconnaissance, not confirmed vulnerabilities\n ports: \"21,22,25,53,80,110,143,443,445,993,995,3000,3306,5432,6379,8000,8080,8443,8888,9090,9200,27017\"\n fast_scan: true\n dalfox:\n enabled: true\n blind_url: \"\" # Optional: blind XSS callback URL (e.g. https://your-burp-collaborator)\n max_urls: 100 # Increased for better XSS coverage\n ffuf:\n enabled: true\n # Wordlist search order (first found wins):\n # 1. This value (if set)\n # 2. /usr/share/seclists/Discovery/Web-Content/common.txt\n # 3. /usr/share/wordlists/dirb/common.txt\n # 4. /opt/SecLists/Discovery/Web-Content/common.txt\n # 5. Built-in ~130 high-value paths (fallback — limited coverage)\n # Recommended: apt install seclists OR set full path below\n wordlist_path: \"\"\n max_targets: 20 # Top N hosts to path-bruteforce (scored by importance)\n vhost_fuzzing: false # Discovery-only; enable manually when needed\n sqli:\n enabled: true # Uses focused Nuclei SQLi templates and parameter filtering\n max_urls: 50 # Max gf-filtered param URLs to test\n arjun:\n enabled: true # Hidden parameter discovery — feeds discovered params into dalfox\n max_targets: 15 # Top N hosts to discover hidden params on\n# AI Analysis\nanalysis:\n min_confidence: 0.85 # Findings below this require manual confirmation and are not reported\n js_analysis: true # AI-powered JS file analysis\n\n# Reporting\nreporting:\n include_poc: true\n severity_filter: [] # Optional actionable severities: critical, high, medium, low\n output_dir: \"./reports\"\n"}, "72391219d44962305c54cc3acf8087c2cb28c73f": {"t": 1785748582.9020936, "n": 5001, "h": "d2a23fcd9d614232", "c": "# Bug Bounty AI Agent Configuration\n\n# ═══════════════════════════════════════════════════════\n# AI Provider Configuration\n# Supported: claude | deepseek | openai | openrouter | custom\n# ═══════════════════════════════════════════════════════\nai:\n provider: \"auto\" # auto = detect from .env (claude/deepseek/openai/openrouter)\n api_key: \"\" # Leave empty — key is read from .env automatically\n model: \"\" # Leave empty for provider default\n max_tokens: 2000\n base_url: \"\" # Only needed for openrouter/custom\n timeout: 300 # Per-request HTTP timeout (seconds). Raise for slow providers/large JS batches\n\n# Provider presets (just change 'provider' and 'api_key'):\n# ┌─────────────┬──────────────────────────┬────────────────────────────┬──────────────┐\n# │ Provider │ Model (default) │ Env Variable │ Cost/1M tok │\n# ├─────────────┼──────────────────────────┼────────────────────────────┼──────────────┤\n# │ claude │ claude-sonnet-4-20250514 │ ANTHROPIC_API_KEY │ $3/$15 │\n# │ deepseek │ deepseek-chat │ DEEPSEEK_API_KEY │ $0.28/$0.42 │\n# │ openai │ gpt-4o-mini │ OPENAI_API_KEY │ $0.15/$0.60 │\n# │ openrouter │ deepseek/deepseek-chat │ OPENROUTER_API_KEY │ varies │\n# │ custom │ (must specify) │ AI_API_KEY │ varies │\n# └─────────────┴──────────────────────────┴────────────────────────────┴──────────────┘\n\n# Legacy Claude config (backward compatibility - used if ai.api_key is empty)\nclaude:\n api_key: \"${ANTHROPIC_API_KEY}\"\n model: \"claude-sonnet-4-20250514\"\n max_tokens: 2000\n\n# C99.nl API Intelligence\nc99:\n api_key: \"${C99_API_KEY}\"\n enabled: true\n\n# Target Configuration\ntarget:\n domains: []\n excluded_subdomains: []\n\n# Optional authenticated testing. Use only accounts and sessions you own.\n# Values are expanded from environment variables and redacted from AI prompts\n# and reports. Avoid placing literal credentials in this file.\nauthentication:\n # Required when headers or cookies are configured. Credentials are injected\n # only when every target in a tool invocation matches this allowlist.\n allowed_hosts: []\n # Example:\n # - \"app.example.com\"\n # - \"*.api.example.com\"\n headers: {}\n # Example:\n # Authorization: \"Bearer ${TARGET_TOKEN}\"\n cookies: {}\n # Example:\n # session: \"${TARGET_SESSION}\"\n\n# Reconnaissance Settings\nrecon:\n enabled: true\n timeout: 300 # seconds\n max_subdomains: 1000\n max_wayback_urls: 10000 # prevent OOM for large domains\n tools:\n subfinder: true\n assetfinder: true\n wayback: true\n katana: true # JS file extraction crawler\n cert_transparency: true\n\n# Scanning Settings\nscanning:\n enabled: true\n threads: 40 # Balanced default for reliable scans on modest servers\n timeout: 1200 # 20 minutes for high-priority templates\n rate_limit: 50 # Per-tool ceiling; lower this to match program rules\n tools:\n nuclei:\n enabled: true\n severity: [] # No filter - run all templates, findings keep their real severity\n templates_path: \"\" # Empty uses Nuclei's installed templates\n httpx:\n enabled: true\n follow_redirects: true\n status_code: true\n nmap:\n enabled: false # Open ports alone are reconnaissance, not confirmed vulnerabilities\n ports: \"21,22,25,53,80,110,143,443,445,993,995,3000,3306,5432,6379,8000,8080,8443,8888,9090,9200,27017\"\n fast_scan: true\n dalfox:\n enabled: true\n blind_url: \"\" # Optional: blind XSS callback URL (e.g. https://your-burp-collaborator)\n max_urls: 100 # Increased for better XSS coverage\n ffuf:\n enabled: true\n # Wordlist search order (first found wins):\n # 1. This value (if set)\n # 2. /usr/share/seclists/Discovery/Web-Content/common.txt\n # 3. /usr/share/wordlists/dirb/common.txt\n # 4. /opt/SecLists/Discovery/Web-Content/common.txt\n # 5. Built-in ~130 high-value paths (fallback — limited coverage)\n # Recommended: apt install seclists OR set full path below\n wordlist_path: \"\"\n max_targets: 20 # Top N hosts to path-bruteforce (scored by importance)\n vhost_fuzzing: false # Discovery-only; enable manually when needed\n sqli:\n enabled: true # Uses focused Nuclei SQLi templates and parameter filtering\n max_urls: 50 # Max gf-filtered param URLs to test\n arjun:\n enabled: true # Hidden parameter discovery — feeds discovered params into dalfox\n max_targets: 15 # Top N hosts to discover hidden params on\n# AI Analysis\nanalysis:\n min_confidence: 0.85 # Findings below this require manual confirmation and are not reported\n js_analysis: true # AI-powered JS file analysis\n\n# Reporting\nreporting:\n include_poc: true\n severity_filter: [] # Optional actionable severities: critical, high, medium, low\n output_dir: \"./reports\"\n"}, "602d8ee4148fdf98d37603a3df7da8086f4509b4": {"t": 1785748585.0039482, "n": 4984, "h": "6a1be5cc01dd948a", "c": "# Bug Bounty AI Agent Configuration\n\n# ═══════════════════════════════════════════════════════\n# AI Provider Configuration\n# Supported: claude | deepseek | openai | openrouter | custom\n# ═══════════════════════════════════════════════════════\nai:\n provider: \"auto\" # auto = detect from .env (claude/deepseek/openai/openrouter)\n api_key: \"\" # Leave empty — key is read from .env automatically\n model: \"\" # Leave empty for provider default\n max_tokens: 2000\n base_url: \"\" # Only needed for openrouter/custom\n timeout: 300 # Per-request HTTP timeout (seconds). Raise for slow providers/large JS batches\n\n# Provider presets (just change 'provider' and 'api_key'):\n# ┌─────────────┬──────────────────────────┬────────────────────────────┬──────────────┐\n# │ Provider │ Model (default) │ Env Variable │ Cost/1M tok │\n# ├─────────────┼──────────────────────────┼────────────────────────────┼──────────────┤\n# │ claude │ claude-sonnet-4-20250514 │ ANTHROPIC_API_KEY │ $3/$15 │\n# │ deepseek │ deepseek-chat │ DEEPSEEK_API_KEY │ $0.28/$0.42 │\n# │ openai │ gpt-4o-mini │ OPENAI_API_KEY │ $0.15/$0.60 │\n# │ openrouter │ deepseek/deepseek-chat │ OPENROUTER_API_KEY │ varies │\n# │ custom │ (must specify) │ AI_API_KEY │ varies │\n# └─────────────┴──────────────────────────┴────────────────────────────┴──────────────┘\n\n# Legacy Claude config (backward compatibility - used if ai.api_key is empty)\nclaude:\n api_key: \"${ANTHROPIC_API_KEY}\"\n model: \"claude-sonnet-4-20250514\"\n max_tokens: 2000\n\n# C99.nl API Intelligence\nc99:\n api_key: \"${C99_API_KEY}\"\n enabled: true\n\n# Target Configuration\ntarget:\n domains: []\n excluded_subdomains: []\n\n# Optional authenticated testing. Use only accounts and sessions you own.\n# Values are expanded from environment variables and redacted from AI prompts\n# and reports. Avoid placing literal credentials in this file.\nauthentication:\n # Required when headers or cookies are configured. Credentials are injected\n # only when every target in a tool invocation matches this allowlist.\n allowed_hosts: []\n # Example:\n # - \"app.example.com\"\n # - \"*.api.example.com\"\n headers: {}\n # Example:\n # Authorization: \"Bearer ${TARGET_TOKEN}\"\n cookies: {}\n # Example:\n # session: \"${TARGET_SESSION}\"\n\n# Reconnaissance Settings\nrecon:\n enabled: true\n timeout: 300 # seconds\n max_subdomains: 1000\n max_wayback_urls: 10000 # prevent OOM for large domains\n tools:\n subfinder: true\n assetfinder: true\n wayback: true\n katana: true # JS file extraction crawler\n cert_transparency: true\n\n# Scanning Settings\nscanning:\n enabled: true\n threads: 75 # Increased for better performance\n timeout: 1200 # 20 minutes for high-priority templates\n rate_limit: 100 # requests per second; lower this to match program rules\n tools:\n nuclei:\n enabled: true\n severity: [] # No filter - run all templates, findings keep their real severity\n templates_path: \"\" # Empty uses Nuclei's installed templates\n httpx:\n enabled: true\n follow_redirects: true\n status_code: true\n nmap:\n enabled: false # Open ports alone are reconnaissance, not confirmed vulnerabilities\n ports: \"21,22,25,53,80,110,143,443,445,993,995,3000,3306,5432,6379,8000,8080,8443,8888,9090,9200,27017\"\n fast_scan: true\n dalfox:\n enabled: true\n blind_url: \"\" # Optional: blind XSS callback URL (e.g. https://your-burp-collaborator)\n max_urls: 100 # Increased for better XSS coverage\n ffuf:\n enabled: true\n # Wordlist search order (first found wins):\n # 1. This value (if set)\n # 2. /usr/share/seclists/Discovery/Web-Content/common.txt\n # 3. /usr/share/wordlists/dirb/common.txt\n # 4. /opt/SecLists/Discovery/Web-Content/common.txt\n # 5. Built-in ~130 high-value paths (fallback — limited coverage)\n # Recommended: apt install seclists OR set full path below\n wordlist_path: \"\"\n max_targets: 20 # Top N hosts to path-bruteforce (scored by importance)\n vhost_fuzzing: false # Discovery-only; enable manually when needed\n sqli:\n enabled: true # Uses focused Nuclei SQLi templates and parameter filtering\n max_urls: 50 # Max gf-filtered param URLs to test\n arjun:\n enabled: true # Hidden parameter discovery — feeds discovered params into dalfox\n max_targets: 15 # Top N hosts to discover hidden params on\n# AI Analysis\nanalysis:\n min_confidence: 0.85 # Findings below this require manual confirmation and are not reported\n js_analysis: true # AI-powered JS file analysis\n\n# Reporting\nreporting:\n include_poc: true\n severity_filter: [] # Optional actionable severities: critical, high, medium, low\n output_dir: \"./reports\"\n"}, "0d87d52a9211ef11b1474ce0da97726a60d5e212": {"t": 1785748586.0964358, "n": 4868, "h": "4d5df6581de33af9", "c": "# Bug Bounty AI Agent Configuration\n\n# ═══════════════════════════════════════════════════════\n# AI Provider Configuration\n# Supported: claude | deepseek | openai | openrouter | custom\n# ═══════════════════════════════════════════════════════\nai:\n provider: \"auto\" # auto = detect from .env (claude/deepseek/openai/openrouter)\n api_key: \"\" # Leave empty — key is read from .env automatically\n model: \"\" # Leave empty for provider default\n max_tokens: 2000\n base_url: \"\" # Only needed for openrouter/custom\n\n# Provider presets (just change 'provider' and 'api_key'):\n# ┌─────────────┬──────────────────────────┬────────────────────────────┬──────────────┐\n# │ Provider │ Model (default) │ Env Variable │ Cost/1M tok │\n# ├─────────────┼──────────────────────────┼────────────────────────────┼──────────────┤\n# │ claude │ claude-sonnet-4-20250514 │ ANTHROPIC_API_KEY │ $3/$15 │\n# │ deepseek │ deepseek-chat │ DEEPSEEK_API_KEY │ $0.28/$0.42 │\n# │ openai │ gpt-4o-mini │ OPENAI_API_KEY │ $0.15/$0.60 │\n# │ openrouter │ deepseek/deepseek-chat │ OPENROUTER_API_KEY │ varies │\n# │ custom │ (must specify) │ AI_API_KEY │ varies │\n# └─────────────┴──────────────────────────┴────────────────────────────┴──────────────┘\n\n# Legacy Claude config (backward compatibility - used if ai.api_key is empty)\nclaude:\n api_key: \"${ANTHROPIC_API_KEY}\"\n model: \"claude-sonnet-4-20250514\"\n max_tokens: 2000\n\n# C99.nl API Intelligence\nc99:\n api_key: \"${C99_API_KEY}\"\n enabled: true\n\n# Target Configuration\ntarget:\n domains: []\n excluded_subdomains: []\n\n# Optional authenticated testing. Use only accounts and sessions you own.\n# Values are expanded from environment variables and redacted from AI prompts\n# and reports. Avoid placing literal credentials in this file.\nauthentication:\n # Required when headers or cookies are configured. Credentials are injected\n # only when every target in a tool invocation matches this allowlist.\n allowed_hosts: []\n # Example:\n # - \"app.example.com\"\n # - \"*.api.example.com\"\n headers: {}\n # Example:\n # Authorization: \"Bearer ${TARGET_TOKEN}\"\n cookies: {}\n # Example:\n # session: \"${TARGET_SESSION}\"\n\n# Reconnaissance Settings\nrecon:\n enabled: true\n timeout: 300 # seconds\n max_subdomains: 1000\n max_wayback_urls: 10000 # prevent OOM for large domains\n tools:\n subfinder: true\n assetfinder: true\n wayback: true\n katana: true # JS file extraction crawler\n cert_transparency: true\n\n# Scanning Settings\nscanning:\n enabled: true\n threads: 75 # Increased for better performance\n timeout: 1200 # 20 minutes for high-priority templates\n rate_limit: 100 # requests per second; lower this to match program rules\n tools:\n nuclei:\n enabled: true\n severity: [] # No filter - run all templates, findings keep their real severity\n templates_path: \"\" # Empty uses Nuclei's installed templates\n httpx:\n enabled: true\n follow_redirects: true\n status_code: true\n nmap:\n enabled: false # Open ports alone are reconnaissance, not confirmed vulnerabilities\n ports: \"21,22,25,53,80,110,143,443,445,993,995,3000,3306,5432,6379,8000,8080,8443,8888,9090,9200,27017\"\n fast_scan: true\n dalfox:\n enabled: true\n blind_url: \"\" # Optional: blind XSS callback URL (e.g. https://your-burp-collaborator)\n max_urls: 100 # Increased for better XSS coverage\n ffuf:\n enabled: true\n # Wordlist search order (first found wins):\n # 1. This value (if set)\n # 2. /usr/share/seclists/Discovery/Web-Content/common.txt\n # 3. /usr/share/wordlists/dirb/common.txt\n # 4. /opt/SecLists/Discovery/Web-Content/common.txt\n # 5. Built-in ~130 high-value paths (fallback — limited coverage)\n # Recommended: apt install seclists OR set full path below\n wordlist_path: \"\"\n max_targets: 20 # Top N hosts to path-bruteforce (scored by importance)\n vhost_fuzzing: false # Discovery-only; enable manually when needed\n sqli:\n enabled: true # Uses focused Nuclei SQLi templates and parameter filtering\n max_urls: 50 # Max gf-filtered param URLs to test\n arjun:\n enabled: true # Hidden parameter discovery — feeds discovered params into dalfox\n max_targets: 15 # Top N hosts to discover hidden params on\n# AI Analysis\nanalysis:\n min_confidence: 0.85 # Findings below this require manual confirmation and are not reported\n js_analysis: true # AI-powered JS file analysis\n\n# Reporting\nreporting:\n include_poc: true\n severity_filter: [] # Optional actionable severities: critical, high, medium, low\n output_dir: \"./reports\"\n"}, "40c6b6a0f17cb90474d6a29d027c4e1c8db363a7": {"t": 1785748587.1757324, "n": 4264, "h": "8b4c2def2e615b21", "c": "# Bug Bounty AI Agent Configuration\n\n# ═══════════════════════════════════════════════════════\n# AI Provider Configuration\n# Supported: claude | deepseek | openai | openrouter | custom\n# ═══════════════════════════════════════════════════════\nai:\n provider: \"auto\" # auto = detect from .env (claude/deepseek/openai/openrouter)\n api_key: \"\" # Leave empty — key is read from .env automatically\n model: \"\" # Leave empty for provider default\n max_tokens: 2000\n base_url: \"\" # Only needed for openrouter/custom\n\n# Provider presets (just change 'provider' and 'api_key'):\n# ┌─────────────┬──────────────────────────┬────────────────────────────┬──────────────┐\n# │ Provider │ Model (default) │ Env Variable │ Cost/1M tok │\n# ├─────────────┼──────────────────────────┼────────────────────────────┼──────────────┤\n# │ claude │ claude-sonnet-4-20250514 │ ANTHROPIC_API_KEY │ $3/$15 │\n# │ deepseek │ deepseek-chat │ DEEPSEEK_API_KEY │ $0.28/$0.42 │\n# │ openai │ gpt-4o-mini │ OPENAI_API_KEY │ $0.15/$0.60 │\n# │ openrouter │ deepseek/deepseek-chat │ OPENROUTER_API_KEY │ varies │\n# │ custom │ (must specify) │ AI_API_KEY │ varies │\n# └─────────────┴──────────────────────────┴────────────────────────────┴──────────────┘\n\n# Legacy Claude config (backward compatibility - used if ai.api_key is empty)\nclaude:\n api_key: \"${ANTHROPIC_API_KEY}\"\n model: \"claude-sonnet-4-20250514\"\n max_tokens: 2000\n\n# C99.nl API Intelligence\nc99:\n api_key: \"${C99_API_KEY}\"\n enabled: true\n\n# Target Configuration\ntarget:\n domains: []\n excluded_subdomains: []\n\n# Reconnaissance Settings\nrecon:\n enabled: true\n timeout: 300 # seconds\n max_subdomains: 1000\n max_wayback_urls: 10000 # prevent OOM for large domains\n tools:\n subfinder: true\n assetfinder: true\n wayback: true\n katana: true # JS file extraction crawler\n cert_transparency: true\n\n# Scanning Settings\nscanning:\n enabled: true\n threads: 75 # Increased for better performance\n timeout: 1200 # 20 minutes for high-priority templates\n rate_limit: 100 # requests per second; lower this to match program rules\n tools:\n nuclei:\n enabled: true\n severity: [] # No filter - run all templates, findings keep their real severity\n templates_path: \"\" # Empty uses Nuclei's installed templates\n httpx:\n enabled: true\n follow_redirects: true\n status_code: true\n nmap:\n enabled: false # Open ports alone are reconnaissance, not confirmed vulnerabilities\n ports: \"21,22,25,53,80,110,143,443,445,993,995,3000,3306,5432,6379,8000,8080,8443,8888,9090,9200,27017\"\n fast_scan: true\n dalfox:\n enabled: true\n blind_url: \"\" # Optional: blind XSS callback URL (e.g. https://your-burp-collaborator)\n max_urls: 100 # Increased for better XSS coverage\n ffuf:\n enabled: true\n # Wordlist search order (first found wins):\n # 1. This value (if set)\n # 2. /usr/share/seclists/Discovery/Web-Content/common.txt\n # 3. /usr/share/wordlists/dirb/common.txt\n # 4. /opt/SecLists/Discovery/Web-Content/common.txt\n # 5. Built-in ~130 high-value paths (fallback — limited coverage)\n # Recommended: apt install seclists OR set full path below\n wordlist_path: \"\"\n max_targets: 20 # Top N hosts to path-bruteforce (scored by importance)\n vhost_fuzzing: false # Discovery-only; enable manually when needed\n sqli:\n enabled: true # Uses focused Nuclei SQLi templates and parameter filtering\n max_urls: 50 # Max gf-filtered param URLs to test\n arjun:\n enabled: true # Hidden parameter discovery — feeds discovered params into dalfox\n max_targets: 15 # Top N hosts to discover hidden params on\n# AI Analysis\nanalysis:\n min_confidence: 0.85 # Findings below this require manual confirmation and are not reported\n js_analysis: true # AI-powered JS file analysis\n\n# Reporting\nreporting:\n include_poc: true\n severity_filter: [] # Optional actionable severities: critical, high, medium, low\n output_dir: \"./reports\"\n"}, "ae7e4472b35fd48887c5cba7ad804a154354f441": {"t": 1785748588.5480673, "n": 4856, "h": "b7f4636728b815b8", "c": "# Bug Bounty AI Agent Configuration\n\n# ═══════════════════════════════════════════════════════\n# AI Provider Configuration\n# Supported: claude | deepseek | openai | openrouter | custom\n# ═══════════════════════════════════════════════════════\nai:\n provider: \"auto\" # auto = detect from .env (claude/deepseek/openai/openrouter)\n api_key: \"\" # Leave empty — key is read from .env automatically\n model: \"\" # Leave empty for provider default\n max_tokens: 2000\n base_url: \"\" # Only needed for openrouter/custom\n\n# Provider presets (just change 'provider' and 'api_key'):\n# ┌─────────────┬──────────────────────────┬────────────────────────────┬──────────────┐\n# │ Provider │ Model (default) │ Env Variable │ Cost/1M tok │\n# ├─────────────┼──────────────────────────┼────────────────────────────┼──────────────┤\n# │ claude │ claude-sonnet-4-20250514 │ ANTHROPIC_API_KEY │ $3/$15 │\n# │ deepseek │ deepseek-chat │ DEEPSEEK_API_KEY │ $0.28/$0.42 │\n# │ openai │ gpt-4o-mini │ OPENAI_API_KEY │ $0.15/$0.60 │\n# │ openrouter │ deepseek/deepseek-chat │ OPENROUTER_API_KEY │ varies │\n# │ custom │ (must specify) │ AI_API_KEY │ varies │\n# └─────────────┴──────────────────────────┴────────────────────────────┴──────────────┘\n\n# Legacy Claude config (backward compatibility - used if ai.api_key is empty)\nclaude:\n api_key: \"${ANTHROPIC_API_KEY}\"\n model: \"claude-sonnet-4-20250514\"\n max_tokens: 2000\n\n# C99.nl API Intelligence\nc99:\n api_key: \"${C99_API_KEY}\"\n enabled: true\n\n# Target Configuration\ntarget:\n domains: []\n excluded_subdomains: []\n scope: \"in-scope-only\"\n\n# Reconnaissance Settings\nrecon:\n enabled: true\n timeout: 300 # seconds\n max_subdomains: 1000\n max_wayback_urls: 10000 # prevent OOM for large domains\n tools:\n subfinder: true\n amass: false # Slower but comprehensive\n assetfinder: true\n github_dorking: true\n wayback: true\n katana: true # JS file extraction crawler\n\n# Scanning Settings\nscanning:\n enabled: true\n threads: 75 # Increased for better performance\n timeout: 1200 # 20 minutes for high-priority templates\n rate_limit: 250 # requests per second (faster scanning)\n tools:\n nuclei:\n enabled: true\n severity: [] # No filter - run all templates, findings keep their real severity\n templates_path: \"/root/nuclei-templates\" # ALL templates for maximum coverage\n httpx:\n enabled: true\n follow_redirects: true\n status_code: true\n nmap:\n enabled: true\n ports: \"21,22,25,53,80,110,143,443,445,993,995,3000,3306,5432,6379,8000,8080,8443,8888,9090,9200,27017\"\n fast_scan: true\n dalfox:\n enabled: true\n blind_url: \"\" # Optional: blind XSS callback URL (e.g. https://your-burp-collaborator)\n max_urls: 100 # Increased for better XSS coverage\n ffuf:\n enabled: true\n # Wordlist search order (first found wins):\n # 1. This value (if set)\n # 2. /usr/share/seclists/Discovery/Web-Content/common.txt\n # 3. /usr/share/wordlists/dirb/common.txt\n # 4. /opt/SecLists/Discovery/Web-Content/common.txt\n # 5. Built-in ~130 high-value paths (fallback — limited coverage)\n # Recommended: apt install seclists OR set full path below\n wordlist_path: \"\"\n max_targets: 20 # Top N hosts to path-bruteforce (scored by importance)\n vhost_fuzzing: true # Also fuzz virtual hosts via Host header — finds hidden internal apps\n sqli:\n enabled: true # SQLi via nuclei templates + gf-style param filtering (replaces sqlmap)\n # NOTE: sqlmap config kept for backward compatibility — set enabled:true above to activate\n sqlmap:\n enabled: true # Uses nuclei sqli templates internally (NOT blind sqlmap)\n max_urls: 50 # Max gf-filtered param URLs to test\n arjun:\n enabled: true # Hidden parameter discovery — feeds discovered params into dalfox\n max_targets: 15 # Top N hosts to discover hidden params on\n cors:\n enabled: true # CORS misconfiguration detection (fast, no external tools needed)\n\n# AI Analysis\nanalysis:\n enabled: true\n min_confidence: 0.7\n auto_validate: true\n false_positive_filter: true\n js_analysis: true # AI-powered JS file analysis\n\n# Reporting\nreporting:\n format: \"markdown\"\n language: \"ar\" # Arabic reports\n include_poc: true\n severity_filter: [] # No filter - include ALL severities (critical, high, medium, low, info)\n output_dir: \"./reports\"\n\n# Notifications\nnotifications:\n enabled: false\n slack_webhook: \"\"\n discord_webhook: \"\"\n\n# Logging\nlogging:\n level: \"info\" # debug, info, warn, error\n file: \"./logs/agent.log\"\n max_size_mb: 100\n"}, "3f611357d566a2993518503fbca9e09289ab9a32": {"t": 1785748589.6094909, "n": 4813, "h": "9445ea1629bff72d", "c": "# Bug Bounty AI Agent Configuration\n\n# ═══════════════════════════════════════════════════════\n# AI Provider Configuration\n# Supported: claude | deepseek | openai | openrouter | custom\n# ═══════════════════════════════════════════════════════\nai:\n provider: \"deepseek\" # Choose your AI provider\n api_key: \"${DEEPSEEK_API_KEY}\" # API key (or use env vars below)\n model: \"deepseek-chat\" # Leave empty for provider default\n max_tokens: 2000\n base_url: \"\" # Only needed for openrouter/custom\n\n# Provider presets (just change 'provider' and 'api_key'):\n# ┌─────────────┬──────────────────────────┬────────────────────────────┬──────────────┐\n# │ Provider │ Model (default) │ Env Variable │ Cost/1M tok │\n# ├─────────────┼──────────────────────────┼────────────────────────────┼──────────────┤\n# │ claude │ claude-sonnet-4-20250514 │ ANTHROPIC_API_KEY │ $3/$15 │\n# │ deepseek │ deepseek-chat │ DEEPSEEK_API_KEY │ $0.28/$0.42 │\n# │ openai │ gpt-4o-mini │ OPENAI_API_KEY │ $0.15/$0.60 │\n# │ openrouter │ deepseek/deepseek-chat │ OPENROUTER_API_KEY │ varies │\n# │ custom │ (must specify) │ AI_API_KEY │ varies │\n# └─────────────┴──────────────────────────┴────────────────────────────┴──────────────┘\n\n# Legacy Claude config (backward compatibility - used if ai.api_key is empty)\nclaude:\n api_key: \"${ANTHROPIC_API_KEY}\"\n model: \"claude-sonnet-4-20250514\"\n max_tokens: 2000\n\n# C99.nl API Intelligence\nc99:\n api_key: \"${C99_API_KEY}\"\n enabled: true\n\n# Target Configuration\ntarget:\n domains: []\n excluded_subdomains: []\n scope: \"in-scope-only\"\n\n# Reconnaissance Settings\nrecon:\n enabled: true\n timeout: 300 # seconds\n max_subdomains: 1000\n max_wayback_urls: 10000 # prevent OOM for large domains\n tools:\n subfinder: true\n amass: false # Slower but comprehensive\n assetfinder: true\n github_dorking: true\n wayback: true\n katana: true # JS file extraction crawler\n\n# Scanning Settings\nscanning:\n enabled: true\n threads: 75 # Increased for better performance\n timeout: 1200 # 20 minutes for high-priority templates\n rate_limit: 250 # requests per second (faster scanning)\n tools:\n nuclei:\n enabled: true\n severity: [] # No filter - run all templates, findings keep their real severity\n templates_path: \"/root/nuclei-templates\" # ALL templates for maximum coverage\n httpx:\n enabled: true\n follow_redirects: true\n status_code: true\n nmap:\n enabled: true\n ports: \"21,22,25,53,80,110,143,443,445,993,995,3000,3306,5432,6379,8000,8080,8443,8888,9090,9200,27017\"\n fast_scan: true\n dalfox:\n enabled: true\n blind_url: \"\" # Optional: blind XSS callback URL (e.g. https://your-burp-collaborator)\n max_urls: 100 # Increased for better XSS coverage\n ffuf:\n enabled: true\n # Wordlist search order (first found wins):\n # 1. This value (if set)\n # 2. /usr/share/seclists/Discovery/Web-Content/common.txt\n # 3. /usr/share/wordlists/dirb/common.txt\n # 4. /opt/SecLists/Discovery/Web-Content/common.txt\n # 5. Built-in ~130 high-value paths (fallback — limited coverage)\n # Recommended: apt install seclists OR set full path below\n wordlist_path: \"\"\n max_targets: 20 # Top N hosts to path-bruteforce (scored by importance)\n vhost_fuzzing: true # Also fuzz virtual hosts via Host header — finds hidden internal apps\n sqli:\n enabled: true # SQLi via nuclei templates + gf-style param filtering (replaces sqlmap)\n # NOTE: sqlmap config kept for backward compatibility — set enabled:true above to activate\n sqlmap:\n enabled: true # Uses nuclei sqli templates internally (NOT blind sqlmap)\n max_urls: 50 # Max gf-filtered param URLs to test\n arjun:\n enabled: true # Hidden parameter discovery — feeds discovered params into dalfox\n max_targets: 15 # Top N hosts to discover hidden params on\n cors:\n enabled: true # CORS misconfiguration detection (fast, no external tools needed)\n\n# AI Analysis\nanalysis:\n enabled: true\n min_confidence: 0.7\n auto_validate: true\n false_positive_filter: true\n js_analysis: true # AI-powered JS file analysis\n\n# Reporting\nreporting:\n format: \"markdown\"\n language: \"ar\" # Arabic reports\n include_poc: true\n severity_filter: [] # No filter - include ALL severities (critical, high, medium, low, info)\n output_dir: \"./reports\"\n\n# Notifications\nnotifications:\n enabled: false\n slack_webhook: \"\"\n discord_webhook: \"\"\n\n# Logging\nlogging:\n level: \"info\" # debug, info, warn, error\n file: \"./logs/agent.log\"\n max_size_mb: 100\n"}, "670da535366057a5e27666335a03e5d83a09e546": {"t": 1785748590.6443946, "n": 4475, "h": "0bf7ea5d4b9b4db5", "c": "# Bug Bounty AI Agent Configuration\n\n# ═══════════════════════════════════════════════════════\n# AI Provider Configuration\n# Supported: claude | deepseek | openai | openrouter | custom\n# ═══════════════════════════════════════════════════════\nai:\n provider: \"deepseek\" # Choose your AI provider\n api_key: \"${DEEPSEEK_API_KEY}\" # API key (or use env vars below)\n model: \"deepseek-chat\" # Leave empty for provider default\n max_tokens: 2000\n base_url: \"\" # Only needed for openrouter/custom\n\n# Provider presets (just change 'provider' and 'api_key'):\n# ┌─────────────┬──────────────────────────┬────────────────────────────┬──────────────┐\n# │ Provider │ Model (default) │ Env Variable │ Cost/1M tok │\n# ├─────────────┼──────────────────────────┼────────────────────────────┼──────────────┤\n# │ claude │ claude-sonnet-4-20250514 │ ANTHROPIC_API_KEY │ $3/$15 │\n# │ deepseek │ deepseek-chat │ DEEPSEEK_API_KEY │ $0.28/$0.42 │\n# │ openai │ gpt-4o-mini │ OPENAI_API_KEY │ $0.15/$0.60 │\n# │ openrouter │ deepseek/deepseek-chat │ OPENROUTER_API_KEY │ varies │\n# │ custom │ (must specify) │ AI_API_KEY │ varies │\n# └─────────────┴──────────────────────────┴────────────────────────────┴──────────────┘\n\n# Legacy Claude config (backward compatibility - used if ai.api_key is empty)\nclaude:\n api_key: \"${ANTHROPIC_API_KEY}\"\n model: \"claude-sonnet-4-20250514\"\n max_tokens: 2000\n\n# C99.nl API Intelligence\nc99:\n api_key: \"${C99_API_KEY}\"\n enabled: true\n\n# Target Configuration\ntarget:\n domains: []\n excluded_subdomains: []\n scope: \"in-scope-only\"\n\n# Reconnaissance Settings\nrecon:\n enabled: true\n timeout: 300 # seconds\n max_subdomains: 1000\n max_wayback_urls: 10000 # prevent OOM for large domains\n tools:\n subfinder: true\n amass: false # Slower but comprehensive\n assetfinder: true\n github_dorking: true\n wayback: true\n katana: true # JS file extraction crawler\n\n# Scanning Settings\nscanning:\n enabled: true\n threads: 75 # Increased for better performance\n timeout: 1200 # 20 minutes for high-priority templates\n rate_limit: 250 # requests per second (faster scanning)\n tools:\n nuclei:\n enabled: true\n severity: [] # No filter - run all templates, findings keep their real severity\n templates_path: \"/root/nuclei-templates\" # ALL templates for maximum coverage\n httpx:\n enabled: true\n follow_redirects: true\n status_code: true\n nmap:\n enabled: true\n ports: \"21,22,25,53,80,110,143,443,445,993,995,3000,3306,5432,6379,8000,8080,8443,8888,9090,9200,27017\"\n fast_scan: true\n dalfox:\n enabled: true\n blind_url: \"\" # Optional: blind XSS callback URL (e.g. https://your-burp-collaborator)\n max_urls: 100 # Increased for better XSS coverage\n ffuf:\n enabled: true\n wordlist_path: \"\" # Auto-detects SecLists/dirb/dirbuster, or specify full path\n max_targets: 20 # Top N hosts to path-bruteforce (scored by importance)\n vhost_fuzzing: true # Also fuzz virtual hosts via Host header — finds hidden internal apps\n sqli:\n enabled: true # SQLi via nuclei templates + gf-style param filtering (replaces sqlmap)\n # NOTE: sqlmap config kept for backward compatibility — set enabled:true above to activate\n sqlmap:\n enabled: true # Uses nuclei sqli templates internally (NOT blind sqlmap)\n max_urls: 50 # Max gf-filtered param URLs to test\n arjun:\n enabled: true # Hidden parameter discovery — feeds discovered params into dalfox\n max_targets: 15 # Top N hosts to discover hidden params on\n cors:\n enabled: true # CORS misconfiguration detection (fast, no external tools needed)\n\n# AI Analysis\nanalysis:\n enabled: true\n min_confidence: 0.7\n auto_validate: true\n false_positive_filter: true\n js_analysis: true # AI-powered JS file analysis\n\n# Reporting\nreporting:\n format: \"markdown\"\n language: \"ar\" # Arabic reports\n include_poc: true\n severity_filter: [] # No filter - include ALL severities (critical, high, medium, low, info)\n output_dir: \"./reports\"\n\n# Notifications\nnotifications:\n enabled: false\n slack_webhook: \"\"\n discord_webhook: \"\"\n\n# Logging\nlogging:\n level: \"info\" # debug, info, warn, error\n file: \"./logs/agent.log\"\n max_size_mb: 100\n"}, "546acc7456bd9880c3c45a233ae3a67d7a3b2f26": {"t": 1786068206.6240044, "n": 5101, "h": "79f3a150e4293b91", "c": "# Bug Bounty AI Agent Configuration\n\n# ═══════════════════════════════════════════════════════\n# AI Provider Configuration\n# Supported: auto | claude | deepseek | openai | openrouter | custom\n# ═══════════════════════════════════════════════════════\nai:\n provider: \"auto\" # Resolves only when exactly one provider-specific key is configured.\n # Or set claude/deepseek/openai/openrouter explicitly.\n api_key: \"\" # Leave empty — key is read from .env automatically\n model: \"\" # Empty selects the resolved provider's reviewed default.\n max_tokens: 8000 # Headroom for complete structured validation batches.\n base_url: \"\" # Hosted-provider endpoints are pinned in code.\n timeout: 300 # Per-request HTTP timeout (seconds). Raise for slow providers/large JS batches\n\n# Provider keys: ANTHROPIC_API_KEY, DEEPSEEK_API_KEY, OPENAI_API_KEY,\n# OPENROUTER_API_KEY, or AI_API_KEY for a fully configured custom endpoint.\n# Model IDs and pricing change over time; verify them with the provider before\n# overriding the release defaults.\n\n# Legacy Claude config (backward compatibility - used if ai.api_key is empty)\nclaude:\n api_key: \"${ANTHROPIC_API_KEY}\"\n model: \"claude-sonnet-5\"\n max_tokens: 2000\n\n# C99.nl API Intelligence\nc99:\n api_key: \"${C99_API_KEY}\"\n enabled: false # Opt in only after configuring a valid key.\n\n# Target Configuration\ntarget:\n domains: []\n excluded_subdomains: []\n\n# Optional authenticated testing. Use only accounts and sessions you own.\n# Values are expanded from environment variables, redacted from AI prompts,\n# reports, and logs, and omitted from external-tool environments. Avoid literal\n# credentials.\nauthentication:\n # Required when headers or cookies are configured. Only explicit HTTPS\n # origins are accepted. Credentials are used by the guarded internal JS\n # downloader and are never passed to external scanner processes.\n allowed_hosts: []\n # Example:\n # - \"https://app.example.com\"\n # - \"https://*.api.example.com:443\"\n headers: {}\n # Example:\n # Authorization: \"Bearer ${TARGET_TOKEN}\"\n cookies: {}\n # Example:\n # session: \"${TARGET_SESSION}\"\n\n# Reconnaissance Settings\nrecon:\n enabled: true\n timeout: 300 # seconds\n max_subdomains: 1000\n max_wayback_urls: 10000 # prevent OOM for large domains\n tools:\n subfinder: true\n assetfinder: true\n wayback: true\n katana: true # JS file extraction crawler\n cert_transparency: true\n\n# Scanning Settings\n#\n# Active scanners run SEQUENTIALLY (nuclei → dalfox), never in parallel. Running\n# several heavy scanners at once on a modest host exhausts memory and the OOM\n# killer terminates them mid-scan. One scanner at a time finishes reliably.\nscanning:\n enabled: true\n threads: 10 # Conservative default; raise only when the program explicitly permits it\n timeout: 1200 # 20-minute budget for the nuclei high-value scan (runs alone)\n # Global requests/sec ceiling used by scanners, Katana, and internal JS downloads.\n # Keep this at or below the bug-bounty program's published traffic limit.\n rate_limit: 25\n tools:\n nuclei:\n enabled: true\n severity: [] # Empty runs actionable severities: critical, high, medium, low\n # High-signal profile. \"misconfig\" (≈800 templates) was dropped: it nearly\n # doubled request volume without producing confirmed findings here. Add it\n # back if your host and the program's rate limits allow a longer scan.\n tags: [\"exposure\", \"takeover\", \"default-login\"]\n templates_path: \"\" # Empty uses Nuclei's installed templates\n httpx:\n enabled: true\n follow_redirects: true\n status_code: true\n dalfox:\n enabled: true\n blind_url: \"\" # Optional: blind XSS callback URL (e.g. https://your-burp-collaborator)\n max_urls: 75 # Parameterized URLs to fuzz for reflected XSS\n nmap:\n enabled: false # Open ports alone are reconnaissance, not confirmed vulnerabilities\n ports: \"21,22,25,53,80,110,143,443,445,993,995,3000,3306,5432,6379,8000,8080,8443,8888,9090,9200,27017\"\n fast_scan: true\n# AI Analysis\nanalysis:\n min_confidence: 0.85 # Findings below this require manual confirmation and are not reported\n js_analysis: true # AI-powered JS file analysis\n\n# Reporting\nreporting:\n include_poc: true\n severity_filter: [] # Optional actionable severities: critical, high, medium, low\n output_dir: \"./reports\"\n\n# Phase 1 — AI Attack-Surface Hunter (hypothesis engine)\n# Reasons over the recon-derived surface to produce ranked attack LEADS\n# (IDOR, access control, business logic, SSRF, injection, auth bypass) that\n# signature scanners cannot find. It NEVER sends a request to the target —\n# every hypothesis is a lead for manual verification.\nhunter:\n enabled: false # Opt in: outputs are unverified leads, never findings.\n max_hypotheses: 40 # cap on ranked leads returned/saved\n max_endpoints: 120 # cap on surface entries sent to the model (parameterized URLs prioritized)\n"}}, ".env.example": {"546acc7456bd9880c3c45a233ae3a67d7a3b2f26": {"t": 1786068204.9352536, "n": 1364, "h": "debf76e5e63abb6c", "c": "# ============================================================\n# HawkEye v2.3.0 — Environment Variables Template\n# Copy this file to .env and fill in your API keys\n# cp .env.example .env\n# chmod 600 .env\n# ============================================================\n\n# ─── AI Provider ─────────────────────────────────────────────\n# Fill exactly one provider key when config.yaml uses provider: auto. If you\n# keep multiple real keys, select the provider explicitly in config.yaml.\n\n# Anthropic Claude\nANTHROPIC_API_KEY=your-anthropic-api-key-here\n\n# DeepSeek\nDEEPSEEK_API_KEY=your-deepseek-api-key-here\n\n# OpenAI\nOPENAI_API_KEY=your-openai-api-key-here\n\n# OpenRouter (access multiple models)\nOPENROUTER_API_KEY=your-openrouter-api-key-here\n\n# Custom OpenAI-compatible endpoint. Configure provider, model, and base_url\n# together under ai: in config.yaml; custom cannot be selected by a flag alone.\nAI_API_KEY=your-custom-provider-api-key-here\n\n# ─── Optional Integrations ───────────────────────────────────\n# C99.nl — extra subdomain intelligence\nC99_API_KEY=your-c99-api-key-here\n\n# ─── Optional Authorized Target Session ─────────────────────\n# Uncomment only when authentication is configured in config.yaml.\n# Use a dedicated, low-privilege test account.\n# TARGET_TOKEN=replace-with-a-limited-test-token\n# TARGET_SESSION=replace-with-a-limited-test-session\n"}, "3bd24b7a821326bf3bf12b6a6ef1605b6b24c5b8": {"t": 1786068499.6128876, "n": 1364, "h": "debf76e5e63abb6c", "c": "# ============================================================\n# HawkEye v2.3.0 — Environment Variables Template\n# Copy this file to .env and fill in your API keys\n# cp .env.example .env\n# chmod 600 .env\n# ============================================================\n\n# ─── AI Provider ─────────────────────────────────────────────\n# Fill exactly one provider key when config.yaml uses provider: auto. If you\n# keep multiple real keys, select the provider explicitly in config.yaml.\n\n# Anthropic Claude\nANTHROPIC_API_KEY=your-anthropic-api-key-here\n\n# DeepSeek\nDEEPSEEK_API_KEY=your-deepseek-api-key-here\n\n# OpenAI\nOPENAI_API_KEY=your-openai-api-key-here\n\n# OpenRouter (access multiple models)\nOPENROUTER_API_KEY=your-openrouter-api-key-here\n\n# Custom OpenAI-compatible endpoint. Configure provider, model, and base_url\n# together under ai: in config.yaml; custom cannot be selected by a flag alone.\nAI_API_KEY=your-custom-provider-api-key-here\n\n# ─── Optional Integrations ───────────────────────────────────\n# C99.nl — extra subdomain intelligence\nC99_API_KEY=your-c99-api-key-here\n\n# ─── Optional Authorized Target Session ─────────────────────\n# Uncomment only when authentication is configured in config.yaml.\n# Use a dedicated, low-privilege test account.\n# TARGET_TOKEN=replace-with-a-limited-test-token\n# TARGET_SESSION=replace-with-a-limited-test-session\n"}}, "internal/config/config_test.go": {"546acc7456bd9880c3c45a233ae3a67d7a3b2f26": {"t": 1786068287.3329446, "n": 25417, "h": "921e91b17c7948a4", "c": "package config\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc validConfigForTest() Config {\n\treturn Config{\n\t\tAI: AIConfig{Provider: \"deepseek\", APIKey: \"test-key\", Model: \"test-model\", MaxTokens: 1000, Timeout: 30},\n\t\tRecon: ReconConfig{Timeout: 1},\n\t\tScanning: ScanningConfig{Threads: 1, RateLimit: 1, Timeout: 1},\n\t\tAnalysis: AnalysisConfig{MinConfidence: 0.85},\n\t\tHunter: HunterConfig{Enabled: true, MaxHypotheses: 40, MaxEndpoints: 120},\n\t}\n}\n\nfunc TestValidateConfigKeysRejectsUnknownFields(t *testing.T) {\n\tpath := filepath.Join(t.TempDir(), \"config.yaml\")\n\tif err := os.WriteFile(path, []byte(\"ai:\\n provider: deepseek\\n typo_field: true\\n\"), 0600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := validateConfigKeys(path); err == nil {\n\t\tt.Fatal(\"expected unknown config field to be rejected\")\n\t}\n}\n\nfunc TestLoadEnvFileRequiresPrivateRegularFile(t *testing.T) {\n\tt.Chdir(t.TempDir())\n\tif err := os.WriteFile(\".env\", []byte(\"AI_API_KEY=test-key\\n\"), 0o644); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := os.Chmod(\".env\", 0o644); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := loadEnvFile(map[string]bool{\"AI_API_KEY\": true}); err == nil || !strings.Contains(err.Error(), \"chmod 600\") {\n\t\tt.Fatalf(\"broad .env permissions were not rejected: %v\", err)\n\t}\n\n\tif err := os.Remove(\".env\"); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := os.Symlink(\"missing-secret-file\", \".env\"); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := loadEnvFile(map[string]bool{\"AI_API_KEY\": true}); err == nil || !strings.Contains(err.Error(), \"regular file\") {\n\t\tt.Fatalf(\"symlink .env was not rejected: %v\", err)\n\t}\n}\n\nfunc TestLoadEnvFileIgnoresProcessControlVariables(t *testing.T) {\n\tt.Chdir(t.TempDir())\n\tt.Setenv(\"PATH\", \"/safe/original/path\")\n\tt.Setenv(\"AI_API_KEY\", \"\")\n\tcontents := \"PATH=/attacker/bin\\nAI_API_KEY=file-api-key\\nUNRELATED=value\\n\"\n\tif err := os.WriteFile(\".env\", []byte(contents), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := loadEnvFile(map[string]bool{\"AI_API_KEY\": true}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif got := os.Getenv(\"PATH\"); got != \"/safe/original/path\" {\n\t\tt.Fatalf(\".env replaced PATH: %q\", got)\n\t}\n\tif got := os.Getenv(\"AI_API_KEY\"); got != \"file-api-key\" {\n\t\tt.Fatalf(\"allowed credential was not loaded: %q\", got)\n\t}\n}\n\nfunc TestAllowedEnvironmentNamesIncludesConfiguredAuthenticationReferences(t *testing.T) {\n\tpath := filepath.Join(t.TempDir(), \"config.yaml\")\n\tconfigText := \"authentication:\\n headers:\\n Authorization: 'Bearer ${TARGET_TOKEN}'\\n cookies:\\n session: '$TARGET_SESSION'\\n\"\n\tif err := os.WriteFile(path, []byte(configText), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tallowed, err := allowedEnvironmentNames(path)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tfor _, name := range []string{\"TARGET_TOKEN\", \"TARGET_SESSION\", \"AI_API_KEY\"} {\n\t\tif !allowed[name] {\n\t\t\tt.Errorf(\"configured environment variable %q was not allowlisted\", name)\n\t\t}\n\t}\n\tif allowed[\"PATH\"] {\n\t\tt.Fatal(\"PATH must never be allowlisted from .env\")\n\t}\n}\n\nfunc TestConfiguredEnvironmentReferencesRejectProcessControlNames(t *testing.T) {\n\tunsafeNames := []string{\n\t\t\"PATH\", \"HTTPS_PROXY\", \"SSL_CERT_FILE\", \"LD_PRELOAD\", \"GIT_ASKPASS\",\n\t\t\"SSH_ASKPASS\", \"BASH_ENV\", \"NODE_OPTIONS\", \"PYTHONPATH\", \"GODEBUG\",\n\t}\n\tfor _, name := range unsafeNames {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tpath := filepath.Join(t.TempDir(), \"config.yaml\")\n\t\t\tconfigText := \"authentication:\\n headers:\\n X-Secret: '${\" + name + \"}'\\n\"\n\t\t\tif err := os.WriteFile(path, []byte(configText), 0o600); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif _, err := allowedEnvironmentNames(path); err == nil || !strings.Contains(err.Error(), name) {\n\t\t\t\tt.Fatalf(\"unsafe environment reference %q was accepted: %v\", name, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCredentialEnvironmentNameConvention(t *testing.T) {\n\tfor _, name := range []string{\n\t\t\"AI_API_KEY\", \"TARGET_TOKEN\", \"TARGET_SESSION\", \"ACME_CLIENT_SECRET\", \"SERVICE_PASSWORD\",\n\t} {\n\t\tif !isSafeCredentialEnvironmentName(name) {\n\t\t\tt.Errorf(\"credential environment name %q was rejected\", name)\n\t\t}\n\t}\n\tfor _, name := range []string{\n\t\t\"target_token\", \"TARGET_VALUE\", \"HTTPS_PROXY\", \"SSL_CERT_FILE\", \"LD_ACCESS_TOKEN\",\n\t} {\n\t\tif isSafeCredentialEnvironmentName(name) {\n\t\t\tt.Errorf(\"unsafe or ambiguous environment name %q was accepted\", name)\n\t\t}\n\t}\n}\n\nfunc TestLoadEnvFileCannotBypassImmutableDenylist(t *testing.T) {\n\tt.Chdir(t.TempDir())\n\tt.Setenv(\"HTTPS_PROXY\", \"\")\n\tif err := os.WriteFile(\".env\", []byte(\"HTTPS_PROXY=https://proxy.attacker.example\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := loadEnvFile(map[string]bool{\"HTTPS_PROXY\": true}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif value := os.Getenv(\"HTTPS_PROXY\"); value != \"\" {\n\t\tt.Fatalf(\"process-control variable bypassed immutable denylist: %q\", value)\n\t}\n}\n\nfunc TestExternalToolEnvironmentIsCaseSensitiveAndUsesExactLocaleAllowlist(t *testing.T) {\n\tt.Setenv(\"path\", \"LOWERCASE_SECRET_MUST_NOT_LEAK\")\n\tt.Setenv(\"home\", \"LOWERCASE_HOME_SECRET_MUST_NOT_LEAK\")\n\tt.Setenv(\"LC_API_TOKEN\", \"LOCALE_PREFIX_SECRET_MUST_NOT_LEAK\")\n\tt.Setenv(\"LC_ALL\", \"C\")\n\n\tenvironment := ExternalToolEnvironment()\n\tjoined := strings.Join(environment, \"\\n\")\n\tfor _, secret := range []string{\n\t\t\"LOWERCASE_SECRET_MUST_NOT_LEAK\",\n\t\t\"LOWERCASE_HOME_SECRET_MUST_NOT_LEAK\",\n\t\t\"LOCALE_PREFIX_SECRET_MUST_NOT_LEAK\",\n\t} {\n\t\tif strings.Contains(joined, secret) {\n\t\t\tt.Fatalf(\"external tool inherited non-allowlisted environment secret %q: %v\", secret, environment)\n\t\t}\n\t}\n\tif !strings.Contains(joined, \"LC_ALL=C\") {\n\t\tt.Fatalf(\"required exact locale variable was omitted: %v\", environment)\n\t}\n}\n\nfunc TestValidateRejectsInvalidValues(t *testing.T) {\n\tcfg := Config{\n\t\tAI: AIConfig{Provider: \"unknown\", APIKey: \"key\"},\n\t\tRecon: ReconConfig{Timeout: 1},\n\t\tScanning: ScanningConfig{Threads: 1, RateLimit: 1},\n\t\tAnalysis: AnalysisConfig{MinConfidence: 0.7},\n\t}\n\tif err := cfg.Validate(); err == nil {\n\t\tt.Fatal(\"expected unsupported provider to be rejected\")\n\t}\n}\n\nfunc TestResolveAIConfigRejectsPlaceholderKeys(t *testing.T) {\n\tfor _, envVar := range []string{\"ANTHROPIC_API_KEY\", \"DEEPSEEK_API_KEY\", \"OPENAI_API_KEY\", \"OPENROUTER_API_KEY\"} {\n\t\tt.Setenv(envVar, \"\")\n\t}\n\n\tcfg := Config{\n\t\tAI: AIConfig{Provider: \"deepseek\", APIKey: \"your-deepseek-api-key-here\"},\n\t\tRecon: ReconConfig{Timeout: 1},\n\t\tScanning: ScanningConfig{Threads: 1, RateLimit: 1},\n\t\tAnalysis: AnalysisConfig{MinConfidence: 0.85},\n\t}\n\tcfg.ResolveAIConfig()\n\n\tif cfg.AI.APIKey != \"\" {\n\t\tt.Fatalf(\"placeholder API key was accepted: %q\", cfg.AI.APIKey)\n\t}\n\tif err := cfg.Validate(); err == nil {\n\t\tt.Fatal(\"expected placeholder API key to fail validation\")\n\t}\n}\n\nfunc TestResolveAIConfigDoesNotSwitchExplicitProvider(t *testing.T) {\n\tfor _, envVar := range []string{\"ANTHROPIC_API_KEY\", \"DEEPSEEK_API_KEY\", \"OPENAI_API_KEY\", \"OPENROUTER_API_KEY\"} {\n\t\tt.Setenv(envVar, \"\")\n\t}\n\tt.Setenv(\"ANTHROPIC_API_KEY\", \"foreign-provider-key\")\n\n\tcfg := validConfigForTest()\n\tcfg.AI = AIConfig{Provider: \"deepseek\"}\n\tcfg.ResolveAIConfig()\n\n\tif cfg.AI.Provider != \"deepseek\" {\n\t\tt.Fatalf(\"explicit provider changed to %q\", cfg.AI.Provider)\n\t}\n\tif cfg.AI.APIKey != \"\" {\n\t\tt.Fatal(\"explicit provider reused a different provider's API key\")\n\t}\n\tif err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), `provider \"deepseek\"`) {\n\t\tt.Fatalf(\"expected provider-specific missing key error, got %v\", err)\n\t}\n}\n\nfunc TestResolveAIConfigAutoDetectionIsOptIn(t *testing.T) {\n\tfor _, envVar := range []string{\"ANTHROPIC_API_KEY\", \"DEEPSEEK_API_KEY\", \"OPENAI_API_KEY\", \"OPENROUTER_API_KEY\"} {\n\t\tt.Setenv(envVar, \"\")\n\t}\n\tt.Setenv(\"OPENAI_API_KEY\", \"openai-test-key\")\n\n\tauto := validConfigForTest()\n\tauto.AI = AIConfig{Provider: \"auto\"}\n\tauto.ResolveAIConfig()\n\tif auto.AI.Provider != \"openai\" || auto.AI.APIKey != \"openai-test-key\" || auto.AI.Model != DefaultAIModel(\"openai\") {\n\t\tt.Fatalf(\"auto provider did not resolve coherently: %#v\", auto.AI)\n\t}\n\n\tomitted := validConfigForTest()\n\tomitted.AI = AIConfig{}\n\tomitted.ResolveAIConfig()\n\tif omitted.AI.Provider != \"\" || omitted.AI.APIKey != \"\" {\n\t\tt.Fatalf(\"omitted provider unexpectedly auto-detected another provider: %#v\", omitted.AI)\n\t}\n\tif err := omitted.Validate(); err == nil {\n\t\tt.Fatal(\"omitted provider must fail instead of selecting a vendor implicitly\")\n\t}\n}\n\nfunc TestResolveAIConfigRejectsAmbiguousAutoDetection(t *testing.T) {\n\tfor _, envVar := range []string{\"ANTHROPIC_API_KEY\", \"DEEPSEEK_API_KEY\", \"OPENAI_API_KEY\", \"OPENROUTER_API_KEY\"} {\n\t\tt.Setenv(envVar, \"\")\n\t}\n\tt.Setenv(\"ANTHROPIC_API_KEY\", \"anthropic-test-key\")\n\tt.Setenv(\"OPENAI_API_KEY\", \"openai-test-key\")\n\tcfg := validConfigForTest()\n\tcfg.AI = AIConfig{Provider: \"auto\"}\n\tcfg.ResolveAIConfig()\n\tif cfg.AI.Provider != \"auto\" || cfg.AI.APIKey != \"\" {\n\t\tt.Fatalf(\"ambiguous auto detection selected a provider: %#v\", cfg.AI)\n\t}\n\tif err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), \"could not detect\") {\n\t\tt.Fatalf(\"ambiguous auto detection must fail closed, got %v\", err)\n\t}\n}\n\nfunc TestValidateBoundsScannerAndHunterInputs(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tmutate func(*Config)\n\t}{\n\t\t{name: \"hunter hypotheses\", mutate: func(cfg *Config) { cfg.Hunter.MaxHypotheses = 201 }},\n\t\t{name: \"hunter endpoints\", mutate: func(cfg *Config) { cfg.Hunter.MaxEndpoints = 1001 }},\n\t\t{name: \"dalfox URLs\", mutate: func(cfg *Config) { cfg.Scanning.Tools.Dalfox.MaxURLs = 10001 }},\n\t\t{name: \"nmap port\", mutate: func(cfg *Config) { cfg.Scanning.Tools.Nmap.Ports = \"443,70000\" }},\n\t\t{name: \"nmap range\", mutate: func(cfg *Config) { cfg.Scanning.Tools.Nmap.Ports = \"100-10\" }},\n\t\t{name: \"nuclei tag\", mutate: func(cfg *Config) { cfg.Scanning.Tools.Nuclei.Tags = []string{\"valid\", \"bad,tag\"} }},\n\t\t{name: \"blind callback query\", mutate: func(cfg *Config) { cfg.Scanning.Tools.Dalfox.BlindURL = \"https://callback.example/?secret=value\" }},\n\t\t{name: \"blind callback private\", mutate: func(cfg *Config) { cfg.Scanning.Tools.Dalfox.BlindURL = \"https://127.0.0.1/callback\" }},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tcfg := validConfigForTest()\n\t\t\ttest.mutate(&cfg)\n\t\t\tif err := cfg.Validate(); err == nil {\n\t\t\t\tt.Fatal(\"unsafe or unbounded scanner configuration was accepted\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestResolveAIConfigRejectsGenericKeyWithAutoProvider(t *testing.T) {\n\tfor _, envVar := range []string{\"ANTHROPIC_API_KEY\", \"DEEPSEEK_API_KEY\", \"OPENAI_API_KEY\", \"OPENROUTER_API_KEY\"} {\n\t\tt.Setenv(envVar, \"\")\n\t}\n\tt.Setenv(\"ANTHROPIC_API_KEY\", \"anthropic-provider-key\")\n\n\tcfg := validConfigForTest()\n\tcfg.AI.Provider = \"auto\"\n\tcfg.AI.APIKey = \"generic-key-that-may-belong-to-openai\"\n\tcfg.ResolveAIConfig()\n\tif cfg.AI.Provider != \"auto\" {\n\t\tt.Fatalf(\"auto provider resolved despite ambiguous generic key: %#v\", cfg.AI)\n\t}\n\tif err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), \"must be empty\") {\n\t\tt.Fatalf(\"ambiguous auto provider/key pairing did not fail closed: %v\", err)\n\t}\n}\n\nfunc TestApplyAIOverridesResetsProviderDerivedDefaults(t *testing.T) {\n\tfor _, envVar := range []string{\"ANTHROPIC_API_KEY\", \"DEEPSEEK_API_KEY\", \"OPENAI_API_KEY\", \"OPENROUTER_API_KEY\"} {\n\t\tt.Setenv(envVar, \"\")\n\t}\n\tt.Setenv(\"OPENAI_API_KEY\", \"openai-test-key\")\n\n\tcfg := validConfigForTest()\n\tcfg.AI.Model = DefaultAIModel(\"deepseek\")\n\tcfg.AI.BaseURL = \"https://api.deepseek.com/v1\"\n\tif err := cfg.ApplyAIOverrides(\"openai\", \"\"); err != nil {\n\t\tt.Fatalf(\"apply provider override: %v\", err)\n\t}\n\tif cfg.AI.Provider != \"openai\" || cfg.AI.APIKey != \"openai-test-key\" {\n\t\tt.Fatalf(\"provider credentials were not resolved coherently: %#v\", cfg.AI)\n\t}\n\tif cfg.AI.Model != DefaultAIModel(\"openai\") || cfg.AI.BaseURL != \"https://api.openai.com/v1\" {\n\t\tt.Fatalf(\"provider defaults were not reset coherently: %#v\", cfg.AI)\n\t}\n\n\tif err := cfg.ApplyAIOverrides(\"\", \"gpt-explicit-model\"); err != nil {\n\t\tt.Fatalf(\"apply model override: %v\", err)\n\t}\n\tif cfg.AI.Model != \"gpt-explicit-model\" {\n\t\tt.Fatalf(\"explicit model override was not preserved: %q\", cfg.AI.Model)\n\t}\n}\n\nfunc TestValidateAIBaseURL(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tbaseURL string\n\t\twantErr bool\n\t}{\n\t\t{name: \"public HTTPS\", baseURL: \"https://ai.example.com/v1\"},\n\t\t{name: \"public HTTPS non-default port\", baseURL: \"https://ai.example.com:8443/v1\"},\n\t\t{name: \"plaintext\", baseURL: \"http://ai.example.com/v1\", wantErr: true},\n\t\t{name: \"loopback name\", baseURL: \"https://localhost/v1\", wantErr: true},\n\t\t{name: \"local suffix\", baseURL: \"https://model.dev.internal/v1\", wantErr: true},\n\t\t{name: \"IPv4 loopback\", baseURL: \"https://127.0.0.1/v1\", wantErr: true},\n\t\t{name: \"short IPv4 loopback\", baseURL: \"https://127.1/v1\", wantErr: true},\n\t\t{name: \"octal IPv4 loopback\", baseURL: \"https://0177.0.0.1/v1\", wantErr: true},\n\t\t{name: \"integer IPv4 loopback\", baseURL: \"https://2130706433/v1\", wantErr: true},\n\t\t{name: \"IPv4 private\", baseURL: \"https://10.10.1.2/v1\", wantErr: true},\n\t\t{name: \"metadata link local\", baseURL: \"https://169.254.169.254/v1\", wantErr: true},\n\t\t{name: \"IPv6 loopback\", baseURL: \"https://[::1]/v1\", wantErr: true},\n\t\t{name: \"userinfo\", baseURL: \"https://user:pass@ai.example.com/v1\", wantErr: true},\n\t\t{name: \"query\", baseURL: \"https://ai.example.com/v1?token=value\", wantErr: true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcfg := validConfigForTest()\n\t\t\tcfg.AI.Provider = \"custom\"\n\t\t\tcfg.AI.BaseURL = tt.baseURL\n\t\t\terr := cfg.Validate()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Fatalf(\"Validate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHostedProvidersPinOfficialBaseURLs(t *testing.T) {\n\ttests := []struct {\n\t\tprovider string\n\t\tofficial string\n\t}{\n\t\t{provider: \"openai\", official: \"https://api.openai.com/v1\"},\n\t\t{provider: \"deepseek\", official: \"https://api.deepseek.com/v1\"},\n\t\t{provider: \"openrouter\", official: \"https://openrouter.ai/api/v1\"},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.provider, func(t *testing.T) {\n\t\t\tcfg := validConfigForTest()\n\t\t\tcfg.AI.Provider = test.provider\n\t\t\tcfg.AI.BaseURL = test.official + \"/\"\n\t\t\tif err := cfg.Validate(); err != nil {\n\t\t\t\tt.Fatalf(\"official endpoint rejected: %v\", err)\n\t\t\t}\n\t\t\tif cfg.AI.BaseURL != test.official {\n\t\t\t\tt.Fatalf(\"official endpoint was not canonicalized: %q\", cfg.AI.BaseURL)\n\t\t\t}\n\n\t\t\tcfg.AI.BaseURL = \"https://gateway.attacker.example/v1\"\n\t\t\tif err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), `provider \"custom\"`) {\n\t\t\t\tt.Fatalf(\"arbitrary hosted-provider endpoint was accepted: %v\", err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHostedProviderRejectsEndpointConfusion(t *testing.T) {\n\tfor _, baseURL := range []string{\n\t\t\"https://api.openai.com.attacker.example/v1\",\n\t\t\"https://api.openai.com:444/v1\",\n\t\t\"https://api.openai.com/v1/../v1\",\n\t\t\"https://api.openai.com/%76%31\",\n\t\t\"https://api.openai.com/v1?\",\n\t} {\n\t\tcfg := validConfigForTest()\n\t\tcfg.AI.Provider = \"openai\"\n\t\tcfg.AI.BaseURL = baseURL\n\t\tif err := cfg.Validate(); err == nil {\n\t\t\tt.Errorf(\"confusable hosted-provider endpoint %q was accepted\", baseURL)\n\t\t}\n\t}\n\n\tcfg := validConfigForTest()\n\tcfg.AI.Provider = \"openai\"\n\tcfg.AI.BaseURL = \"HTTPS://API.OPENAI.COM:443/v1/\"\n\tif err := cfg.Validate(); err != nil || cfg.AI.BaseURL != \"https://api.openai.com/v1\" {\n\t\tt.Fatalf(\"equivalent official endpoint was not canonicalized: base_url=%q err=%v\", cfg.AI.BaseURL, err)\n\t}\n}\n\nfunc TestCustomProviderRetainsExplicitPublicEndpoint(t *testing.T) {\n\tcfg := validConfigForTest()\n\tcfg.AI.Provider = \"custom\"\n\tcfg.AI.BaseURL = \"https://gateway.example.com/compatible/v1\"\n\tif err := cfg.Validate(); err != nil {\n\t\tt.Fatalf(\"explicit custom endpoint rejected: %v\", err)\n\t}\n}\n\nfunc TestValidateCustomProviderRequiresModel(t *testing.T) {\n\tcfg := validConfigForTest()\n\tcfg.AI = AIConfig{Provider: \"custom\", APIKey: \"test-key\", BaseURL: \"https://ai.example.com/v1\"}\n\tif err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), \"model is required\") {\n\t\tt.Fatalf(\"expected missing custom model to be rejected, got %v\", err)\n\t}\n}\n\nfunc TestAuthenticationHeadersAndSecrets(t *testing.T) {\n\tt.Setenv(\"TARGET_TOKEN\", \"secret-token-value\")\n\tauth := AuthenticationConfig{\n\t\tAllowedHosts: []string{\"https://app.example.com\", \"https://*.api.example.com\"},\n\t\tHeaders: map[string]string{\"Authorization\": \"Bearer ${TARGET_TOKEN}\"},\n\t\tCookies: map[string]string{\"session\": \"${TARGET_TOKEN}\"},\n\t}\n\tauth.expandEnv()\n\n\theaders := auth.HeaderValuesForTargets(\"https://app.example.com/private\")\n\tif headers[\"Authorization\"] != \"Bearer secret-token-value\" ||\n\t\theaders[\"Cookie\"] != \"session=secret-token-value\" {\n\t\tt.Fatalf(\"unexpected authentication headers: %#v\", headers)\n\t}\n\tif len(auth.HeaderValuesForTargets(\"https://v1.api.example.com/private\")) == 0 ||\n\t\tlen(auth.HeaderValuesForTargets(\"https://api.example.com/private\")) != 0 ||\n\t\tlen(auth.HeaderValuesForTargets(\"https://evil.example.com/private\")) != 0 {\n\t\tt.Fatal(\"authentication host allowlist was not enforced\")\n\t}\n\n\tcfg := Config{Authentication: auth}\n\tredacted := cfg.Redact(\"Authorization: Bearer secret-token-value; session=secret-token-value\")\n\tif redacted != \"Authorization: [REDACTED]; session=[REDACTED]\" {\n\t\tt.Fatalf(\"secret was not redacted: %q\", redacted)\n\t}\n}\n\nfunc TestValidateRejectsShortConfiguredSecretsWithoutCorruptingOutput(t *testing.T) {\n\tcfg := Config{Authentication: AuthenticationConfig{\n\t\tHeaders: map[string]string{\"X-API-Key\": \"abc\"},\n\t\tCookies: map[string]string{\"sid\": \"xy\"},\n\t}}\n\tif got := cfg.Redact(\"banana/example\"); got != \"banana/example\" {\n\t\tt.Fatalf(\"invalid short values corrupted unrelated output: %q\", got)\n\t}\n\tvalid := validConfigForTest()\n\tvalid.Authentication = AuthenticationConfig{\n\t\tAllowedHosts: []string{\"https://app.example.com\"},\n\t\tHeaders: map[string]string{\"X-API-Key\": \"abc\"},\n\t}\n\tif err := valid.Validate(); err == nil || !strings.Contains(err.Error(), \"at least 4\") {\n\t\tt.Fatalf(\"short credential must be rejected, got %v\", err)\n\t}\n}\n\nfunc TestAuthenticationExactOriginRules(t *testing.T) {\n\tauth := AuthenticationConfig{AllowedHosts: []string{\n\t\t\"https://app.example.com\",\n\t\t\"https://api.example.com:8443\",\n\t\t\"https://*.svc.example.com\",\n\t}}\n\n\ttests := []struct {\n\t\ttarget string\n\t\twant bool\n\t}{\n\t\t{target: \"https://app.example.com/private\", want: true},\n\t\t{target: \"https://app.example.com:443/private\", want: true},\n\t\t{target: \"http://app.example.com/private\", want: false},\n\t\t{target: \"https://app.example.com:444/private\", want: false},\n\t\t{target: \"app.example.com\", want: false},\n\t\t{target: \"https://api.example.com:8443/private\", want: true},\n\t\t{target: \"https://api.example.com/private\", want: false},\n\t\t{target: \"https://v1.svc.example.com/private\", want: true},\n\t\t{target: \"http://v1.svc.example.com/private\", want: false},\n\t}\n\tfor _, tt := range tests {\n\t\tif got := auth.AllowsTarget(tt.target); got != tt.want {\n\t\t\tt.Errorf(\"AllowsTarget(%q) = %v, want %v\", tt.target, got, tt.want)\n\t\t}\n\t}\n}\n\nfunc TestValidateAuthenticationOriginRules(t *testing.T) {\n\tvalid := []string{\n\t\t\"https://app.example.com\",\n\t\t\"https://app.example.com:8443/\",\n\t\t\"https://*.api.example.com\",\n\t}\n\tfor _, rule := range valid {\n\t\tcfg := validConfigForTest()\n\t\tcfg.Authentication = AuthenticationConfig{\n\t\t\tAllowedHosts: []string{rule},\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer test-token\"},\n\t\t}\n\t\tif err := cfg.Validate(); err != nil {\n\t\t\tt.Errorf(\"valid authentication rule %q rejected: %v\", rule, err)\n\t\t}\n\t}\n\n\tinvalid := []string{\n\t\t\"ftp://app.example.com\",\n\t\t\"http://app.example.com\",\n\t\t\"legacy.example.com\",\n\t\t\"*.legacy.example.com\",\n\t\t\"https://user@app.example.com\",\n\t\t\"https://app.example.com/private\",\n\t\t\"https://app.example.com?token=value\",\n\t\t\"https://app.example.com:99999\",\n\t\t\"https://*.com\",\n\t\t\"https://*.co.uk\",\n\t\t\"https://*.localhost\",\n\t\t\"https://bad..example.com\",\n\t}\n\tfor _, rule := range invalid {\n\t\tcfg := validConfigForTest()\n\t\tcfg.Authentication = AuthenticationConfig{\n\t\t\tAllowedHosts: []string{rule},\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer test-token\"},\n\t\t}\n\t\tif err := cfg.Validate(); err == nil {\n\t\t\tt.Errorf(\"invalid authentication rule %q was accepted\", rule)\n\t\t}\n\t}\n}\n\nfunc TestValidateRejectsCaseInsensitiveAuthenticationHeaderDuplicates(t *testing.T) {\n\tcfg := validConfigForTest()\n\tcfg.Authentication = AuthenticationConfig{\n\t\tAllowedHosts: []string{\"https://app.example.com\"},\n\t\tHeaders: map[string]string{\n\t\t\t\"Authorization\": \"Bearer first-token\",\n\t\t\t\"authorization\": \"Bearer second-token\",\n\t\t},\n\t}\n\tif err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), \"duplicate\") {\n\t\tt.Fatalf(\"case-insensitive duplicate authentication headers were accepted: %v\", err)\n\t}\n}\n\nfunc TestValidateAuthenticationCredentialComponent(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\theader string\n\t\tvalue string\n\t\twantErr bool\n\t}{\n\t\t{name: \"valid bearer\", header: \"Authorization\", value: \"Bearer opaque-token\"},\n\t\t{name: \"short bearer credential\", header: \"Authorization\", value: \"Bearer x\", wantErr: true},\n\t\t{name: \"missing authorization scheme\", header: \"Authorization\", value: \"opaque-token\", wantErr: true},\n\t\t{name: \"placeholder bearer\", header: \"Authorization\", value: \"Bearer your-token\", wantErr: true},\n\t\t{name: \"valid custom credential\", header: \"X-API-Key\", value: \"opaque-key\"},\n\t\t{name: \"placeholder custom credential\", header: \"X-API-Key\", value: \"changeme\", wantErr: true},\n\t\t{name: \"proxy credential forbidden\", header: \"Proxy-Authorization\", value: \"Basic b3BhcXVl\", wantErr: true},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tcfg := validConfigForTest()\n\t\t\tcfg.Authentication = AuthenticationConfig{\n\t\t\t\tAllowedHosts: []string{\"https://app.example.com\"},\n\t\t\t\tHeaders: map[string]string{test.header: test.value},\n\t\t\t}\n\t\t\terr := cfg.Validate()\n\t\t\tif (err != nil) != test.wantErr {\n\t\t\t\tt.Fatalf(\"Validate() error = %v, wantErr %v\", err, test.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValidateRejectsCookieHeaderAndStructuredCookieCollision(t *testing.T) {\n\tcfg := validConfigForTest()\n\tcfg.Authentication = AuthenticationConfig{\n\t\tAllowedHosts: []string{\"https://app.example.com\"},\n\t\tHeaders: map[string]string{\"cookie\": \"raw=opaque-value\"},\n\t\tCookies: map[string]string{\"session\": \"structured-value\"},\n\t}\n\tif err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), \"conflicts\") {\n\t\tt.Fatalf(\"Cookie header/cookie map collision was accepted: %v\", err)\n\t}\n}\n\nfunc TestValidateRejectsUnsafeAuthenticationHeaders(t *testing.T) {\n\tcfg := Config{\n\t\tAI: AIConfig{Provider: \"deepseek\", APIKey: \"test-key\"},\n\t\tRecon: ReconConfig{Timeout: 1},\n\t\tScanning: ScanningConfig{Threads: 1, RateLimit: 1},\n\t\tAnalysis: AnalysisConfig{MinConfidence: 0.85},\n\t\tAuthentication: AuthenticationConfig{\n\t\t\tAllowedHosts: []string{\"https://example.com\"},\n\t\t\tHeaders: map[string]string{\"Host\": \"evil.example\"},\n\t\t},\n\t}\n\tif err := cfg.Validate(); err == nil {\n\t\tt.Fatal(\"expected unsafe authentication header to be rejected\")\n\t}\n}\n\nfunc TestValidateRequiresAuthenticationAllowedHosts(t *testing.T) {\n\tcfg := Config{\n\t\tAI: AIConfig{Provider: \"deepseek\", APIKey: \"test-key\"},\n\t\tRecon: ReconConfig{Timeout: 1},\n\t\tScanning: ScanningConfig{Threads: 1, RateLimit: 1},\n\t\tAnalysis: AnalysisConfig{MinConfidence: 0.85},\n\t\tAuthentication: AuthenticationConfig{Headers: map[string]string{\"Authorization\": \"Bearer token-value\"}},\n\t}\n\tif err := cfg.Validate(); err == nil {\n\t\tt.Fatal(\"expected authentication without allowed_hosts to be rejected\")\n\t}\n}\n\nfunc TestSaveAtomicallyReplacesExistingFileWithPrivateMode(t *testing.T) {\n\tdirectory := t.TempDir()\n\tpath := filepath.Join(directory, \"config.yaml\")\n\tif err := os.WriteFile(path, []byte(\"old: data\\n\"), 0o644); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := os.Chmod(path, 0o644); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tcfg := Config{AI: AIConfig{Provider: \"custom\", APIKey: \"opaque-api-key\"}}\n\tif err := cfg.Save(path); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tinfo, err := os.Stat(path)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif permissions := info.Mode().Perm(); permissions != 0o600 {\n\t\tt.Fatalf(\"saved config permissions = %o, want 600\", permissions)\n\t}\n\tcontents, err := os.ReadFile(path)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !strings.Contains(string(contents), \"opaque-api-key\") || strings.Contains(string(contents), \"old: data\") {\n\t\tt.Fatalf(\"saved config contents were not replaced: %q\", contents)\n\t}\n\tleftovers, err := filepath.Glob(filepath.Join(directory, \".config.yaml.tmp-*\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(leftovers) != 0 {\n\t\tt.Fatalf(\"temporary config files were left behind: %v\", leftovers)\n\t}\n}\n\nfunc TestSaveReplacesSymlinkWithoutOverwritingItsTarget(t *testing.T) {\n\tdirectory := t.TempDir()\n\tvictim := filepath.Join(directory, \"victim.txt\")\n\tpath := filepath.Join(directory, \"config.yaml\")\n\tif err := os.WriteFile(victim, []byte(\"do-not-touch\"), 0o644); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := os.Symlink(victim, path); err != nil {\n\t\tt.Skipf(\"symlinks unavailable: %v\", err)\n\t}\n\n\tcfg := Config{AI: AIConfig{APIKey: \"opaque-api-key\"}}\n\tif err := cfg.Save(path); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tvictimContents, err := os.ReadFile(victim)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif string(victimContents) != \"do-not-touch\" {\n\t\tt.Fatalf(\"Save followed and overwrote symlink target: %q\", victimContents)\n\t}\n\tinfo, err := os.Lstat(path)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !info.Mode().IsRegular() || info.Mode().Perm() != 0o600 {\n\t\tt.Fatalf(\"saved config did not replace symlink with private regular file: %v\", info.Mode())\n\t}\n}\n"}, "3bd24b7a821326bf3bf12b6a6ef1605b6b24c5b8": {"t": 1786068372.7326043, "n": 25417, "h": "921e91b17c7948a4", "c": "package config\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc validConfigForTest() Config {\n\treturn Config{\n\t\tAI: AIConfig{Provider: \"deepseek\", APIKey: \"test-key\", Model: \"test-model\", MaxTokens: 1000, Timeout: 30},\n\t\tRecon: ReconConfig{Timeout: 1},\n\t\tScanning: ScanningConfig{Threads: 1, RateLimit: 1, Timeout: 1},\n\t\tAnalysis: AnalysisConfig{MinConfidence: 0.85},\n\t\tHunter: HunterConfig{Enabled: true, MaxHypotheses: 40, MaxEndpoints: 120},\n\t}\n}\n\nfunc TestValidateConfigKeysRejectsUnknownFields(t *testing.T) {\n\tpath := filepath.Join(t.TempDir(), \"config.yaml\")\n\tif err := os.WriteFile(path, []byte(\"ai:\\n provider: deepseek\\n typo_field: true\\n\"), 0600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := validateConfigKeys(path); err == nil {\n\t\tt.Fatal(\"expected unknown config field to be rejected\")\n\t}\n}\n\nfunc TestLoadEnvFileRequiresPrivateRegularFile(t *testing.T) {\n\tt.Chdir(t.TempDir())\n\tif err := os.WriteFile(\".env\", []byte(\"AI_API_KEY=test-key\\n\"), 0o644); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := os.Chmod(\".env\", 0o644); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := loadEnvFile(map[string]bool{\"AI_API_KEY\": true}); err == nil || !strings.Contains(err.Error(), \"chmod 600\") {\n\t\tt.Fatalf(\"broad .env permissions were not rejected: %v\", err)\n\t}\n\n\tif err := os.Remove(\".env\"); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := os.Symlink(\"missing-secret-file\", \".env\"); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := loadEnvFile(map[string]bool{\"AI_API_KEY\": true}); err == nil || !strings.Contains(err.Error(), \"regular file\") {\n\t\tt.Fatalf(\"symlink .env was not rejected: %v\", err)\n\t}\n}\n\nfunc TestLoadEnvFileIgnoresProcessControlVariables(t *testing.T) {\n\tt.Chdir(t.TempDir())\n\tt.Setenv(\"PATH\", \"/safe/original/path\")\n\tt.Setenv(\"AI_API_KEY\", \"\")\n\tcontents := \"PATH=/attacker/bin\\nAI_API_KEY=file-api-key\\nUNRELATED=value\\n\"\n\tif err := os.WriteFile(\".env\", []byte(contents), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := loadEnvFile(map[string]bool{\"AI_API_KEY\": true}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif got := os.Getenv(\"PATH\"); got != \"/safe/original/path\" {\n\t\tt.Fatalf(\".env replaced PATH: %q\", got)\n\t}\n\tif got := os.Getenv(\"AI_API_KEY\"); got != \"file-api-key\" {\n\t\tt.Fatalf(\"allowed credential was not loaded: %q\", got)\n\t}\n}\n\nfunc TestAllowedEnvironmentNamesIncludesConfiguredAuthenticationReferences(t *testing.T) {\n\tpath := filepath.Join(t.TempDir(), \"config.yaml\")\n\tconfigText := \"authentication:\\n headers:\\n Authorization: 'Bearer ${TARGET_TOKEN}'\\n cookies:\\n session: '$TARGET_SESSION'\\n\"\n\tif err := os.WriteFile(path, []byte(configText), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tallowed, err := allowedEnvironmentNames(path)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tfor _, name := range []string{\"TARGET_TOKEN\", \"TARGET_SESSION\", \"AI_API_KEY\"} {\n\t\tif !allowed[name] {\n\t\t\tt.Errorf(\"configured environment variable %q was not allowlisted\", name)\n\t\t}\n\t}\n\tif allowed[\"PATH\"] {\n\t\tt.Fatal(\"PATH must never be allowlisted from .env\")\n\t}\n}\n\nfunc TestConfiguredEnvironmentReferencesRejectProcessControlNames(t *testing.T) {\n\tunsafeNames := []string{\n\t\t\"PATH\", \"HTTPS_PROXY\", \"SSL_CERT_FILE\", \"LD_PRELOAD\", \"GIT_ASKPASS\",\n\t\t\"SSH_ASKPASS\", \"BASH_ENV\", \"NODE_OPTIONS\", \"PYTHONPATH\", \"GODEBUG\",\n\t}\n\tfor _, name := range unsafeNames {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tpath := filepath.Join(t.TempDir(), \"config.yaml\")\n\t\t\tconfigText := \"authentication:\\n headers:\\n X-Secret: '${\" + name + \"}'\\n\"\n\t\t\tif err := os.WriteFile(path, []byte(configText), 0o600); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif _, err := allowedEnvironmentNames(path); err == nil || !strings.Contains(err.Error(), name) {\n\t\t\t\tt.Fatalf(\"unsafe environment reference %q was accepted: %v\", name, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCredentialEnvironmentNameConvention(t *testing.T) {\n\tfor _, name := range []string{\n\t\t\"AI_API_KEY\", \"TARGET_TOKEN\", \"TARGET_SESSION\", \"ACME_CLIENT_SECRET\", \"SERVICE_PASSWORD\",\n\t} {\n\t\tif !isSafeCredentialEnvironmentName(name) {\n\t\t\tt.Errorf(\"credential environment name %q was rejected\", name)\n\t\t}\n\t}\n\tfor _, name := range []string{\n\t\t\"target_token\", \"TARGET_VALUE\", \"HTTPS_PROXY\", \"SSL_CERT_FILE\", \"LD_ACCESS_TOKEN\",\n\t} {\n\t\tif isSafeCredentialEnvironmentName(name) {\n\t\t\tt.Errorf(\"unsafe or ambiguous environment name %q was accepted\", name)\n\t\t}\n\t}\n}\n\nfunc TestLoadEnvFileCannotBypassImmutableDenylist(t *testing.T) {\n\tt.Chdir(t.TempDir())\n\tt.Setenv(\"HTTPS_PROXY\", \"\")\n\tif err := os.WriteFile(\".env\", []byte(\"HTTPS_PROXY=https://proxy.attacker.example\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := loadEnvFile(map[string]bool{\"HTTPS_PROXY\": true}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif value := os.Getenv(\"HTTPS_PROXY\"); value != \"\" {\n\t\tt.Fatalf(\"process-control variable bypassed immutable denylist: %q\", value)\n\t}\n}\n\nfunc TestExternalToolEnvironmentIsCaseSensitiveAndUsesExactLocaleAllowlist(t *testing.T) {\n\tt.Setenv(\"path\", \"LOWERCASE_SECRET_MUST_NOT_LEAK\")\n\tt.Setenv(\"home\", \"LOWERCASE_HOME_SECRET_MUST_NOT_LEAK\")\n\tt.Setenv(\"LC_API_TOKEN\", \"LOCALE_PREFIX_SECRET_MUST_NOT_LEAK\")\n\tt.Setenv(\"LC_ALL\", \"C\")\n\n\tenvironment := ExternalToolEnvironment()\n\tjoined := strings.Join(environment, \"\\n\")\n\tfor _, secret := range []string{\n\t\t\"LOWERCASE_SECRET_MUST_NOT_LEAK\",\n\t\t\"LOWERCASE_HOME_SECRET_MUST_NOT_LEAK\",\n\t\t\"LOCALE_PREFIX_SECRET_MUST_NOT_LEAK\",\n\t} {\n\t\tif strings.Contains(joined, secret) {\n\t\t\tt.Fatalf(\"external tool inherited non-allowlisted environment secret %q: %v\", secret, environment)\n\t\t}\n\t}\n\tif !strings.Contains(joined, \"LC_ALL=C\") {\n\t\tt.Fatalf(\"required exact locale variable was omitted: %v\", environment)\n\t}\n}\n\nfunc TestValidateRejectsInvalidValues(t *testing.T) {\n\tcfg := Config{\n\t\tAI: AIConfig{Provider: \"unknown\", APIKey: \"key\"},\n\t\tRecon: ReconConfig{Timeout: 1},\n\t\tScanning: ScanningConfig{Threads: 1, RateLimit: 1},\n\t\tAnalysis: AnalysisConfig{MinConfidence: 0.7},\n\t}\n\tif err := cfg.Validate(); err == nil {\n\t\tt.Fatal(\"expected unsupported provider to be rejected\")\n\t}\n}\n\nfunc TestResolveAIConfigRejectsPlaceholderKeys(t *testing.T) {\n\tfor _, envVar := range []string{\"ANTHROPIC_API_KEY\", \"DEEPSEEK_API_KEY\", \"OPENAI_API_KEY\", \"OPENROUTER_API_KEY\"} {\n\t\tt.Setenv(envVar, \"\")\n\t}\n\n\tcfg := Config{\n\t\tAI: AIConfig{Provider: \"deepseek\", APIKey: \"your-deepseek-api-key-here\"},\n\t\tRecon: ReconConfig{Timeout: 1},\n\t\tScanning: ScanningConfig{Threads: 1, RateLimit: 1},\n\t\tAnalysis: AnalysisConfig{MinConfidence: 0.85},\n\t}\n\tcfg.ResolveAIConfig()\n\n\tif cfg.AI.APIKey != \"\" {\n\t\tt.Fatalf(\"placeholder API key was accepted: %q\", cfg.AI.APIKey)\n\t}\n\tif err := cfg.Validate(); err == nil {\n\t\tt.Fatal(\"expected placeholder API key to fail validation\")\n\t}\n}\n\nfunc TestResolveAIConfigDoesNotSwitchExplicitProvider(t *testing.T) {\n\tfor _, envVar := range []string{\"ANTHROPIC_API_KEY\", \"DEEPSEEK_API_KEY\", \"OPENAI_API_KEY\", \"OPENROUTER_API_KEY\"} {\n\t\tt.Setenv(envVar, \"\")\n\t}\n\tt.Setenv(\"ANTHROPIC_API_KEY\", \"foreign-provider-key\")\n\n\tcfg := validConfigForTest()\n\tcfg.AI = AIConfig{Provider: \"deepseek\"}\n\tcfg.ResolveAIConfig()\n\n\tif cfg.AI.Provider != \"deepseek\" {\n\t\tt.Fatalf(\"explicit provider changed to %q\", cfg.AI.Provider)\n\t}\n\tif cfg.AI.APIKey != \"\" {\n\t\tt.Fatal(\"explicit provider reused a different provider's API key\")\n\t}\n\tif err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), `provider \"deepseek\"`) {\n\t\tt.Fatalf(\"expected provider-specific missing key error, got %v\", err)\n\t}\n}\n\nfunc TestResolveAIConfigAutoDetectionIsOptIn(t *testing.T) {\n\tfor _, envVar := range []string{\"ANTHROPIC_API_KEY\", \"DEEPSEEK_API_KEY\", \"OPENAI_API_KEY\", \"OPENROUTER_API_KEY\"} {\n\t\tt.Setenv(envVar, \"\")\n\t}\n\tt.Setenv(\"OPENAI_API_KEY\", \"openai-test-key\")\n\n\tauto := validConfigForTest()\n\tauto.AI = AIConfig{Provider: \"auto\"}\n\tauto.ResolveAIConfig()\n\tif auto.AI.Provider != \"openai\" || auto.AI.APIKey != \"openai-test-key\" || auto.AI.Model != DefaultAIModel(\"openai\") {\n\t\tt.Fatalf(\"auto provider did not resolve coherently: %#v\", auto.AI)\n\t}\n\n\tomitted := validConfigForTest()\n\tomitted.AI = AIConfig{}\n\tomitted.ResolveAIConfig()\n\tif omitted.AI.Provider != \"\" || omitted.AI.APIKey != \"\" {\n\t\tt.Fatalf(\"omitted provider unexpectedly auto-detected another provider: %#v\", omitted.AI)\n\t}\n\tif err := omitted.Validate(); err == nil {\n\t\tt.Fatal(\"omitted provider must fail instead of selecting a vendor implicitly\")\n\t}\n}\n\nfunc TestResolveAIConfigRejectsAmbiguousAutoDetection(t *testing.T) {\n\tfor _, envVar := range []string{\"ANTHROPIC_API_KEY\", \"DEEPSEEK_API_KEY\", \"OPENAI_API_KEY\", \"OPENROUTER_API_KEY\"} {\n\t\tt.Setenv(envVar, \"\")\n\t}\n\tt.Setenv(\"ANTHROPIC_API_KEY\", \"anthropic-test-key\")\n\tt.Setenv(\"OPENAI_API_KEY\", \"openai-test-key\")\n\tcfg := validConfigForTest()\n\tcfg.AI = AIConfig{Provider: \"auto\"}\n\tcfg.ResolveAIConfig()\n\tif cfg.AI.Provider != \"auto\" || cfg.AI.APIKey != \"\" {\n\t\tt.Fatalf(\"ambiguous auto detection selected a provider: %#v\", cfg.AI)\n\t}\n\tif err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), \"could not detect\") {\n\t\tt.Fatalf(\"ambiguous auto detection must fail closed, got %v\", err)\n\t}\n}\n\nfunc TestValidateBoundsScannerAndHunterInputs(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tmutate func(*Config)\n\t}{\n\t\t{name: \"hunter hypotheses\", mutate: func(cfg *Config) { cfg.Hunter.MaxHypotheses = 201 }},\n\t\t{name: \"hunter endpoints\", mutate: func(cfg *Config) { cfg.Hunter.MaxEndpoints = 1001 }},\n\t\t{name: \"dalfox URLs\", mutate: func(cfg *Config) { cfg.Scanning.Tools.Dalfox.MaxURLs = 10001 }},\n\t\t{name: \"nmap port\", mutate: func(cfg *Config) { cfg.Scanning.Tools.Nmap.Ports = \"443,70000\" }},\n\t\t{name: \"nmap range\", mutate: func(cfg *Config) { cfg.Scanning.Tools.Nmap.Ports = \"100-10\" }},\n\t\t{name: \"nuclei tag\", mutate: func(cfg *Config) { cfg.Scanning.Tools.Nuclei.Tags = []string{\"valid\", \"bad,tag\"} }},\n\t\t{name: \"blind callback query\", mutate: func(cfg *Config) { cfg.Scanning.Tools.Dalfox.BlindURL = \"https://callback.example/?secret=value\" }},\n\t\t{name: \"blind callback private\", mutate: func(cfg *Config) { cfg.Scanning.Tools.Dalfox.BlindURL = \"https://127.0.0.1/callback\" }},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tcfg := validConfigForTest()\n\t\t\ttest.mutate(&cfg)\n\t\t\tif err := cfg.Validate(); err == nil {\n\t\t\t\tt.Fatal(\"unsafe or unbounded scanner configuration was accepted\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestResolveAIConfigRejectsGenericKeyWithAutoProvider(t *testing.T) {\n\tfor _, envVar := range []string{\"ANTHROPIC_API_KEY\", \"DEEPSEEK_API_KEY\", \"OPENAI_API_KEY\", \"OPENROUTER_API_KEY\"} {\n\t\tt.Setenv(envVar, \"\")\n\t}\n\tt.Setenv(\"ANTHROPIC_API_KEY\", \"anthropic-provider-key\")\n\n\tcfg := validConfigForTest()\n\tcfg.AI.Provider = \"auto\"\n\tcfg.AI.APIKey = \"generic-key-that-may-belong-to-openai\"\n\tcfg.ResolveAIConfig()\n\tif cfg.AI.Provider != \"auto\" {\n\t\tt.Fatalf(\"auto provider resolved despite ambiguous generic key: %#v\", cfg.AI)\n\t}\n\tif err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), \"must be empty\") {\n\t\tt.Fatalf(\"ambiguous auto provider/key pairing did not fail closed: %v\", err)\n\t}\n}\n\nfunc TestApplyAIOverridesResetsProviderDerivedDefaults(t *testing.T) {\n\tfor _, envVar := range []string{\"ANTHROPIC_API_KEY\", \"DEEPSEEK_API_KEY\", \"OPENAI_API_KEY\", \"OPENROUTER_API_KEY\"} {\n\t\tt.Setenv(envVar, \"\")\n\t}\n\tt.Setenv(\"OPENAI_API_KEY\", \"openai-test-key\")\n\n\tcfg := validConfigForTest()\n\tcfg.AI.Model = DefaultAIModel(\"deepseek\")\n\tcfg.AI.BaseURL = \"https://api.deepseek.com/v1\"\n\tif err := cfg.ApplyAIOverrides(\"openai\", \"\"); err != nil {\n\t\tt.Fatalf(\"apply provider override: %v\", err)\n\t}\n\tif cfg.AI.Provider != \"openai\" || cfg.AI.APIKey != \"openai-test-key\" {\n\t\tt.Fatalf(\"provider credentials were not resolved coherently: %#v\", cfg.AI)\n\t}\n\tif cfg.AI.Model != DefaultAIModel(\"openai\") || cfg.AI.BaseURL != \"https://api.openai.com/v1\" {\n\t\tt.Fatalf(\"provider defaults were not reset coherently: %#v\", cfg.AI)\n\t}\n\n\tif err := cfg.ApplyAIOverrides(\"\", \"gpt-explicit-model\"); err != nil {\n\t\tt.Fatalf(\"apply model override: %v\", err)\n\t}\n\tif cfg.AI.Model != \"gpt-explicit-model\" {\n\t\tt.Fatalf(\"explicit model override was not preserved: %q\", cfg.AI.Model)\n\t}\n}\n\nfunc TestValidateAIBaseURL(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tbaseURL string\n\t\twantErr bool\n\t}{\n\t\t{name: \"public HTTPS\", baseURL: \"https://ai.example.com/v1\"},\n\t\t{name: \"public HTTPS non-default port\", baseURL: \"https://ai.example.com:8443/v1\"},\n\t\t{name: \"plaintext\", baseURL: \"http://ai.example.com/v1\", wantErr: true},\n\t\t{name: \"loopback name\", baseURL: \"https://localhost/v1\", wantErr: true},\n\t\t{name: \"local suffix\", baseURL: \"https://model.dev.internal/v1\", wantErr: true},\n\t\t{name: \"IPv4 loopback\", baseURL: \"https://127.0.0.1/v1\", wantErr: true},\n\t\t{name: \"short IPv4 loopback\", baseURL: \"https://127.1/v1\", wantErr: true},\n\t\t{name: \"octal IPv4 loopback\", baseURL: \"https://0177.0.0.1/v1\", wantErr: true},\n\t\t{name: \"integer IPv4 loopback\", baseURL: \"https://2130706433/v1\", wantErr: true},\n\t\t{name: \"IPv4 private\", baseURL: \"https://10.10.1.2/v1\", wantErr: true},\n\t\t{name: \"metadata link local\", baseURL: \"https://169.254.169.254/v1\", wantErr: true},\n\t\t{name: \"IPv6 loopback\", baseURL: \"https://[::1]/v1\", wantErr: true},\n\t\t{name: \"userinfo\", baseURL: \"https://user:pass@ai.example.com/v1\", wantErr: true},\n\t\t{name: \"query\", baseURL: \"https://ai.example.com/v1?token=value\", wantErr: true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcfg := validConfigForTest()\n\t\t\tcfg.AI.Provider = \"custom\"\n\t\t\tcfg.AI.BaseURL = tt.baseURL\n\t\t\terr := cfg.Validate()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Fatalf(\"Validate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHostedProvidersPinOfficialBaseURLs(t *testing.T) {\n\ttests := []struct {\n\t\tprovider string\n\t\tofficial string\n\t}{\n\t\t{provider: \"openai\", official: \"https://api.openai.com/v1\"},\n\t\t{provider: \"deepseek\", official: \"https://api.deepseek.com/v1\"},\n\t\t{provider: \"openrouter\", official: \"https://openrouter.ai/api/v1\"},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.provider, func(t *testing.T) {\n\t\t\tcfg := validConfigForTest()\n\t\t\tcfg.AI.Provider = test.provider\n\t\t\tcfg.AI.BaseURL = test.official + \"/\"\n\t\t\tif err := cfg.Validate(); err != nil {\n\t\t\t\tt.Fatalf(\"official endpoint rejected: %v\", err)\n\t\t\t}\n\t\t\tif cfg.AI.BaseURL != test.official {\n\t\t\t\tt.Fatalf(\"official endpoint was not canonicalized: %q\", cfg.AI.BaseURL)\n\t\t\t}\n\n\t\t\tcfg.AI.BaseURL = \"https://gateway.attacker.example/v1\"\n\t\t\tif err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), `provider \"custom\"`) {\n\t\t\t\tt.Fatalf(\"arbitrary hosted-provider endpoint was accepted: %v\", err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHostedProviderRejectsEndpointConfusion(t *testing.T) {\n\tfor _, baseURL := range []string{\n\t\t\"https://api.openai.com.attacker.example/v1\",\n\t\t\"https://api.openai.com:444/v1\",\n\t\t\"https://api.openai.com/v1/../v1\",\n\t\t\"https://api.openai.com/%76%31\",\n\t\t\"https://api.openai.com/v1?\",\n\t} {\n\t\tcfg := validConfigForTest()\n\t\tcfg.AI.Provider = \"openai\"\n\t\tcfg.AI.BaseURL = baseURL\n\t\tif err := cfg.Validate(); err == nil {\n\t\t\tt.Errorf(\"confusable hosted-provider endpoint %q was accepted\", baseURL)\n\t\t}\n\t}\n\n\tcfg := validConfigForTest()\n\tcfg.AI.Provider = \"openai\"\n\tcfg.AI.BaseURL = \"HTTPS://API.OPENAI.COM:443/v1/\"\n\tif err := cfg.Validate(); err != nil || cfg.AI.BaseURL != \"https://api.openai.com/v1\" {\n\t\tt.Fatalf(\"equivalent official endpoint was not canonicalized: base_url=%q err=%v\", cfg.AI.BaseURL, err)\n\t}\n}\n\nfunc TestCustomProviderRetainsExplicitPublicEndpoint(t *testing.T) {\n\tcfg := validConfigForTest()\n\tcfg.AI.Provider = \"custom\"\n\tcfg.AI.BaseURL = \"https://gateway.example.com/compatible/v1\"\n\tif err := cfg.Validate(); err != nil {\n\t\tt.Fatalf(\"explicit custom endpoint rejected: %v\", err)\n\t}\n}\n\nfunc TestValidateCustomProviderRequiresModel(t *testing.T) {\n\tcfg := validConfigForTest()\n\tcfg.AI = AIConfig{Provider: \"custom\", APIKey: \"test-key\", BaseURL: \"https://ai.example.com/v1\"}\n\tif err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), \"model is required\") {\n\t\tt.Fatalf(\"expected missing custom model to be rejected, got %v\", err)\n\t}\n}\n\nfunc TestAuthenticationHeadersAndSecrets(t *testing.T) {\n\tt.Setenv(\"TARGET_TOKEN\", \"secret-token-value\")\n\tauth := AuthenticationConfig{\n\t\tAllowedHosts: []string{\"https://app.example.com\", \"https://*.api.example.com\"},\n\t\tHeaders: map[string]string{\"Authorization\": \"Bearer ${TARGET_TOKEN}\"},\n\t\tCookies: map[string]string{\"session\": \"${TARGET_TOKEN}\"},\n\t}\n\tauth.expandEnv()\n\n\theaders := auth.HeaderValuesForTargets(\"https://app.example.com/private\")\n\tif headers[\"Authorization\"] != \"Bearer secret-token-value\" ||\n\t\theaders[\"Cookie\"] != \"session=secret-token-value\" {\n\t\tt.Fatalf(\"unexpected authentication headers: %#v\", headers)\n\t}\n\tif len(auth.HeaderValuesForTargets(\"https://v1.api.example.com/private\")) == 0 ||\n\t\tlen(auth.HeaderValuesForTargets(\"https://api.example.com/private\")) != 0 ||\n\t\tlen(auth.HeaderValuesForTargets(\"https://evil.example.com/private\")) != 0 {\n\t\tt.Fatal(\"authentication host allowlist was not enforced\")\n\t}\n\n\tcfg := Config{Authentication: auth}\n\tredacted := cfg.Redact(\"Authorization: Bearer secret-token-value; session=secret-token-value\")\n\tif redacted != \"Authorization: [REDACTED]; session=[REDACTED]\" {\n\t\tt.Fatalf(\"secret was not redacted: %q\", redacted)\n\t}\n}\n\nfunc TestValidateRejectsShortConfiguredSecretsWithoutCorruptingOutput(t *testing.T) {\n\tcfg := Config{Authentication: AuthenticationConfig{\n\t\tHeaders: map[string]string{\"X-API-Key\": \"abc\"},\n\t\tCookies: map[string]string{\"sid\": \"xy\"},\n\t}}\n\tif got := cfg.Redact(\"banana/example\"); got != \"banana/example\" {\n\t\tt.Fatalf(\"invalid short values corrupted unrelated output: %q\", got)\n\t}\n\tvalid := validConfigForTest()\n\tvalid.Authentication = AuthenticationConfig{\n\t\tAllowedHosts: []string{\"https://app.example.com\"},\n\t\tHeaders: map[string]string{\"X-API-Key\": \"abc\"},\n\t}\n\tif err := valid.Validate(); err == nil || !strings.Contains(err.Error(), \"at least 4\") {\n\t\tt.Fatalf(\"short credential must be rejected, got %v\", err)\n\t}\n}\n\nfunc TestAuthenticationExactOriginRules(t *testing.T) {\n\tauth := AuthenticationConfig{AllowedHosts: []string{\n\t\t\"https://app.example.com\",\n\t\t\"https://api.example.com:8443\",\n\t\t\"https://*.svc.example.com\",\n\t}}\n\n\ttests := []struct {\n\t\ttarget string\n\t\twant bool\n\t}{\n\t\t{target: \"https://app.example.com/private\", want: true},\n\t\t{target: \"https://app.example.com:443/private\", want: true},\n\t\t{target: \"http://app.example.com/private\", want: false},\n\t\t{target: \"https://app.example.com:444/private\", want: false},\n\t\t{target: \"app.example.com\", want: false},\n\t\t{target: \"https://api.example.com:8443/private\", want: true},\n\t\t{target: \"https://api.example.com/private\", want: false},\n\t\t{target: \"https://v1.svc.example.com/private\", want: true},\n\t\t{target: \"http://v1.svc.example.com/private\", want: false},\n\t}\n\tfor _, tt := range tests {\n\t\tif got := auth.AllowsTarget(tt.target); got != tt.want {\n\t\t\tt.Errorf(\"AllowsTarget(%q) = %v, want %v\", tt.target, got, tt.want)\n\t\t}\n\t}\n}\n\nfunc TestValidateAuthenticationOriginRules(t *testing.T) {\n\tvalid := []string{\n\t\t\"https://app.example.com\",\n\t\t\"https://app.example.com:8443/\",\n\t\t\"https://*.api.example.com\",\n\t}\n\tfor _, rule := range valid {\n\t\tcfg := validConfigForTest()\n\t\tcfg.Authentication = AuthenticationConfig{\n\t\t\tAllowedHosts: []string{rule},\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer test-token\"},\n\t\t}\n\t\tif err := cfg.Validate(); err != nil {\n\t\t\tt.Errorf(\"valid authentication rule %q rejected: %v\", rule, err)\n\t\t}\n\t}\n\n\tinvalid := []string{\n\t\t\"ftp://app.example.com\",\n\t\t\"http://app.example.com\",\n\t\t\"legacy.example.com\",\n\t\t\"*.legacy.example.com\",\n\t\t\"https://user@app.example.com\",\n\t\t\"https://app.example.com/private\",\n\t\t\"https://app.example.com?token=value\",\n\t\t\"https://app.example.com:99999\",\n\t\t\"https://*.com\",\n\t\t\"https://*.co.uk\",\n\t\t\"https://*.localhost\",\n\t\t\"https://bad..example.com\",\n\t}\n\tfor _, rule := range invalid {\n\t\tcfg := validConfigForTest()\n\t\tcfg.Authentication = AuthenticationConfig{\n\t\t\tAllowedHosts: []string{rule},\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer test-token\"},\n\t\t}\n\t\tif err := cfg.Validate(); err == nil {\n\t\t\tt.Errorf(\"invalid authentication rule %q was accepted\", rule)\n\t\t}\n\t}\n}\n\nfunc TestValidateRejectsCaseInsensitiveAuthenticationHeaderDuplicates(t *testing.T) {\n\tcfg := validConfigForTest()\n\tcfg.Authentication = AuthenticationConfig{\n\t\tAllowedHosts: []string{\"https://app.example.com\"},\n\t\tHeaders: map[string]string{\n\t\t\t\"Authorization\": \"Bearer first-token\",\n\t\t\t\"authorization\": \"Bearer second-token\",\n\t\t},\n\t}\n\tif err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), \"duplicate\") {\n\t\tt.Fatalf(\"case-insensitive duplicate authentication headers were accepted: %v\", err)\n\t}\n}\n\nfunc TestValidateAuthenticationCredentialComponent(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\theader string\n\t\tvalue string\n\t\twantErr bool\n\t}{\n\t\t{name: \"valid bearer\", header: \"Authorization\", value: \"Bearer opaque-token\"},\n\t\t{name: \"short bearer credential\", header: \"Authorization\", value: \"Bearer x\", wantErr: true},\n\t\t{name: \"missing authorization scheme\", header: \"Authorization\", value: \"opaque-token\", wantErr: true},\n\t\t{name: \"placeholder bearer\", header: \"Authorization\", value: \"Bearer your-token\", wantErr: true},\n\t\t{name: \"valid custom credential\", header: \"X-API-Key\", value: \"opaque-key\"},\n\t\t{name: \"placeholder custom credential\", header: \"X-API-Key\", value: \"changeme\", wantErr: true},\n\t\t{name: \"proxy credential forbidden\", header: \"Proxy-Authorization\", value: \"Basic b3BhcXVl\", wantErr: true},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tcfg := validConfigForTest()\n\t\t\tcfg.Authentication = AuthenticationConfig{\n\t\t\t\tAllowedHosts: []string{\"https://app.example.com\"},\n\t\t\t\tHeaders: map[string]string{test.header: test.value},\n\t\t\t}\n\t\t\terr := cfg.Validate()\n\t\t\tif (err != nil) != test.wantErr {\n\t\t\t\tt.Fatalf(\"Validate() error = %v, wantErr %v\", err, test.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValidateRejectsCookieHeaderAndStructuredCookieCollision(t *testing.T) {\n\tcfg := validConfigForTest()\n\tcfg.Authentication = AuthenticationConfig{\n\t\tAllowedHosts: []string{\"https://app.example.com\"},\n\t\tHeaders: map[string]string{\"cookie\": \"raw=opaque-value\"},\n\t\tCookies: map[string]string{\"session\": \"structured-value\"},\n\t}\n\tif err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), \"conflicts\") {\n\t\tt.Fatalf(\"Cookie header/cookie map collision was accepted: %v\", err)\n\t}\n}\n\nfunc TestValidateRejectsUnsafeAuthenticationHeaders(t *testing.T) {\n\tcfg := Config{\n\t\tAI: AIConfig{Provider: \"deepseek\", APIKey: \"test-key\"},\n\t\tRecon: ReconConfig{Timeout: 1},\n\t\tScanning: ScanningConfig{Threads: 1, RateLimit: 1},\n\t\tAnalysis: AnalysisConfig{MinConfidence: 0.85},\n\t\tAuthentication: AuthenticationConfig{\n\t\t\tAllowedHosts: []string{\"https://example.com\"},\n\t\t\tHeaders: map[string]string{\"Host\": \"evil.example\"},\n\t\t},\n\t}\n\tif err := cfg.Validate(); err == nil {\n\t\tt.Fatal(\"expected unsafe authentication header to be rejected\")\n\t}\n}\n\nfunc TestValidateRequiresAuthenticationAllowedHosts(t *testing.T) {\n\tcfg := Config{\n\t\tAI: AIConfig{Provider: \"deepseek\", APIKey: \"test-key\"},\n\t\tRecon: ReconConfig{Timeout: 1},\n\t\tScanning: ScanningConfig{Threads: 1, RateLimit: 1},\n\t\tAnalysis: AnalysisConfig{MinConfidence: 0.85},\n\t\tAuthentication: AuthenticationConfig{Headers: map[string]string{\"Authorization\": \"Bearer token-value\"}},\n\t}\n\tif err := cfg.Validate(); err == nil {\n\t\tt.Fatal(\"expected authentication without allowed_hosts to be rejected\")\n\t}\n}\n\nfunc TestSaveAtomicallyReplacesExistingFileWithPrivateMode(t *testing.T) {\n\tdirectory := t.TempDir()\n\tpath := filepath.Join(directory, \"config.yaml\")\n\tif err := os.WriteFile(path, []byte(\"old: data\\n\"), 0o644); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := os.Chmod(path, 0o644); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tcfg := Config{AI: AIConfig{Provider: \"custom\", APIKey: \"opaque-api-key\"}}\n\tif err := cfg.Save(path); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tinfo, err := os.Stat(path)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif permissions := info.Mode().Perm(); permissions != 0o600 {\n\t\tt.Fatalf(\"saved config permissions = %o, want 600\", permissions)\n\t}\n\tcontents, err := os.ReadFile(path)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !strings.Contains(string(contents), \"opaque-api-key\") || strings.Contains(string(contents), \"old: data\") {\n\t\tt.Fatalf(\"saved config contents were not replaced: %q\", contents)\n\t}\n\tleftovers, err := filepath.Glob(filepath.Join(directory, \".config.yaml.tmp-*\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(leftovers) != 0 {\n\t\tt.Fatalf(\"temporary config files were left behind: %v\", leftovers)\n\t}\n}\n\nfunc TestSaveReplacesSymlinkWithoutOverwritingItsTarget(t *testing.T) {\n\tdirectory := t.TempDir()\n\tvictim := filepath.Join(directory, \"victim.txt\")\n\tpath := filepath.Join(directory, \"config.yaml\")\n\tif err := os.WriteFile(victim, []byte(\"do-not-touch\"), 0o644); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := os.Symlink(victim, path); err != nil {\n\t\tt.Skipf(\"symlinks unavailable: %v\", err)\n\t}\n\n\tcfg := Config{AI: AIConfig{APIKey: \"opaque-api-key\"}}\n\tif err := cfg.Save(path); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tvictimContents, err := os.ReadFile(victim)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif string(victimContents) != \"do-not-touch\" {\n\t\tt.Fatalf(\"Save followed and overwrote symlink target: %q\", victimContents)\n\t}\n\tinfo, err := os.Lstat(path)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !info.Mode().IsRegular() || info.Mode().Perm() != 0o600 {\n\t\tt.Fatalf(\"saved config did not replace symlink with private regular file: %v\", info.Mode())\n\t}\n}\n"}}}} |