Skip to content

JS Reconnaissance

Samuele Giampieri edited this page Oct 25, 2026 · 13 revisions

📖 Canonical version: read this page on the official docs site — https://www.redamon.org/docs/js-reconnaissance. The GitHub wiki is a mirror.

JS Reconnaissance

JS Recon Settings

JS Recon is RedAmon's deep JavaScript reconnaissance engine. It runs as GROUP 5b in the recon pipeline -- immediately after resource enumeration -- and analyzes every JavaScript file discovered by crawlers (Katana, Hakrawler, GAU) plus any manually uploaded files. The goal is to extract secrets, endpoints, dependency confusion risks, source map exposures, DOM XSS sinks, and framework fingerprints embedded in client-side code.

Pipeline position: GROUP 5b runs whenever JS_RECON_ENABLED (JS Recon in the project form) is true. It is not a scan module: there is no js_recon entry to add to scanModules, and an older project or preset that still lists one loads fine but that entry does nothing. JS Recon cannot browse. It analyzes the JS files that resource enumeration's crawlers and HTTP probing found, so keep Resource Enumeration (or at least HTTP Probing) in the scan modules; with neither, it analyzes only manually uploaded files. JS Recon feeds discovered subdomains back into the pipeline and updates the Neo4j graph in a background thread.


What JS Recon Discovers

JS Recon runs seven parallel analysis modules via ThreadPoolExecutor(max_workers=4):

Module What It Finds Severity Range
Pattern Scanning 90+ secret patterns (cloud credentials, payment keys, auth tokens, infrastructure URLs, info leaks) Critical - Info
Secret Validation Live validation of 21 secret types against their APIs (GitHub, Stripe, Slack, AWS, etc.) Critical - High
Source Map Discovery Exposed .map files via comment parsing, HTTP headers, and path probing; the map's own sources go through every other module Low - Info (what the sources hold is reported as its own Secret, Endpoint and DOM-sink findings)
Dependency Confusion Scoped npm packages not registered on public npm (attacker could register them) Critical - High
Endpoint Extraction REST APIs, GraphQL, WebSocket, router definitions, admin/debug paths, API docs High - Info
Framework & Security Framework detection (12 built-in), DOM XSS sinks (15 patterns), dev comments with sensitive keywords Critical - Info
AI SDK Detection (Phase 6) LLM/vector-DB SDK imports, hard-coded provider keys, dangerouslyAllowBrowser opt-in, AI-frontend product markers in JS chunks, provider base URLs Critical - Info

Module Details

1. Pattern Scanning (Secrets Detection)

Scans every JS file against 90+ hardcoded regex patterns organized into seven categories:

Category Examples
Cloud AWS Access Key, GCP API Key, Azure Client Secret, DigitalOcean Token, Firebase Config
Payment Stripe Secret/Publishable Key, Square Access Token, PayPal Client ID, Razorpay Key
Auth GitHub Token, GitLab PAT, Slack Token/Webhook, Discord Bot Token, Twilio SID, Telegram Bot Token
JS Services Sentry DSN, Algolia API Key, Mapbox Token, Supabase Key, OpenAI API Key
General Secrets JWT tokens, generic API keys, private keys, database URIs, NPM tokens
Infrastructure S3 bucket URLs, GCP Storage URLs, Azure Blob Storage URLs
Info Leaks Email addresses, private IPs (RFC 1918), UUIDs, debug flags

Each finding includes a confidence level (high/medium/low), severity, redacted value, line number, and surrounding code context. A minimum confidence filter (JS_RECON_MIN_CONFIDENCE) controls which findings are kept.

Confidence. high is kept for a value with a provider-specific shape (AKIA…, ghp_…, xoxb-…, sk_live_…). A pattern that finds a generic run of characters only because a keyword sits nearby says medium: AWS Secret Key, Azure AD Client Secret, Azure Connection String, Plaid Secret Key, GoCardless API Token, Notion API Token, Harness API Key and Basic Auth Header. The Priority Board reads high as enough confidence for "Act soon", so an unvalidated keyword-anchored hit now stays in "Plan" at most. Raising JS_RECON_MIN_CONFIDENCE to high therefore also drops these patterns.

