
Encuentra cada solicitud de API que realiza una aplicación web, desde el JavaScript que envía: método, URL, cuerpo, encabezados y operaciones GraphQL. Un solo comando.
Recupera cada petición que una aplicación web está construida para hacer a partir del JavaScript que entrega: método, URL, cuerpo, tipo de contenido y cabeceras.
El bundle de JavaScript de una aplicación de una sola página contiene cada petición API que la aplicación puede hacer. Dada una URL, jshound encuentra el bundle de la aplicación, lo rastrea por completo, incluidos los chunks cargados de forma diferida, y extrae esas peticiones.
Es pasivo. Las únicas peticiones que realiza son GETs no autenticados para archivos estáticos públicos, la página y su JavaScript. El análisis se ejecuta localmente y no se envía nada a la API de la aplicación.
git clone https://github.com/AmbitSecurity/jshound && cd jshound
python3 -m venv .venv && source .venv/bin/activate
pip install -e .
Requiere Python 3.10 o superior y Node 18 o superior.
jshound supabase.com/dashboard
$ jshound supabase.com/dashboard
307 128B https://supabase.com/dashboard
200 19K https://supabase.com/dashboard/org
200 28K https://frontend-assets.supabase.com/studio/8617532ae1a4/_next/static/chunks/1p849pzlxcp4n.js?dpl=dpl_ESaSquAbnWrJqbvvyRh7F1qj5wWH
...
supabase.com 200 2282 files (2281 from frontend-assets.supabase.com, 1 from supabase.com) -> 387 endpoints
1 declared chunk(s) could not be fetched
-> .jshound/supabase.com/endpoints.txt (also requests.jsonl, urls.txt, openapi.json)
frontend-assets.supabase.com, donde reside la compilación de supabase.POST https://api.supabase.com/platform/auth/{ref}/otp {"phone":"state:phone"}
DELETE https://api.supabase.com/v1/projects/{param_n}/functions/{param_e}
DELETE https://supabase.com/dashboard/api/integrations/stripe-sync application/json {"projectRef":"input:e","startTime":"input:t"}
POST https://supabase.com/docs/api/search/fts application/json {"query":"computed:trim"}
Varios objetivos a la vez, o una lista por stdin:
jshound app.example.com api.example.com
cat scope.txt | jshound
Los archivos que ya están en disco, como la carpeta js/ de una ejecución anterior, se leen sin tocar la red:
$ jshound examples/demo-app
GET https://demo-app.test/api/feature-flags
POST https://demo-app.test/t/events {"sid":"?","evt":"?"}
DELETE https://demo-app.test/v1/sessions/current
POST {baseUrl}/v1/media multipart/form-data {"file":"file-upload"}
POST {baseUrl}/v1/subscribe application/json {"email":"computed:trim","pageUri":"state:href"}
GET {baseUrl}/v1/users/{param_o}/profile
Todo va a .jshound/<host>/, o a -o DIR:
endpoints.txt | la tabla anterior, una petición por línea |
requests.jsonl | un objeto JSON por petición: method, url, body, content_type, headers, graphql o rpc, y la página y el archivo del que proviene |
urls.txt | METHOD URL para las peticiones cuyo host es conocido, para herramientas que aceptan una lista |
openapi.json | un documento OpenAPI 3, para importar en Burp, Caido o Postman |
js/ | cada archivo que se obtuvo |
{...} es un valor que jshound no pudo resolver, escrito tal como lo escribe el código: {baseUrl}, {window.__CONFIG__.apiBaseUrl}. En una ruta, {ref} es un parámetro, y {param_o} es el parámetro de función o./api/surveys/, pasa por código que jshound aún no sigue hasta su base. requests.jsonl indica el archivo del que proviene.input: un argumento, state: una propiedad de un objeto, computed: el resultado de una llamada, ? desconocido.application/json? es un tipo de contenido inferido a partir de las otras peticiones del archivo en lugar de leído en el punto de llamada. ~{...} es un cuerpo que tiene más campos que los listados.{graphql} es una operación GraphQL, con su nombre, tipo y variables como cuerpo.| bundler | URLs de chunks diferidos | árbol de módulos | cómo |
|---|---|---|---|
| webpack 5 | sí | sí | lee __webpack_require__.u del árbol de sintaxis |
| webpack 4 | sí | sí | no tiene .u, por lo que el constructor de URL con nombre se empareja por forma en lugar de por nombre |
| Rspack | sí | sí | compatible con webpack, normalmente emite ESM |
| Turbopack | sí | n/a | no hay constructor en absoluto; la lista de chunks es un array literal |
| Vite / Rollup | sí | n/a | las rutas están escritas (__vite__mapDeps); el ESM plano no tiene registro que recorrer |
| esbuild | sí | n/a | igual |
| Parcel | sí | n/a | el global del runtime lleva un sufijo por compilación (parcelRequire5707) y la tabla de chunks es un objeto, no un array |
| Metro (React Native) | n/a | n/a | entrega un solo bundle, sin chunks; se detecta por __METRO_GLOBAL_PREFIX__ |
| webpack Module Federation | sí | sí | el constructor no lleva mapa de ids, por lo que los ids se toman de los puntos de llamada __webpack_require__.e(480) |
jshound solicita el objetivo que le indiques, y los archivos JavaScript que cargan las páginas de ese objetivo, dondequiera que estén alojados. Para supabase eso es mayormente frontend-assets.supabase.com. Son archivos estáticos públicos que el navegador de cualquier visitante descarga. Esos otros hosts no reciben nada más que esos GETs.
Rechaza cualquier URL que un objetivo nombre en espacio de direcciones privado (loopback, RFC1918, link local), a menos que el objetivo que le diste sea él mismo privado o pases --allow-private.
-o DIR | dónde van los resultados (por defecto .jshound/<host>) |
-q | no imprimir cada petición a medida que se realiza |
-v | imprimir también el registro de análisis |
--max-chunks N | detenerse tras esta cantidad de chunks diferidos e importaciones (por defecto 50000) |
--no-chunks | no seguir los chunks diferidos |
--proxy URL | enviar cada petición a través de un proxy (o establecer JSHOUND_PROXY) |
--proxy-ca FILE | el certificado CA de un proxy que intercepta TLS |
--client PROFILE | fijar un perfil TLS de navegador en lugar de rotar cuando se bloquea |
--allow-private | permitir URLs en espacio de direcciones privado (ver Alcance) |