Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
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.
2026-08-07 12:53:47 +08:00

1 line
127 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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"}}}}