Keyword-anchored patterns are bounded. About 70 patterns look for a value near a keyword (etsy … "<24 characters>"). The keyword and the value may be at most 120 characters apart, and the nearest value is taken. On a minified bundle, one line used to let a match run from the keyword to the last candidate anywhere on the line. The Azure AD client secret also needs the value to be assigned to the client-secret key (clientSecret: "…", client_secret = "…"), with azure, msal or aad within those 120 characters: "azure" anywhere on the line plus any long quoted string later (MSAL's empty clientSecret: "" default did it) is no longer a hit.

Context around each secret. Each Secret stores up to 400 characters of the code around the match, as context: on a line over 1,000 characters (minified code) about 150 characters either side of the match, otherwise the line itself with the nearest 150 characters of the lines above and below. Every secret-shaped value in that text, the match itself and any other key on the same line, is shown in its redacted form (AKIAQ7...B3V6), whole even where the edge of the window cuts it. Emails, private IPs, UUIDs and bucket URLs stay readable. The Priority Board's reviewer reads it as evidence.

Keys that are public by design. A browser key, a publishable key, a DSN or a write-only analytics key ships to every browser that loads the page. These are written at severity info and flagged public_client_key, not dropped (an unrestricted Google key can still be billed against a paid API):

Family Pattern names
Google / Firebase GCP API Key (AIza…), Firebase API Key
Analytics and monitoring Segment Write Key, Amplitude API Key, New Relic Browser API Token (NRJS-…), Sentry DSN
Payments and maps Stripe Publishable Key (pk_live_…), Google reCAPTCHA Key, Mapbox Token (pk.…), Flutterwave Public Key
Search, chat, realtime Algolia App ID, Algolia Search Key, Intercom App ID, Pusher Key
Auth and mail Clerk Publishable Key, Mailgun Public Key (pubkey-…)
Supabase Supabase anon key, Supabase Publishable Key (sb_publishable_…)

An Algolia key is the public search-only key only when its label says so (searchKey, search_only_api_key, …); the admin key has the same format, so an unlabelled one stays a high Algolia API Key. The Priority Board ranks public keys in Track, the client report lists them on an "Expected-public client keys" line instead of counting them, the Red Zone secrets table shows them at info with a public key chip, and Mute Rules can select them with Public by design. A node written by an earlier release has no flag and keeps its old severity until JS Recon runs again; every reader also treats its pattern name as public meanwhile.

Keys that look public but are not. Some values have a public twin:

Finding Severity How it is told apart
Supabase service_role key critical (high confidence) A Supabase key is a JWT. Its payload is base64url-decoded (never verified, at most 4 KB; anything unreadable leaves the finding as it was) and its role claim read: service_role bypasses row-level security, anon is the public key above. A Supabase key matched both by the Supabase pattern and the JWT pattern is one finding.
Supabase Secret Key (sb_secret_…) critical The new key format; sb_publishable_… is its public twin
Signed URL in client code medium, low once expired A presigned S3 URL (X-Amz-Signature with its X-Amz-Credential) or a GCS signed URL (X-Goog-Signature): whoever holds it reads or writes the object until it expires. X-Amz-Date / X-Goog-Date plus -Expires in the past makes it low.
Zapier Webhook, Make Webhook high hooks.zapier.com/hooks/catch/… and hook.<region>.make.com/…: a POST runs the owner's automation, like a Slack or Discord webhook

CSS-module class names. A CSS-modules build names a form field's class after the field: password: "LoginForm_password__3xYz9". A value shaped like a CSS-module class (<Component>_<class>__<hash>, or css-loader's ___ and -- forms) is noise, like a route, a selector or an i18n key.

Embedded binary data. A match sitting inside a long base64 run (an embedded image or font) or right next to a font marker (@font-face, fontFamily, base64,, .woff) is dropped as noise. Only the text around the match counts: a match is inside a base64 run only when the run reaches well past it, so a long JWT, a Bearer header or a 1Password service-account token is kept; and on a line over 1,000 characters only the 300 characters either side of the match are checked for font markers, so one fontFamily near the top of a minified bundle no longer hides every secret on that line.

Custom patterns: Upload a JSON or TXT file with additional regex patterns via the project settings UI.


2. Secret Validation

When JS_RECON_VALIDATE_KEYS is enabled, discovered secrets are tested against their respective service APIs with a single, minimally-scoped request per secret. Per-service rate limiting (1 req/sec) prevents abuse.

Supported services (21):

Service Validation Method
AWS STS GetCallerIdentity
GitHub /user endpoint
GitLab /api/v4/user
Slack auth.test
Stripe /v1/charges?limit=1
Google Maps Geocode API
Twilio /Accounts
SendGrid /v3/scopes
Mailgun /domains
Mailchimp /3.0/ping
HubSpot /crm/v3/objects/contacts
Heroku /account
Firebase Realtime Database URL: one anonymous /.json?shallow=true read. 200 (publicly readable) is validated; 401 (the database rules refuse the read) is rejected; 403 is unknown
DigitalOcean /v2/account
Telegram /getMe
Discord /users/@me
Postmark /server
Okta /api/v1/users?limit=1
Shopify /admin/api/shop.json
Cloudflare /client/v4/user
OpenAI /v1/models

Each validation returns one of the following statuses, plus scope/permissions info when available. What the provider said and what it could not tell are kept apart: only rejected means the key is dead.

Status Badge Meaning
validated LIVE (red) API call confirmed the key is active and working. The scope/permissions field shows what access the key grants (e.g., user=admin, 3 scopes, database publicly readable). This is a confirmed, exploitable finding.
rejected rejected The provider said it does not accept the key: a 401, or the provider's own answer (Slack invalid_auth, token_revoked…; Google "The provided API key is invalid"). The key was real at some point but no longer works; the Priority Board moves it out of the ranking (inactive). A Firebase database whose rules refuse an anonymous read (401) is rejected too ("Database locked"): the protection works, so it is never proof.
unknown could not check No verdict: a timeout, a 429 or 5xx, a network error, a call the validator's circuit breaker skipped, or a 403 (often a live key missing the scope or SSO grant the check needed). The key may well be live; it stays ranked.
format_invalid malformed The value fails the format check of a validator that makes no call (a Twilio SID or Twitter bearer token with too little entropy).
format_validated format ok The value passes a format-only check; nothing was called.
unvalidated no validator No automated validator exists for this secret type, or the pattern did not include a validator_ref. The key may or may not be live -- manual verification is needed. Most generic regex matches (e.g., Generic API Key) fall into this category.
incomplete incomplete A validator exists but requires additional context that was not found alongside the key. For example, AWS validation needs both the Access Key ID and Secret Key; Twilio needs both the Account SID and Auth Token. Only one half was discovered.
skipped not checked Validation was not attempted. This happens when: (1) the Validate Keys setting is disabled in project settings, or (2) the finding has low confidence and was excluded from validation to avoid false API calls.

