Lucene search
+L

JS Source Map Analyzer

🗓️ 10 Jul 2026 11:31:42Reported by KittySploitType 
kittysploit
 kittysploit
🔗 github.com👁 4 Views

JS Source Map Analyzer extracts secrets and sources from .map files.

Refs
Code
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
JavaScript source map analyzer — recover decompiled/original sources from .map files.

Fetches bundled .js, resolves ``sourceMappingURL``, downloads .map JSON, and extracts
API endpoints, secrets, and original file paths from ``sourcesContent``.
"""

from __future__ import annotations

import json
import re
from typing import Any, Dict, List, Optional, Set
from urllib.parse import urljoin, urlparse

from kittysploit import *
from lib.osint.js_secrets import extract_secret_hints
from lib.osint.js_sourcemap import (
    extract_from_sourcemap,
    parse_sourcemap_json,
    resolve_sourcemap_url,
)
from lib.protocols.http.http_client import Http_client


class Module(Auxiliary, Http_client):
    __info__ = {
        "name": "JS Source Map Analyzer",
        "author": ["KittySploit Team"],
        "description": (
            "Download client JS bundles, follow sourceMappingURL references, parse .map "
            "files, and extract decompiled sources, API routes, and secret literals."
        ),
        "tags": ["osint", "passive", "web", "javascript", "sourcemap", "webpack"],
        "agent": {
            "risk": "passive",
            "effects": ["network_probe"],
            "expected_requests": 6,
            "reversible": True,
            "approval_required": False,
            "produces": ["endpoints", "tech_hints", "risk_signals", "params"],
            "chain": {
                "produces_capabilities": [
                    {"capability": "graphql_endpoint", "from_detail": "graphql_endpoint"}],
                "suggested_followups": [
                    "auxiliary/scanner/http/graphql_abuse",
                    "auxiliary/scanner/http/api_fuzzer",
                    "auxiliary/scanner/http/jwt_oauth_probe"],
            },
        },
    }

    target = OptString("", "Target URL or domain", required=True)
    max_js = OptInteger(15, "Maximum JS bundles to analyze", False)
    max_display = OptInteger(25, "Maximum endpoints/secrets to print", False)
    show_details = OptBool(True, "Print recovered endpoints and secret hints", False)
    output_file = OptString("", "Optional JSON output file", False)

    def _print_findings(self, endpoints: List[str], keys: List[Dict[str, str]], source_files: List[str]) -> None:
        limit = max(1, int(self.max_display or 25))
        if bool(self.show_details) and endpoints:
            print_info("-" * 72)
            print_status(f"API / route hints ({min(len(endpoints), limit)} shown)")
            for ep in endpoints[:limit]:
                print_info(f"  {ep}")
            if len(endpoints) > limit:
                print_info(f"  ... and {len(endpoints) - limit} more (use output_file for full dump)")

        if bool(self.show_details) and keys:
            print_info("-" * 72)
            if keys:
                print_warning(f"Credential literals ({len(keys)} match(es))")
                rows = []
                for row in keys[:limit]:
                    name = str(row.get("name") or "secret")
                    value = str(row.get("value") or "")
                    if len(value) > 160:
                        value = value[:160] + "…"
                    source = str(row.get("source") or row.get("from") or "")[:80]
                    rows.append([name, value, source])
                print_table(["Name", "Value", "Source file"], rows)
            else:
                print_info("No credential-like literals after i18n/noise filtering")

        if bool(self.show_details) and source_files:
            print_info("-" * 72)
            print_status(f"Decompiled source paths ({min(len(source_files), limit)} shown)")
            for path in source_files[:limit]:
                print_info(f"  {path}")
            if len(source_files) > limit:
                print_info(f"  ... and {len(source_files) - limit} more")

    def _normalize_base_url(self, value: str) -> Optional[str]:
        v = str(value or "").strip()
        if not v:
            return None
        if not v.startswith(("http://", "https://")):
            v = "https://" + v
        return v

    def _fetch(self, url: str, timeout: int = 12) -> tuple[Optional[int], str, str]:
        parsed = urlparse(url)
        host = parsed.hostname
        if not host:
            return None, "", url
        scheme = (parsed.scheme or "https").lower()
        port = parsed.port or (443 if scheme == "https" else 80)
        path = parsed.path or "/"
        if parsed.query:
            path = f"{path}?{parsed.query}"
        old_target, old_port, old_ssl = self.target, getattr(self, "port", 443), getattr(self, "ssl", True)
        try:
            self.target = host
            self.port = int(port)
            self.ssl = scheme == "https"
            r = self.http_request(method="GET", path=path, allow_redirects=True, timeout=timeout)
            if not r:
                return None, "", url
            return r.status_code, r.text or "", getattr(r, "url", url) or url
        except Exception:
            return None, "", url
        finally:
            self.target, self.port, self.ssl = old_target, old_port, old_ssl

    def _extract_js_urls(self, html: str, base_url: str) -> List[str]:
        urls: Set[str] = set()
        for match in re.findall(r"""<script[^>]+src=["']([^"']+)["']""", html, flags=re.IGNORECASE):
            abs_url = urljoin(base_url, match.strip())
            if abs_url.startswith(("http://", "https://")) and abs_url.lower().endswith(".js"):
                urls.add(abs_url)
        for match in re.findall(r"""["']([^"']+\.js(?:\?[^"']*)?)["']""", html):
            abs_url = urljoin(base_url, match.strip())
            if abs_url.startswith(("http://", "https://")):
                urls.add(abs_url)
        return sorted(urls)

    def run(self):
        base_url = self._normalize_base_url(self.target)
        if not base_url:
            print_error("target is required")
            return False

        print_status(f"Analyzing JS source maps for {base_url}")
        status, html, final_url = self._fetch(base_url)
        if status is None or not html:
            print_error("Could not fetch target page")
            return False

        js_urls = self._extract_js_urls(html, final_url)[: int(self.max_js or 15)]
        print_info(f"JS bundles to inspect: {len(js_urls)}")

        maps_found: List[Dict[str, Any]] = []
        all_endpoints: Set[str] = set()
        all_keys: List[Dict[str, str]] = []
        source_files: Set[str] = set()
        graphql_endpoint = ""

        for js_url in js_urls:
            st, js_body, fetched_js = self._fetch(js_url)
            if st is None or not js_body:
                continue
            map_ref = resolve_sourcemap_url(js_body, fetched_js)
            if not map_ref:
                continue
            print_info(f"  sourceMappingURL: {map_ref[:120]}")

            smap = None
            map_fetched = map_ref
            if map_ref.startswith("data:"):
                from lib.osint.js_sourcemap import decode_data_sourcemap
                smap = decode_data_sourcemap(map_ref)
            else:
                map_url = urljoin(fetched_js, map_ref)
                mst, map_body, map_fetched = self._fetch(map_url)
                if mst and map_body:
                    smap = parse_sourcemap_json(map_body)
            if not smap:
                print_warning(f"  Could not parse map for {fetched_js}")
                continue

            extracted = extract_from_sourcemap(smap, js_url=fetched_js)
            extracted["map_url"] = map_fetched
            maps_found.append(extracted)
            for ep in extracted.get("endpoints") or []:
                all_endpoints.add(str(ep))
                if "graphql" in str(ep).lower() and not graphql_endpoint:
                    graphql_endpoint = str(ep).split("?", 1)[0]
            for row in extracted.get("key_hints") or []:
                if isinstance(row, dict):
                    all_keys.append(row)
            for row in extracted.get("recovered_sources") or []:
                if isinstance(row, dict) and row.get("source"):
                    source_files.add(str(row["source"]))

            print_success(
                f"  map OK: {extracted.get('source_count', 0)} sources, "
                f"{len(extracted.get('endpoints') or [])} endpoints, "
                f"{sum(1 for r in extracted.get('recovered_sources') or [] if r.get('has_content'))} decompiled"
            )

        if not maps_found:
            print_warning("No source maps recovered — bundles may be unmapped or blocked")
            self.vulnerability_info = {"reason": "No source maps found", "js_checked": len(js_urls)}
            return False

        findings = {
            "endpoints": sorted(all_endpoints)[:200],
            "key_hints": all_keys[:100],
            "source_files": sorted(source_files)[:120],
            "maps": maps_found,
            "graphql_endpoint": graphql_endpoint,
        }
        self.vulnerability_info = {
            "reason": (
                f"Recovered {len(maps_found)} source map(s), {len(source_files)} source file(s), "
                f"{len(all_endpoints)} endpoints, {len(all_keys)} secret hint(s)"
            ),
            "findings": findings,
            "map_count": len(maps_found),
            "decompiled_sources": sum(
                1 for m in maps_found for r in (m.get("recovered_sources") or []) if r.get("has_content")
            ),
            "graphql_endpoint": graphql_endpoint,
        }

        print_success(
            f"Source maps: {len(maps_found)} | decompiled files: {self.vulnerability_info['decompiled_sources']} | "
            f"endpoints: {len(all_endpoints)} | secrets: {len(all_keys)}"
        )
        self._print_findings(
            sorted(all_endpoints),
            all_keys,
            sorted(source_files),
        )
        if self.output_file:
            try:
                with open(str(self.output_file), "w", encoding="utf-8") as fp:
                    json.dump(self.vulnerability_info, fp, indent=2)
            except Exception as exc:
                print_warning(f"Could not write output: {exc}")
        return True

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

02 Aug 2026 20:51Current
5.8Medium risk
Vulners AI Score5.8
4