Back to Articles

Noir: The Static Analysis Tool That Hunts Shadow APIs in Your Codebase

[ View on GitHub ]

Noir: The Static Analysis Tool That Hunts Shadow APIs in Your Codebase

Hook

Your API documentation says you have 47 endpoints. Your source code reveals 112. Those 65 invisible endpoints? They're what attackers find first.

Context

The shadow API problem is endemic in modern development. Deprecated routes that never got removed, experimental endpoints left in production code, internal APIs that bypass authentication middleware, and microservices that evolved faster than documentation could keep pace. Traditional API discovery relies on three approaches, all flawed: OpenAPI specifications that require developer discipline to maintain (and are often outdated within weeks), runtime proxies like Burp Suite that only capture what they observe during testing (missing authentication-gated endpoints), and manual code review that doesn't scale beyond toy projects.

This gap creates a dangerous blind spot in application security. Penetration testers waste days manually grepping for route definitions. DAST tools miss entire authentication-required surfaces. Security teams operate with incomplete attack surface maps. Noir emerged from OWASP to solve this precise problem: systematically extract every HTTP endpoint directly from source code across multiple languages and frameworks, then output that inventory in formats that feed directly into dynamic testing tools. It's positioned as the bridge between static and dynamic analysis, answering the question "what endpoints actually exist?" before asking "are they secure?"

Technical Insight

Noir's architecture is built around language-specific analyzers that parse source code to extract endpoint metadata. Written in Crystal—a compiled language with Ruby-like syntax—it achieves the performance needed to scan large monorepos while maintaining the readability that makes community contributions feasible. Each analyzer understands framework-specific routing patterns, whether that's Express.js route definitions, Django URL patterns, or Spring Boot controller annotations.

Here's what Noir discovers in a typical Express.js application:

// app.js
const express = require('express');
const app = express();

app.get('/api/users', listUsers);
app.post('/api/users', createUser);
app.delete('/api/users/:id', deleteUser);

// Deprecated but not removed
app.get('/api/v1/legacy-endpoint', oldHandler);

// Internal admin route
app.post('/internal/flush-cache', flushCache);

Running noir -u https://github.com/yourorg/yourapp produces output like this:

{
  "endpoints": [
    {
      "url": "/api/users",
      "method": "GET",
      "params": [],
      "protocol": "http"
    },
    {
      "url": "/api/users",
      "method": "POST",
      "params": [],
      "protocol": "http"
    },
    {
      "url": "/api/users/:id",
      "method": "DELETE",
      "params": ["id"],
      "protocol": "http"
    },
    {
      "url": "/api/v1/legacy-endpoint",
      "method": "GET",
      "params": [],
      "protocol": "http"
    },
    {
      "url": "/internal/flush-cache",
      "method": "POST",
      "params": [],
      "protocol": "http"
    }
  ]
}

Notice how it captures everything—including that legacy endpoint developers forgot about and the internal route that might lack proper authentication checks. This is the shadow API inventory that no documentation reflects.

The real architectural innovation is Noir's multi-format output pipeline. Beyond JSON, it generates OpenAPI 3.0 specifications, which means you can immediately import discovered endpoints into tools like Postman, generate client SDKs, or feed them into DAST scanners. The -f openapi3 flag transforms that endpoint list into a machine-readable spec that becomes your ground truth for security testing. No more manually building request collections or wondering if your DAST tool is hitting all endpoints.

Where Noir gets particularly interesting is its LLM integration for unsupported frameworks. When it encounters routing patterns it doesn't have a native parser for, it can invoke language models to extract endpoint information. This approach trades some precision for massive coverage expansion. A custom internal framework written in Go? The LLM mode attempts extraction even without a dedicated Go analyzer. It's imperfect but pragmatic—better to discover 80% of endpoints in an unsupported framework than 0%.

The tool also handles parameter extraction intelligently. Path parameters like :id, query parameters defined in code, and request body schemas all get captured and included in output. This metadata becomes crucial when feeding endpoints to fuzzing tools—knowing that /api/users/:id expects a numeric ID parameter means your fuzzer can generate appropriate test cases rather than random strings.

Integration with existing DevSecOps pipelines is straightforward. Run Noir during CI, export endpoints as JSON artifacts, then pass that artifact to your DAST stage. Tools like ZAP and Burp Suite can import the endpoint list and systematically test each discovered route with authentication contexts that runtime crawlers would miss. This is the SAST-to-DAST bridge in practice: static analysis discovers the attack surface, dynamic analysis probes it for vulnerabilities.

Gotcha

Static analysis has inherent limitations that Noir can't fully overcome. Dynamically constructed routes are its Achilles heel. If your application builds endpoint paths at runtime based on database configuration or environment variables, Noir won't see them in source code. Frameworks that use heavy metaprogramming or reflection to define routes present similar challenges—the endpoint definitions exist in code but aren't statically analyzable without executing the program.

Framework coverage is comprehensive but not universal. Popular frameworks like Express, Flask, Django, Rails, and Spring Boot have mature analyzers with high accuracy. Newer frameworks, internal custom routing libraries, or niche languages might fall back to LLM-based extraction with significantly lower confidence. The difference between native parsing and LLM extraction is the difference between "these are definitely your endpoints" and "these might be your endpoints." You'll want to validate LLM-discovered routes before trusting them in production security testing. Additionally, complex routing logic—like conditional routes that register based on feature flags or middleware that dynamically modifies paths—may produce incomplete or incorrect results. Noir excels at straightforward declarative routing but struggles with imperative endpoint construction.

Verdict

Use if: You're conducting whitebox security assessments with source code access and need comprehensive endpoint enumeration, especially across polyglot codebases with multiple frameworks. Use it when your API documentation is suspect or nonexistent and you need ground truth from code. It's invaluable in DevSecOps pipelines where you're feeding discovered endpoints into DAST tools for authenticated scanning, or when you're mapping attack surfaces for penetration testing and want to ensure nothing gets missed. The OpenAPI export alone justifies adoption if you're trying to bootstrap API documentation from legacy codebases. Skip if: You only have runtime access to applications without source code—use a crawling proxy instead. Skip it if your APIs are already comprehensively documented with actively maintained OpenAPI specs that you trust, or if your frameworks rely heavily on dynamic route construction that static analysis can't capture. Also skip if you're looking for general-purpose SAST that detects SQL injection or XSS—Noir is specialized for endpoint discovery, not vulnerability detection. It tells you where to test, not what's broken.

// ADD TO YOUR README
[![Featured on Starlog](https://starlog.is/api/badge/cybersecurity/owasp-noir-noir.svg)](https://starlog.is/api/badge-click/cybersecurity/owasp-noir-noir)