Findings written by an earlier release hold invalid, which mixed rejections with calls that never got an answer, and sometimes error. They keep that word until JS Recon runs again; the Priority Board still reads an invalid with a 401 as rejected and anything else as open, and a mute rule on invalid also matches rejected and format_invalid.


3. Source Map Discovery

Discovers exposed source maps through three methods:

  1. Comment parsing -- extracts //# sourceMappingURL= and /*# sourceMappingURL= directives
  2. HTTP header checking -- looks for SourceMap and X-SourceMap response headers
  3. Path probing -- tests 8 default paths (e.g., {url}.map, {url}.js.map) plus custom probe paths

When a source map is accessible, JS Recon:

  • Extracts original source filenames (up to 100)
  • Hands each of the target's own sources with text (not node_modules or the bundler runtime, which are public code full of example keys) to every module, as a file of its own named <map URL>#<source path> (for example https://app.example.com/static/js/main.js.map#src/config.ts). A key in src/config.ts becomes a Secret with that source_url, under its own JS file node, and goes through validation like any other; a fetch("/api/v2/orders") there becomes an Endpoint; DOM sinks and developer comments are found in the readable code. A source over 2 MB, and anything past 50 MB of recovered sources in one run, is not analyzed (the run records it). A framework that every original file imports is reported once per map
  • Reports the map itself as source_map_exposure: low when it embeds the target's own source text, info when it lists only file paths, low when every source is a library. secrets_in_source counts the Secrets found in its sources. Discovery has at most half of the JS Recon timeout, so the other modules always get their share
  • Reports a map that is referenced but not served as source_map_reference (info) only when that says something: an access refusal (401 or 403), or a map on another host it could not or may not reach. A 403 needs corroboration, because S3 without ListBucket, and CloudFront in front of it, refuse keys that do not exist: a 403 carrying S3 markers (<Code>AccessDenied</Code>, Server: AmazonS3, an x-amz-* header) is not reported, and otherwise one made-up sibling (<map URL>.<random>) is requested; if it is refused alike the row is dropped. A sibling check that gets no answer keeps the row, marked control_check: failed. No map is fetched from a host the Rules of Engagement exclude

Custom probe paths: Upload a TXT file with additional URL templates to test.


4. Dependency Confusion Detection

Extracts scoped npm packages (@scope/package) from JavaScript code using multiple patterns:

  • ES6 imports (import ... from '@scope/pkg')
  • CommonJS requires (require('@scope/pkg'))
  • Dynamic imports (import('@scope/pkg'))
  • Export statements (export ... from '@scope/pkg')
  • Webpack chunk names

Each scoped package is checked against the public npm registry:

Condition Severity Risk
Package not on public npm Critical Attacker can register the package name and inject malicious code
Package exists but marked "internal" High Verify ownership -- could be a squatted name

Well-known public scopes (100+) are automatically filtered to reduce noise.

Custom packages: Upload a TXT file listing known internal package names to skip.


5. Endpoint Extraction

Deep pattern-based parsing extracts multiple endpoint types:

Type Detection Method
REST API fetch(), axios, $.ajax, XMLHttpRequest, Node http clients, superagent, and any receiver's .get/.post/.put/.patch/.delete/.head/.options (this.get("/v1/orders"), minified n.get("/users")), plus a request object passed to a call (r({url: "/api/export", method: "GET"}))
GraphQL GraphQL endpoint URLs, introspection queries
WebSocket new WebSocket(), socket.io connections
Router definitions React Router, Vue Router, Express route patterns
Config objects baseURL, apiUrl, serverUrl patterns
API documentation Swagger, OpenAPI, GraphQL Playground paths
Admin/Debug Admin panels, debug endpoints, internal tools (classified as HIGH severity)

Endpoints are categorized (api, admin_debug, authentication, file_upload, etc.) and deduplicated by signature. Query parameters are extracted from URL patterns.

The URL may be a single-, double- or backtick-quoted string, after a base the code adds in front: fetch(API_BASE + "/v2/items") yields /v2/items. A template literal keeps its literal part: axios.get(`/api/users/${id}`) is stored as path /api/users/ with path_template /api/users/${id} on the Endpoint. A path without a leading slash (api.get("v1/accounts")) counts only from a call that is HTTP by name (fetch, axios, api, http, client); from any other receiver, map.get("key"), params.get("q") or headers.get("content-type") is a lookup, not an endpoint. Express-style app.get / router.get calls stay route definitions.

Tool miss: when 20 or more files were analyzed and not one endpoint came out, the app most likely calls its API in a way the extractor does not read. The run says so in its log, sets endpoint_extraction_miss in the JS Recon scan metadata, and records a js_endpoint_extraction note in the report's Coverage Limitations, so "no endpoints" is not read as "the app calls nothing".

An endpoint's HTTP method comes from its own call: the verb the call names (axios.post(...), client.put(...), xhr.open('PATCH', ...)), then a method: or type: key in that call's own options; a call that names neither is GET. Other code on the same line, such as a postMessage in a minified bundle, never changes it.

Endpoint validation (Validate Endpoints, off by default): each extracted endpoint on an in-scope host is probed once, with GET for a GET endpoint and HEAD or OPTIONS for any other method, so nothing is changed. An accepted status makes it hittable, but a server that answers every path alike (a single-page app returning its index page) would make every endpoint hittable. So a hittable endpoint is checked against a control: the same host is asked, the same way, for a made-up path next to it. When the made-up path gets the same answer the endpoint is catch_all and, like a not_hittable one, gets no Endpoint node. A control that cannot be sent or read keeps the endpoint hittable.

