💡 The Idea

Traditional vulnerability scanners are pattern matchers. They look for known signatures, compare version numbers against CVE databases, and fire alerts when something matches a rule someone wrote months ago.

That works well for known vulnerabilities. It falls apart for:

  • Logic flaws that don't have signatures
  • Business logic issues unique to the app
  • Chained vulnerabilities that individually look benign
  • Context-dependent issues a rule can't understand

The idea behind this project was simple: what if instead of matching patterns, the scanner could reason about code and behavior the way a human researcher does? Feed it source code, API responses, or HTTP traffic and ask: "What looks exploitable here?"

That's what I built. Here's how it works, what it found, and where it still falls short.

🏗️ How It's Built

The scanner has three components working together:

Component 1: Data Collection Crawls the target, captures HTTP requests and responses, pulls in any available source code or JavaScript files.

Component 2: AI Analysis Engine Sends collected data to Claude's API in structured chunks. Each chunk gets a focused prompt asking for specific vulnerability classes. Results come back as structured JSON.

Component 3: Report Generator Aggregates findings, deduplicates, scores by severity, and produces a formatted report.

The whole thing is Python. Under 400 lines for the core logic.

⚙️ The Core Analysis Engine

This is the part that matters most. The quality of your findings is almost entirely determined by how you prompt the model.

import anthropic
import json
import requests
from urllib.parse import urljoin
from bs4 import BeautifulSoup


client = anthropic.Anthropic()

def analyze_endpoint(url: str, request_data: dict, response_data: dict) -> dict:
    """
    Send an HTTP request/response pair to Claude for vulnerability analysis.
    Returns structured findings.
    """
    prompt = f"""You are a security researcher analyzing an HTTP request and response
for vulnerabilities. Be specific and technical. Only report genuine security issues.

TARGET URL: {url}

REQUEST:
Method: {request_data.get('method', 'GET')}
Headers: {json.dumps(request_data.get('headers', {}), indent=2)}
Body: {request_data.get('body', 'None')}

RESPONSE:
Status: {response_data.get('status_code')}
Headers: {json.dumps(dict(response_data.get('headers', {})), indent=2)}
Body (first 3000 chars): {str(response_data.get('body', ''))[:3000]}

Analyze for these vulnerability classes:
1. Injection vulnerabilities (SQL, NoSQL, LDAP, command injection)
2. Broken authentication or session management issues
3. Sensitive data exposure in response headers or body
4. Security misconfigurations
5. Broken access control indicators
6. Information disclosure

Respond ONLY in this JSON format:
{{
  "findings": [
    {{
      "vulnerability": "vulnerability name",
      "severity": "critical/high/medium/low/info",
      "location": "where exactly in the request/response",
      "evidence": "specific text or pattern that indicates this issue",
      "explanation": "why this is a vulnerability",
      "recommendation": "how to fix it"
    }}
  ],
  "confidence": "high/medium/low",
  "notes": "any additional observations"
}}


If no vulnerabilities are found, return an empty findings array."""

    message = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1500,
        messages=[{"role": "user", "content": prompt}]
    )

    response_text = message.content[0].text

    try:
        cleaned = response_text.strip()
        if cleaned.startswith("```"):
            cleaned = cleaned.split("```")[1]
            if cleaned.startswith("json"):
                cleaned = cleaned[4:]
        return json.loads(cleaned.strip())
    except json.JSONDecodeError:
        return {"findings": [], "confidence": "low", "notes": "Parse error"}

def analyze_javascript(js_content: str, source_url: str) -> dict:
    """
    Analyze JavaScript files for security issues:
    exposed API keys, hardcoded credentials, insecure endpoints.
    """

    prompt = f"""Analyze this JavaScript code for security vulnerabilities.
Focus on: hardcoded secrets or API keys, exposed internal endpoints,
insecure cryptography, client-side authentication bypass, dangerous eval() usage.

Source: {source_url}

Code:
{js_content[:5000]}

Respond in the same JSON format as a standard vulnerability finding."""

    message = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1000,
        messages=[{"role": "user", "content": prompt}]
    )

    try:
        return json.loads(message.content[0].text.strip())

    except:
        return {"findings": [], "confidence": "low"}