Custom keywords: Upload a TXT file with additional endpoint search keywords.


6. Framework Detection, DOM Sinks & Dev Comments

Framework detection identifies 12 built-in frameworks with version extraction:

React, Next.js, Vue.js, Nuxt, Angular, jQuery, Svelte, Ember.js, Backbone.js, Lodash, Moment.js, Bootstrap

DOM sink detection flags 15 XSS-vulnerable patterns:

Sink Type Examples
Direct injection innerHTML, outerHTML, document.write()
Code execution eval(), Function(), setTimeout(string), setInterval(string)
Navigation location.href, location.assign(), window.open()
Prototype pollution __proto__, constructor.prototype, Object.assign misuse
React-specific dangerouslySetInnerHTML

Dev comment extraction finds TODO/FIXME/HACK comments containing sensitive keywords (password, secret, admin, bypass, hardcoded).

Custom frameworks: Upload a JSON file with additional framework signatures.


7. AI SDK Detection (Phase 6 — Adversarial AI rollout)

Detects AI/LLM signals inside every JavaScript file the previous modules already downloaded. Reads from recon/helpers/ai_signal_catalog.py — the same single source of truth used by every other Adversarial AI hook. Pure pattern matching; sends no additional traffic to the target.

Five new JsReconFinding.finding_type values:

finding_type What it catches Typical severity
ai-sdk-client Vendor SDK imports shipped to the browser. openai, @anthropic-ai/sdk, @google/genai, @langchain/* (12 sub-packages), @llamaindex/*, ai/react + @ai-sdk/* (12 providers), @modelcontextprotocol/sdk, Pinecone/Qdrant/Chroma/Weaviate clients, langfuse, langsmith, portkey-ai, ollama. 65 SDK families total. medium–high
ai-sdk-key-literal Hard-coded provider API keys. Two tiers: prefix-anchored (OpenAI sk-proj-*T3BlbkFJ*, Anthropic sk-ant-api03-*AA, HuggingFace hf_*, Groq gsk_*, Replicate r8_*, Langfuse sk-lf-*, Pinecone pcsk_*, OpenRouter sk-or-v1-*, etc.) and constructor-context (paired with the SDK class to suppress false positives). Includes NEXT_PUBLIC_* env-var hydration leaks and Bearer <key>/x-api-key header literals. critical
ai-sdk-browser-allowed The OpenAI/Anthropic SDK dangerouslyAllowBrowser: true escape hatch. Catches bareword JS, terser-minified !0, and JSON-stringified "dangerouslyAllowBrowser":true (the form seen in Next.js __NEXT_DATA__ blobs). critical
ai-frontend-detected AI-product markers in shipped JS chunks the http_probe HTML/title channels cannot see (those live in async-loaded chunks). Open WebUI WEBUI_* constants, Gradio customElements.define("gradio-app",...), Streamlit stApp testids, Flowise chatflowid, LobeChat @lobehub/*, AnythingLLM, NextChat, BetterChatGPT, SillyTavern, ComfyUI, AUTOMATIC1111, InvokeAI, Jupyter-AI, Chainlit. medium–high
ai-provider-url Provider base URLs hard-coded in bundles. OpenAI, Anthropic, Cohere, Gemini, Vertex AI, HuggingFace Inference, Replicate, Groq, Together, DeepSeek, Perplexity, Fireworks, Mistral, xAI, OpenRouter, AWS Bedrock regional endpoints, Azure OpenAI, Helicone/Portkey/Cloudflare AI gateways, Langfuse Cloud, LangSmith API. medium

Catalogue size: 164 patterns across the 5 channels (33 prefix-anchored keys + 18 constructor-context + 65 SDK imports + 3 browser flags + 23 frontend markers + 22 provider URLs).

Constructor-context suppression. The pattern new OpenAI({apiKey: "sk-proj-...T3BlbkFJ..."}) would match both the constructor-context regex AND the prefix-anchored regex. Without dedup that's two findings for one underlying problem. The matcher records each constructor match's byte range in a "claimed" set; subsequent prefix matches whose span falls inside a claimed range are suppressed. Result: exactly one finding per leaked key, attributed to its SDK (OpenAI SDK constructor rather than generic OpenAI API Key).

Gemini disambiguation. Google's AIzaSy[A-Za-z0-9_-]{33} key shape is shared by Gemini, Maps, Firebase, YouTube Data, Translate, and Cloud Functions. A naive match would flood reports with false positives. The catalogue scans ±2KB around each match for Gemini-specific tokens (@google/genai, @google/generative-ai, GoogleGenerativeAI, generativelanguage.googleapis.com, gemini-1.5, x-goog-api-key); if any are present the finding escalates to Google Gemini API Key / critical, otherwise it stays at Google API Key (likely Maps/Firebase) / medium.

Secret enrichment (value-prefixed reuse). When an ai-sdk-key-literal finding's captured value overlaps an existing Secret node (caught earlier by the legacy JS_SECRET_PATTERNS scan), the matching Secret gets ai_provider and ai_finding_id properties set. The Cypher dedup is gated on the Secret's matched_text starting with a known AI-key prefix (sk-, hf_, lsv2_, gsk_, r8_, pcsk_, pplx-, xai-, csk-, tgp_, pa-, AIzaSy, co_, rpa_, pk-lf-, fw_) so Stripe/Slack/AWS literals that happen to co-locate in the same JS file are never wrongly enriched. Existing "show me all leaked API keys" queries automatically gain AI context: MATCH (s:Secret) WHERE s.ai_provider IS NOT NULL.

Toggle. JS Recon → AI SDK Detection (default on). Disabling skips the pass entirely; existing js_recon findings are unaffected.

Example queries.

// Sites shipping an LLM SDK to the browser
MATCH (b:BaseURL)-[:HAS_JS_FILE]->(:JsReconFinding {finding_type:'js_file'})
  -[:HAS_JS_FINDING]->(jf:JsReconFinding {finding_type:'ai-sdk-client'})
RETURN b.url, jf.sdk_name, jf.source_url AS js_file
ORDER BY b.url;

// CRITICAL: hard-coded keys in client-shipped JS
MATCH (jf:JsReconFinding {finding_type:'ai-sdk-key-literal', severity:'critical'})
RETURN jf.source_url, jf.sdk_name, jf.sample;

// Pivot from generic Secret to AI provider
MATCH (s:Secret) WHERE s.ai_provider IS NOT NULL
RETURN s.source_url, s.secret_type, s.ai_provider, s.validation_status;

For the full catalogue rationale, signal channel architecture, and per-vendor cite list see Adversarial AI Recon → JS Recon AI SDK Detection.


File Collection

JS Recon collects files from two sources:

  1. Pipeline output -- JS URLs discovered by Katana, Hakrawler, GAU, and jsluice during resource enumeration, and URLs HTTP probing found
  2. Manual uploads -- files uploaded via the JS Recon Upload API (/api/js-recon/{projectId}/upload)

The crawler's exclude list does not hide JS from JS Recon. Katana and Hakrawler drop URLs that match KATANA_EXCLUDE_PATTERNS, and the shipped list holds /static/, /_next/static, .chunk.js, .bundle.js, /dist/, /build/ and .min.js: exactly where React, Next.js and webpack apps keep their own code. Every in-scope JS URL a crawler saw is handed to JS Recon (and jsluice) before that filter. The exclude list still keeps those files out of the crawl's URL list and out of the Endpoint nodes. A partial JS Recon run reads the JS files the last run analyzed (its JS file nodes) as well as the graph's Endpoints.

Filtering rules:

  • A JS file is one whose name ends in .js, .mjs, .jsx, .ts or .tsx, also with a cache-busting suffix (app.js__v_3fa2, bundle.mjs~1, main.js-1a2b; the query string is ignored), but not app.js.map or app.js.gz. An extensionless name under /js/ or /javascript/, or a URL HTTP probing saw answer with a JavaScript Content-Type, counts too
  • At download, an HTML page is never analyzed: a single-page app answers a missing .js with its HTML shell and a 200. A JavaScript or ECMAScript Content-Type is accepted on any URL; otherwise the name must say JS (S3 and CDNs serve bundles as application/octet-stream)
  • Skipped: node_modules/, CDN libraries, jQuery/Bootstrap minified files
  • Optional: framework chunks (.chunk.js, .bundle.js), archived URLs from GAU/Wayback
  • A host the Rules of Engagement exclude is dropped, whichever source named it

Limits:

  • Max files: 10,000 (JS_RECON_MAX_FILES; 50 in stealth mode, and the memory governor can lower it). The budget is shared round-robin between hosts, so one large site cannot take it all, in a fixed order: within a host, the app's own entry points and page chunks (main, app, index, _app, pages/, chunks/) first, vendor and runtime files last. The JS Recon scan metadata records, per host, how many files there were, how many were taken and how many were cut, and a cut appears in the report's Coverage Limitations as js_recon_file_cap
  • Max file size: 5 MB per file
  • Download concurrency: 10 threads (configurable)
  • Lines longer than 500k characters are skipped to prevent regex DoS
  • Volume, measured: a 520-chunk Next.js-style site with a first-party source map per chunk (544 files, 21 MB, plus 1,046 recovered sources) is downloaded in seconds and analyzed in about a minute, at under 300 MB of memory

What a run could not re-check is kept. At the end of a run, js_recon findings it did not report again are removed, except where the run could not look:

  • no JS file came from the pipeline although the target had live URLs (Resource Enumeration off or failed, a crawl that found nothing usable), or JS Recon crashed: every earlier js_recon finding is kept;
  • the JS_RECON_MAX_FILES cap cut the file list, or an analyzer failed or timed out: the findings on the hosts of the files it did not analyze are kept, and the hosts it did analyze are cleaned up as usual.

A run with no live target at all keeps nothing back. What was cut is listed in the report's Coverage Limitations.

What each host got is recorded on it. Every web host the run looked at (each live one HTTP probing answered, each one it was handed pages on, each one serving a candidate file) gets a coverage stamp on its BaseURL: complete with how many files it read and how many findings came out, no_input when the host served no JavaScript, partial with the reason (file cap, host unreachable, analyzer failed or timed out), or failed. A host the Rules of Engagement exclude gets none, and neither does one that answered only with a 5xx and served no candidate file. A partial JS Recon run stamps the hosts it analyzed even when it found nothing, so "analyzed, nothing found" stays apart from "never analyzed".

JS File Sources (toggles)

Three toggles control which JS files are collected beyond the default crawler output:

Toggle Default Description
Include Webpack Chunks On Analyze .chunk.js and .bundle.js files that Katana may exclude. These contain application code with embedded secrets
Include Framework JS On Fetch Next.js (/_next/static/chunks/) and Nuxt.js (/_nuxt/) bundles that Katana excludes. Often contain API keys and Firebase configs
Include Archived JS On Analyze JS files that only web archives know (GAU, URLScan, ParamSpider). Old builds often contain hardcoded keys removed from production. Off, a JS URL that only an archive reported is left out; one a crawler or HTTP probing also saw is always analyzed. Needs GAU (or URLScan) on to have anything to add

Manual JS File Upload

Upload JS files directly for analysis without crawling. This is useful for files obtained from:

  • Burp Suite -- exported JS responses from intercepted traffic
  • Mobile APKs -- extracted JavaScript bundles from hybrid mobile apps
  • Browser DevTools -- saved JS files from authenticated areas
  • Internal apps -- JS files from applications not reachable by crawlers

How to use:

  1. Open your project settings and expand the JS Recon Scanner section
  2. Scroll to Manual JS File Upload at the bottom
  3. Click Upload JS Files and select one or more files
  4. Uploaded files appear in a list with size and a delete button

Manual upload is only available in edit mode (after the project has been created).

Constraints:

Rule Value
Accepted extensions .js, .mjs, .map, .json
Max file size 10 MB per file
Multiple files Yes (select multiple in the file dialog)
Storage location /data/js-recon-uploads/{projectId}/

Uploaded files are automatically merged with pipeline-discovered JS files during the scan. They can also be analyzed standalone when JS Recon runs in manual_upload mode.


Custom Extension Files

Five upload slots let you extend the built-in detection capabilities with target-specific data. These are additive -- they run alongside the defaults, never replacing them. Each upload type has client-side validation that runs before the file is sent to the server; if validation fails, a modal shows the exact error.

Custom extension files are only available in edit mode (after the project has been created). Click the Custom Extension Files collapsible header in the JS Recon settings to reveal all five slots.

Each upload slot has:

  • An Upload button to select the file
  • A display of the current file name and size (if uploaded)
  • A Delete button to remove the current file
  • A ? icon that opens a format guide modal with examples and how-it-works explanation

Upload 1: Custom Secret Patterns

Purpose: Add your own regex patterns to detect company-specific secrets, internal API key formats, or custom tokens that the built-in 90+ patterns do not cover.

Accepted formats: .json or .txt

JSON Schema

[
  {
    "name": "MyCompany API Key",
    "regex": "MYCO-[a-f0-9]{32}",
    "severity": "critical",
    "confidence": "high"
  },
  {
    "name": "Internal Service Token",
    "regex": "svc_tok_[A-Za-z0-9]{40}",
    "severity": "high",
    "confidence": "medium"
  }
]
Field Type Required Values
name string Yes Display name for the pattern
regex string Yes Python-compatible regex (compiled via re module)
severity string No critical, high, medium, low, info (default: medium)
confidence string No high, medium, low (default: medium)

TXT Format

One pattern per line, pipe-delimited:

MyCompany API Key|MYCO-[a-f0-9]{32}|critical|high
Internal Token|svc_tok_[A-Za-z0-9]{40}|high|medium
# Lines starting with # are comments

Field order: name | regex | severity | confidence (severity and confidence are optional).

Validation Rules

  • JSON: Must be a non-empty array. Each item must have name (string) and regex (string). Each regex is compiled to verify syntax. severity must be one of critical/high/medium/low/info. confidence must be one of high/medium/low.
  • TXT: Each non-comment line must have at least 2 pipe-separated fields (name|regex). Each regex is compiled to verify syntax.

How It Works

Each pattern is compiled as a Python regex and applied line-by-line to every downloaded JS file. When a match is found, a finding is created with the specified severity and confidence. The matched text is redacted in the output (first 6 + last 4 chars shown). Patterns with a high false-positive rate should use confidence low.

Regex syntax: Patterns run in Python (re module), not JavaScript. Avoid JS-only syntax like (?<name>...) named groups -- use (?P<name>...) or plain capture groups instead.


Upload 2: Source Map Probe Paths

Purpose: Add extra URL path templates to probe when looking for .map source map files. The scanner already tries 8 default paths. Use this to add paths specific to your target application's build tool or CDN structure.

Accepted format: .txt

File Format

One path template per line:

{base}/assets/maps/{filename}.map
{base}/sourcemaps/{filename}.map
{base}/build/static/js/{filename}.map
{base}/_assets/{filename}.map
# Lines starting with # are comments

Template Variables

Variable Expands To Example
{url} Full JS file URL https://example.com/js/app.js
{base} Scheme + host https://example.com
{filename} JS filename only app.js

Validation Rules

  • File must contain at least one non-comment line
  • Each line must contain either a { template variable or a / path separator

How It Works

For each downloaded JS file, the scanner first checks for a sourceMappingURL comment and SourceMap HTTP header. If neither is found, it probes each path template by replacing {url}, {base}, and {filename} with actual values and making an HTTP GET request. If a valid source map JSON (with "version" and "sources" fields) is returned, the scanner parses it, extracts original source filenames, and hands each first-party source in sourcesContent to every analysis module as a file of its own (see Source Map Discovery above).


Upload 3: Internal Package Names

Purpose: List known internal/private npm package names used by the target organization. These are ALWAYS checked against the public npm registry, even if not found in the JS code via import/require statements. This is useful when minified JS strips import names.

Accepted format: .txt

File Format

One scoped package name per line:

@mycompany/auth-sdk
@mycompany/api-client
@mycompany/shared-utils
@internal/config
@targetcorp/payment-lib
# Lines starting with # are comments

Validation Rules

  • File must contain at least one non-comment line
  • Every line must use the @scope/name format (must start with @ and contain /)
  • Well-known public scopes (@types, @babel, @angular, @vue, @react, etc. -- 100+ scopes) are automatically skipped during scanning

How It Works

For each package name, the scanner makes a GET request to https://registry.npmjs.org/{package}:

npm Registry Response Finding Severity Meaning
404 (not found) Critical Package does not exist on public npm. An attacker could register it and inject malicious code when the target runs npm install
200 (exists) High Package exists but is listed as "internal." Verify ownership -- could be a squatted name

Results appear in the Dependencies tab of the JS Recon dashboard.


Upload 4: Endpoint Keywords

Purpose: Add extra keywords to search for in JavaScript content. When a keyword is found inside a quoted string in the JS code, the surrounding URL is extracted as a discovered endpoint. Use this for target-specific API paths that the built-in patterns might miss.

Accepted format: .txt

File Format

One keyword per line:

/internal-api/v2/
/backoffice/
mycompany-service
admin-panel
graphql-gateway
/legacy/api/
# Lines starting with # are comments

Validation Rules

  • File must contain at least one non-comment line
  • Each keyword must be at least 2 characters long

Tips

  • Use path fragments like /internal-api/ for precision
  • Use service names like mycompany-service for broader matching
  • Avoid very short keywords (< 4 chars) to reduce false positives

How It Works

For each keyword, the scanner searches all JS file content using a case-insensitive regex. When a match is found, it extracts the surrounding quoted string (the URL/path containing the keyword). Each discovered URL is classified by category (admin, debug, auth, api, etc.) and assigned a severity. Results appear in the Endpoints tab of the JS Recon dashboard.


Upload 5: Framework Signatures

Purpose: Add detection signatures for custom or internal JavaScript frameworks not covered by the 12 built-in ones. Each signature defines regex patterns that identify the framework and optionally extract its version.

Accepted format: .json

JSON Schema

[
  {
    "name": "MyCompanyFramework",
    "patterns": [
      "MyFramework\\.init",
      "__MY_FRAMEWORK__",
      "myfw-version"
    ],
    "version_regex": "MyFramework\\.version\\s*=\\s*[\"']([0-9.]+)[\"']"
  },
  {
    "name": "InternalRouter",
    "patterns": [
      "InternalRouter\\.navigate",
      "__INTERNAL_ROUTER__"
    ],
    "version_regex": null
  }
]
Field Type Required Description
name string Yes Display name for the framework
patterns string[] Yes Non-empty array of regex strings. If any pattern matches, the framework is detected
version_regex string or null No Regex with capture group 1 for version extraction. Set to null if not needed

JSON Escaping Rules

Since regexes live inside JSON strings, backslashes need double-escaping:

Regex Intent In JSON Notes
Literal dot \\. One backslash + dot in JSON string
Whitespace \s \\s One backslash + s in JSON string
Quote in regex [\"'] or ['\"] Escaped quote inside JSON

Validation Rules

  • Must be a non-empty JSON array
  • Each item must have name (string) and patterns (non-empty array of strings)
  • Every pattern string is compiled as a regex to verify syntax
  • version_regex (if not null) is compiled as a regex to verify syntax

How It Works

Each signature's patterns are compiled as Python regexes (re module) and searched in the JS file content. If any pattern matches, the framework is detected. The version_regex (if provided) is then used to extract the version number from capture group 1. Detected frameworks appear in the Security tab of the JS Recon dashboard under "Frameworks." Version information enables targeted CVE lookups.

Regex syntax: Use Python regex syntax. Avoid JS-only features like (?<name>...) named groups -- use (?P<name>...) instead.


Subdomain Feedback Loop

JS Recon extracts hostnames from discovered endpoints, source map URLs, and cloud asset URLs. New in-scope subdomains are merged back into combined_result['dns']['subdomains'] with source='js_recon', making them available to downstream pipeline modules.


Graph Integration

JS Recon creates and updates the following Neo4j node types:

Node Type Relationship Created When
JsReconFinding (finding_type='js_file') (BaseURL)-[:HAS_JS_FILE]->(JsReconFinding) (or Domain for uploaded files) One per analyzed JS file; acts as the parent for all other findings
JsReconFinding (finding types below) (JsReconFinding{js_file})-[:HAS_JS_FINDING]->(JsReconFinding) Dependency confusion, source maps, DOM sinks, frameworks, dev comments, emails, internal IPs, object references (UUID/IDOR candidates), cloud assets
JsReconFinding (finding_type='external_domain') (Domain)-[:HAS_JS_FINDING]->(JsReconFinding) 3rd-party domains leaked in JS URLs (no single parent JS file)
Secret (JsReconFinding{js_file})-[:HAS_SECRET]->(Secret) Discovered secrets (with source='js_recon')
Endpoint (JsReconFinding{js_file})-[:HAS_ENDPOINT]->(Endpoint) Discovered endpoints (with source='js_recon' or js_recon_source=true)

All nodes include user_id and project_id for multi-tenant isolation. Graph updates run in a dedicated background thread.

JsReconFinding finding_type values: js_file, dependency_confusion, source_map_exposure, dom_sink, dev_comment, framework, email, internal_ip, object_reference, cloud_asset, external_domain, plus the Phase 6 AI SDK values: ai-sdk-client, ai-sdk-key-literal, ai-sdk-browser-allowed, ai-frontend-detected, ai-provider-url

AI SDK findings carry extra properties beyond the core set: sdk_name (canonical vendor product name), ai_provider (mirror of sdk_name for prefix-consistent queries), sample (redacted form of captured key — first 6 chars + ... + last 4, never the full secret), byte_offset (stable across re-scans for idempotent MERGE), detection_method='ai_sdk_catalogue'.

Secret enrichment. For ai-sdk-key-literal findings, the Phase 6 mixin also enriches matching Secret nodes (caught by the legacy JS_SECRET_PATTERNS scan) with ai_provider and ai_finding_id. This means existing "all leaked secrets" queries automatically gain AI context without a parallel taxonomy.

JsReconFinding core properties: id, finding_type, severity, confidence, title, detail, evidence, source_url, base_url, source, discovered_at, user_id, project_id, updated_at

JsReconFinding type-specific properties:

  • cloud_asset -> cloud_provider (aws/gcp/azure), cloud_asset_type
  • external_domain -> times_seen, sample_urls
  • object_reference -> potential_idor (boolean; heuristic flag, not actual IDOR detection -- only means the value matched a UUID v4 pattern and could be worth testing manually)

Security Measures

  • Secret redaction -- plaintext secret values are never stored; only redacted form (first 6 + last 4 characters) persists in output and graph
  • Rate limiting -- per-service 1 req/sec rate limit during secret validation
  • File size limits -- 10 MB per JS file download, 10 MB per manual upload
  • Project isolation -- uploaded files stored in /data/js-recon-uploads/{projectId}/
  • Regex DoS prevention -- lines exceeding 500k characters are skipped

API Endpoints

Method Path Description
GET /api/js-recon/{projectId}/upload List uploaded JS files
POST /api/js-recon/{projectId}/upload Upload a JS file for analysis (max 10 MB; .js, .mjs, .map, .json)
DELETE /api/js-recon/{projectId}/upload?name=filename.js Delete an uploaded file
GET /api/js-recon/{projectId}/download Download JS Recon scan results

Frontend: JsReconTable

JS Recon results are displayed in the Red Zone via a dedicated table component with six sub-tabs:

Tab Content
Secrets Severity badges, redacted values, validation status, confidence, category
Endpoints Method, path, full URL, type (REST/GraphQL/WS), category, source JS file
Dependencies Package name, scope, npm registry status, severity, recommendation
Source Maps JS URL, map URL, accessible status, discovery method, file count, embedded secrets
Security Frameworks (name, version), DOM sinks (type, pattern, severity), dev comments, IDOR candidates
Attack Surface New subdomains, cloud assets (S3/GCP/Azure), emails, internal IPs, external domains

Features: full-text search across all fields, pagination (50 items/page), and three export formats — XLSX (one worksheet per category), JSON (object keyed by section name), and MD (one ## Section per category, GFM table per section). See Data Export & Import for format details.


Configuration Reference

See Project Settings Reference > JS Reconnaissance for the complete parameter table.

Quick overview:

Setting Default Purpose
JS_RECON_ENABLED false Master toggle
JS_RECON_MAX_FILES 500 Max JS files to download
JS_RECON_CONCURRENCY 10 Download threads
JS_RECON_TIMEOUT 900 Total timeout (seconds)
JS_RECON_REGEX_PATTERNS true Secret pattern scanning
JS_RECON_SOURCE_MAPS true Source map discovery
JS_RECON_DEPENDENCY_CHECK true npm dependency confusion
JS_RECON_EXTRACT_ENDPOINTS true Endpoint extraction
JS_RECON_DOM_SINKS true DOM sink detection
JS_RECON_FRAMEWORK_DETECT true Framework detection
JS_RECON_DEV_COMMENTS true Dev comment extraction
JS_RECON_VALIDATE_KEYS true Live secret validation
JS_RECON_MIN_CONFIDENCE low Minimum confidence filter

Output Structure

The full JS Recon output is stored in combined_result['js_recon'] and includes:

js_recon/
  scan_metadata/       -- timestamp, mode, file count, duration
  secrets[]            -- id, name, severity, redacted_value, validation status
  endpoints[]          -- id, method, path, type, category, source_js
  dependencies[]       -- package_name, scope, npm_exists, severity
  source_maps[]        -- js_url, map_url, accessible, source_files
  dom_sinks[]          -- type, pattern, severity, line
  frameworks[]         -- name, version, confidence
  dev_comments[]       -- content, type, severity, line
  cloud_assets[]       -- provider, type, url
  emails[]             -- email, category, context
  ip_addresses[]       -- ip, type (private), context
  object_references[]  -- type (uuid), value, potential_idor
  discovered_subdomains[]  -- new in-scope subdomains
  external_domains[]   -- out-of-scope domains with frequency
  summary/             -- aggregated counts by severity, type, and category

Execution Modes

Mode Trigger File Source
post_recon Pipeline GROUP 5b (automatic) JS URLs from resource_enum + http_probe, merged with any uploaded files
standalone Manual trigger via orchestrator Crawls target domain then analyzes (bypasses pipeline)
manual_upload Uploaded files present during post_recon Uploaded files merged with pipeline-discovered URLs

JS Recon runs independently of resource enumeration. When resource_enum is active, pipeline-discovered JS URLs are merged with any uploaded files. When resource_enum is disabled, JS Recon analyzes uploaded files only. If neither source has files, JS Recon exits gracefully with zero findings.


Next Steps

Home


Getting Started

Core Workflow

Scanning & OSINT

AI & Automation

Plugins

HackLab

Analysis & Reporting

Contributing

Reference & Help

Clone this wiki locally