🔎 The Crawler

The scanner needs to collect data before it can analyze anything. Here's the lightweight crawler:

def crawl_target(base_url: str, max_pages: int = 30) -> list:
    """
    Crawl a target and collect request/response pairs for analysis.
    Returns list of dicts containing url, request, response data.
    """
    
    visited = set()
    to_visit = [base_url]
    collected = []
    
    session = requests.Session()
    session.headers.update({
        "User-Agent": "Mozilla/5.0 (Security Assessment Bot)"
    })
    
    while to_visit and len(visited) < max_pages:
        url = to_visit.pop(0)
    
        if url in visited:
            continue
    
        try:
            response = session.get(url, timeout=10, allow_redirects=True)
            visited.add(url)
     
             collected.append({
                "url": url,
                "request": {
                    "method": "GET",
                    "headers": dict(response.request.headers),
                    "body": None
                },
                "response": {
                    "status_code": response.status_code,
                    "headers": dict(response.headers),
                    "body": response.text
                }
            })
      
          # Extract links for further crawling
            if "text/html" in response.headers.get("Content-Type", ""):
                soup = BeautifulSoup(response.text, "html.parser")
                for link in soup.find_all("a", href=True):
                    absolute = urljoin(base_url, link["href"])
                    if base_url in absolute and absolute not in visited:
                        to_visit.append(absolute)
    
                # Also extract JS files for separate analysis
                for script in soup.find_all("script", src=True):
                    js_url = urljoin(base_url, script["src"])
                    if base_url in js_url:
                        js_resp = session.get(js_url, timeout=10)
                        collected.append({
                            "url": js_url,
                            "type": "javascript",
                            "content": js_resp.text
                        })
          
          except requests.RequestException as e:
            print(f"  [!] Error crawling {url}: {e}")
            continue
    
    print(f"[+] Collected {len(collected)} pages and resources")
    return collected

def run_scanner(target_url: str, output_file: str = "report.json"):
    """Main scanner orchestration"""
    print(f"[*] Starting AI vulnerability scan: {target_url}")
    all_findings = []
    pages = crawl_target(target_url)
    
    for i, page in enumerate(pages, 1):
        print(f"[*] Analyzing ({i}/{len(pages)}): {page['url']}")
        
        if page.get("type") == "javascript":
            result = analyze_javascript(page["content"], page["url"])
        else:
            result = analyze_endpoint(
                page["url"],
                page["request"],
                page["response"]
            )
        if result.get("findings"):
            for finding in result["findings"]:
                finding["source_url"] = page["url"]
                all_findings.append(finding)
                print(f"  [!] {finding['severity'].upper()}: {finding['vulnerability']}")
    
     
    # Sort by severity
    severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
    all_findings.sort(key=lambda x: severity_order.get(x["severity"], 5))
    
    report = {
        "target": target_url,
        "total_pages_analyzed": len(pages),
        "total_findings": len(all_findings),
        "findings": all_findings
    }
    with open(output_file, "w") as f:
        json.dump(report, f, indent=2)
    print(f"\\n[✓] Scan complete. {len(all_findings)} findings. Report: {output_file}")
    return report
if __name__ == "__main__":
    import sys
    target = sys.argv[1] if len(sys.argv) > 1 else "<http://localhost:3000>"
    run_scanner(target)

🧪 What I Tested It On

I ran this against OWASP Juice Shop, a deliberately vulnerable web application designed for exactly this kind of testing. Here's what the scanner found across 30 pages:

Critical Findings (2)

  • Hardcoded JWT secret in a JavaScript file. The scanner pulled the JS bundle, spotted what looked like a signing key in a configuration object, and flagged it correctly. A traditional scanner would have missed this entirely since there's no CVE pattern to match.
  • Admin endpoint exposed without authentication. The AI noticed that a /rest/admin endpoint returned a 200 status with administrative data despite no auth headers in the request. It also noted this was unusual given other endpoints returned 401.

High Findings (4)

  • Sensitive data in error responses. Several error pages returned stack traces with internal path information and database query details. The scanner picked up on the specifics: table names, column structures, internal server paths.
  • Missing security headers. Standard finding, but the scanner went further and explained which specific attacks each missing header enabled. Not just "X-Frame-Options missing" but "missing X-Frame-Options enables clickjacking attacks against authenticated user actions on this domain."
  • SQL injection surface identified. Parameters in search endpoints were flagged as potentially unsanitized. The scanner didn't confirm exploitability (it wasn't sending test payloads), but it identified the surface correctly.
  • User enumeration via response timing differences. The scanner noticed that the login endpoint returned faster for nonexistent users than for existing ones. This is a subtle finding that requires reasoning about behavior across multiple responses, not pattern matching.

Medium Findings (6)

  • Verbose server headers, insecure cookie flags, CORS misconfiguration, password returned in profile response (yes, really), internal IP addresses in responses, and outdated library versions visible in headers.

🎯 Where It Does Well

The scanner genuinely outperformed traditional tools in a few areas:

Context-aware analysis

  • It doesn't just flag things in isolation. When it found a missing security header, it related that to other findings on the same endpoint.
  • "This endpoint handles payment data AND is missing HSTS AND returns sensitive fields in the response" is a better finding than three separate low-severity alerts.

JavaScript analysis

  • Static analysis of JS bundles is where it really shines.
  • Finding hardcoded secrets, internal API endpoints, commented-out debug code, and insecure patterns in minified JavaScript is tedious manually and nearly impossible with signature-based tools.

Logic issue detection

  • The user enumeration via timing difference is a good example. No signature exists for this.
  • The AI reasoned about what the behavior implied about the backend logic.

Natural language explanations

  • Every finding comes with an explanation that a developer can actually act on.
  • Not just "SQL injection detected" but a specific explanation of the parameter, the likely backend query structure, and a concrete fix.

⚠️ Where It Falls Short

Honest assessment: this is a research tool, not a production scanner.

False positives exist.

  • The model occasionally flags things that look suspicious but are actually fine. About 15 to 20% of findings in my testing needed manual verification to confirm.
  • For a real engagement, you review everything anyway, so this is acceptable. For automated blocking, it's not.

It doesn't exploit, it suspects.

  • The scanner identifies potential SQL injection surfaces but doesn't send payloads to confirm them.
  • It flags what looks like a misconfigured CORS policy but doesn't prove it's exploitable. You still need to manually verify and exploit.

Token limits constrain depth.

  • Large JavaScript bundles get truncated. Complex multi-page flows where a vulnerability spans several requests are hard to capture in the context window.

Cost scales with scope.

  • Analyzing 30 pages with moderate response sizes used roughly $0.40 in API calls.
  • For a large application with hundreds of endpoints, that adds up. Not prohibitive, but worth tracking.

It's not a replacement for manual testing.

  • Business logic flaws that require understanding of the application's purpose, multi-step attack chains, and anything requiring stateful exploitation still need a human.

🔮 What's Next for the Project

A few improvements I'm working on or planning:

  • Authenticated scanning : log in first, pass session cookies to the crawler so it sees the authenticated attack surface
  • Active verification : after the AI identifies a potential injection point, send targeted payloads to confirm exploitability before reporting
  • Diff-based scanning : run the scanner regularly and only analyze pages that changed since the last run
  • Burp Suite integration : export the proxy history from Burp and feed it directly into the analyzer instead of using the built-in crawler
  • Fine-tuning prompts per vulnerability class : specialized prompts for IDOR, auth issues, and API security get better results than a single general prompt

🏁 Final Thought

  • The most interesting thing about building this wasn't the code. It was watching the AI reason about security issues in ways that surprised me.
  • Finding the timing-based user enumeration wasn't in the prompt. Neither was connecting missing headers to specific attack scenarios based on what data the endpoint handled.
  • The model brought security knowledge I didn't explicitly give it.