chore: auto-commit (2 archivos)

- CONVENTIONS.md
- tools/gen_osint_tools.py

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-14 23:55:18 +02:00
parent cb7f6e92a0
commit 0e7b615a1e
2 changed files with 973 additions and 0 deletions
+110
View File
@@ -167,3 +167,113 @@ usa `tipo: organizacion` / `tipo: lugar` en vez de `tipo: persona`.
El migrador debe ser re-ejecutable: si una persona ya existe en osint con su slug, se
actualiza (no se duplica). Los attachments ya movidos no se vuelven a mover.
## 9. Scans de red (recon)
Todo escaneo de red de una investigación —WHOIS, RDAP, DNS, nmap, traceroute, ping— se
**archiva SIEMPRE en OSINT**. No existen scans sueltos: el resultado queda como nota navegable
en el vault y como fila consultable en la base de datos. Lo gestionan las funciones del grupo
de capacidad `recon` del registry (dominio `cybersecurity`); ver `docs/capabilities/recon.md`.
### 9.1 Nota del scan en el vault
Cada scan produce una nota Markdown bajo la carpeta del dominio escaneado:
```
dominios/<slug>/recon/<scan_type>-<YYYYMMDD-HHMM>.md
```
donde `<scan_type>` es uno de `whois | rdap | dns | nmap | traceroute | ping` y el timestamp
tiene granularidad de minuto. Frontmatter de la nota:
```yaml
tipo: scan-red
scan_tipo: whois # whois|rdap|dns|nmap|traceroute|ping
target: "ejemplo.com" # objetivo original (dominio, host o IP)
slug: ejemplo.com # slug del target (clave de la carpeta)
fecha: 2026-06-14T13:18:00 # ISO, momento del scan
herramienta: whois # CLI usada (whois, dig, nmap, ...)
tags: [scan-red, whois, recon]
```
Body: cabecera con target/tipo/herramienta/fecha, un `## Resumen` opcional con los campos
destacados del scan, y la salida cruda completa (`raw`) dentro de un bloque de código. La nota
es la **capa crítica**: si no se puede escribir, el guardado falla.
### 9.2 Tabla `network_scans` (DuckDB, service osint_db)
Además de la nota, cada scan se registra en la tabla `network_scans` (schema `main`) de la
base DuckDB que posee el service `osint_db` (single-writer), vía
`POST http://127.0.0.1:8771/api/scan`. Columnas:
| Columna | Qué |
|---|---|
| `id` | Identificador del scan |
| `target` | Objetivo original (dominio/host/IP) |
| `target_slug` | Slug del target (clave de agrupación) |
| `scan_type` | `whois \| rdap \| dns \| nmap \| traceroute \| ping` |
| `tool` | CLI usada (whois, dig, nmap, ...) |
| `scan_ts` | Timestamp ISO del scan |
| `note_path` | Ruta relativa de la nota en el vault |
| `summary` | JSON con los campos resumidos del scan |
| `created_at` | Timestamp de inserción |
Es la **capa best-effort**: si `osint_db` está caído o no expone el endpoint, el guardado
degrada a solo-nota (`registered=False` + aviso) sin fallar. El re-ingest del vault NO borra
`network_scans` —es una tabla de datos vivos, no derivada de las notas.
### 9.3 Cómo lanzar y guardar
El camino canónico es el pipeline one-shot del registry, que escanea y archiva en una sola
llamada:
```bash
cd /home/enmanuel/fn_registry
./fn run recon_osint <target> <scan_type> # p.ej. ./fn run recon_osint ejemplo.com whois
```
Para un nmap pesado (full-tcp, vuln, udp-top) lanzar en segundo plano por la duración:
```bash
nohup ./fn run recon_osint scanme.nmap.org nmap --profile full-tcp --timeout-s 7200 \
> /tmp/recon-fulltcp.log 2>&1 &
```
Alternativa atómica (controlas el scan y lo guardas aparte) desde Python, importando las
funciones del registry —no se reescriben:
```python
import sys; sys.path.insert(0, "python/functions")
from cybersecurity import dns_records
from cybersecurity.save_scan_to_osint import save_scan_to_osint
scan = dns_records("ejemplo.com")
if scan["status"] == "ok":
save_scan_to_osint("ejemplo.com", "dns", scan["raw"],
summary={"A": scan["records"].get("A")}, tool="dig")
```
### 9.4 Cómo consultar scans guardados
Desde una nota del vault, con un bloque `osintdb` (plugin osint-db) que consulta la tabla:
````markdown
```osintdb
SELECT scan_type, tool, scan_ts, note_path
FROM network_scans
WHERE target_slug = 'ejemplo.com'
ORDER BY scan_ts DESC
```
````
O contra el service directamente vía `/api/query` (mismo SQL). El slug del target se deriva
igual que en todo el vault:
```python
import re
slug = re.sub(r"[^a-z0-9._-]+", "-", target.lower())
```
> Nota: el `slug` de un dominio/host (p.ej. `ejemplo.com`, `192.168.1.10`) conserva puntos y
> guiones porque el set permitido es `[a-z0-9._-]`; difiere del slug de persona de la sección 2,
> que solo admite `[a-z0-9-]`.
+863
View File
@@ -0,0 +1,863 @@
#!/usr/bin/env python3
"""Genera las fichas de herramientas OSINT en el vault osint + el MOC indice.
Usa la funcion del registry create_obsidian_note_py_obsidian para escribir cada
ficha como .md plano con frontmatter YAML (sin abrir la GUI de Obsidian).
Fuente de verdad: la lista TOOLS de abajo. Idempotente con overwrite=True.
"""
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "python", "functions"))
# Permitir ejecucion desde la raiz del repo tambien:
sys.path.insert(0, os.path.join("/home/enmanuel/fn_registry", "python", "functions"))
from obsidian import create_obsidian_note # noqa: E402
VAULT = "/home/enmanuel/Obsidian/osint"
# Etiquetas de categoria legibles para el MOC.
CAT_LABELS = {
"buscador": "Buscadores y meta-buscadores",
"identidad": "Identidad — username, email, telefono, brechas",
"social": "Redes sociales y rostros",
"dominio": "Dominios e infraestructura (pasivo)",
"geolocalizacion": "Geolocalizacion (imagen, mapas, satelite, metadatos, IP)",
"imagen-forense": "Verificacion forense de imagen y video",
"archivo": "Archivo e historico web",
"empresa-es": "Empresas y registros (España)",
"framework": "Frameworks y portales de referencia",
}
CAT_ORDER = list(CAT_LABELS.keys())
# Cada tool: slug, nombre, url, cat, coste, reg (requiere cuenta), amb, antibot
# (bloquea curl / WAF, necesita navegador real), tags extra, para, como, gotchas.
TOOLS = [
# ---------- Buscadores ----------
dict(slug="google-dorking", nombre="Google Dorking", url="https://www.google.com/advanced_search",
cat="buscador", coste="free", reg=False, amb="global", antibot=False,
tags=["dorks", "buscador"],
para="Busqueda avanzada con operadores (site:, filetype:, intext:, inurl:) para encontrar documentos, perfiles y filtraciones indexadas.",
como="Combina operadores: `site:linkedin.com \"nombre\"`, `filetype:pdf \"empresa\"`, `intext:\"email@dominio\"`.",
gotchas=["Google limita resultados y muestra captcha si detecta scraping.",
"Los dorks no son ilegales pero acceder a lo que exponen puede serlo segun contexto."]),
dict(slug="bing", nombre="Bing Search", url="https://www.bing.com",
cat="buscador", coste="free", reg=False, amb="global", antibot=False,
tags=["buscador"],
para="Segundo indice: a veces conserva resultados que Google ya purgo y soporta operadores propios.",
como="Usa operadores `site:`, `ip:`, `feed:`. Util como contraste de Google.",
gotchas=["Indice menor que Google; complementario, no sustituto."]),
dict(slug="yandex-search", nombre="Yandex Search", url="https://yandex.com",
cat="buscador", coste="free", reg=False, amb="global", antibot=False,
tags=["buscador", "ruso"],
para="Buscador ruso con cobertura fuerte de contenido del este de Europa y Asia que Google indexa peor.",
como="Busqueda normal + su reverse image (ver ficha yandex-images) es la mejor del mercado.",
gotchas=["Interfaz/resultados sesgados a region; usa yandex.com (no .ru) para ingles."]),
dict(slug="duckduckgo", nombre="DuckDuckGo", url="https://duckduckgo.com",
cat="buscador", coste="free", reg=False, amb="global", antibot=False,
tags=["buscador", "privacidad"],
para="Buscador sin tracking; sus bangs (!g, !w) saltan a otros indices rapido.",
como="Usa `!` bangs para redirigir busquedas; util para consultas sin personalizacion.",
gotchas=["Indice propio limitado; mezcla resultados de Bing."]),
dict(slug="brave-search", nombre="Brave Search", url="https://search.brave.com",
cat="buscador", coste="free", reg=False, amb="global", antibot=False,
tags=["buscador", "privacidad"],
para="Indice independiente (no depende de Google/Bing); buen contraste para descubrir fuentes distintas.",
como="Busqueda directa; ofrece API de pago para automatizar.",
gotchas=["Indice mas joven; cobertura desigual por idioma."]),
dict(slug="intelligence-x", nombre="Intelligence X", url="https://intelx.io",
cat="buscador", coste="freemium", reg=True, amb="global", antibot=False,
tags=["leaks", "pastes", "darkweb"],
para="Motor que indexa filtraciones, pastes, documentos, darkweb y datos historicos por selector (email, dominio, BTC, IP).",
como="Busca un selector (email/dominio); la vista previa es gratis, el contenido completo requiere creditos.",
gotchas=["Manejar datos de brechas puede tener implicaciones legales segun jurisdiccion.",
"Cuenta gratuita muy limitada."]),
# ---------- Identidad ----------
dict(slug="whatsmyname", nombre="WhatsMyName", url="https://whatsmyname.app",
cat="identidad", coste="free", reg=False, amb="global", antibot=False,
tags=["username", "enumeracion"],
para="Enumera en que cientos de sitios existe un nombre de usuario concreto.",
como="Escribe el username; devuelve presencia por sitio. Mismo dataset que usan herramientas CLI.",
gotchas=["Falsos positivos: algunos sitios devuelven 200 para cualquier usuario.",
"No confirma que sea la MISMA persona, solo que el handle existe."]),
dict(slug="sherlock", nombre="Sherlock (CLI)", url="https://github.com/sherlock-project/sherlock",
cat="identidad", coste="free", reg=False, amb="global", antibot=False,
tags=["username", "cli"],
para="Herramienta CLI que busca un username en 400+ redes sociales.",
como="`sherlock <username>`; genera lista de URLs donde el handle existe.",
gotchas=["Falsos positivos frecuentes; verificar a mano.",
"Requiere instalacion local (pip/docker)."]),
dict(slug="maigret", nombre="Maigret (CLI)", url="https://github.com/soxoj/maigret",
cat="identidad", coste="free", reg=False, amb="global", antibot=False,
tags=["username", "cli"],
para="Sucesor mas potente de Sherlock: 2500+ sitios y extrae datos del perfil (no solo presencia).",
como="`maigret <username> --html`; genera informe con perfiles y metadatos extraidos.",
gotchas=["Mas lento por la cobertura; usar --top-sites para acotar.",
"Instalacion local requerida."]),
dict(slug="namechk", nombre="Namechk", url="https://namechk.com",
cat="identidad", coste="free", reg=False, amb="global", antibot=True,
tags=["username", "dominio"],
para="Comprueba disponibilidad de un username (y dominios) en muchas plataformas a la vez.",
como="Escribe el handle; marca ocupado/libre por servicio.",
gotchas=["Pensado para branding, no para OSINT: no enlaza al perfil.",
"Protegido por WAF; necesita navegador real (curl da 403)."]),
dict(slug="instantusername", nombre="Instant Username Search", url="https://instantusername.com",
cat="identidad", coste="free", reg=False, amb="global", antibot=True,
tags=["username"],
para="Chequeo de username en tiempo real sobre decenas de servicios.",
como="Escribe el handle; resultados incrementales segun teclea.",
gotchas=["Cobertura menor que Maigret.",
"WAF: necesita navegador real."]),
dict(slug="hunter-io", nombre="Hunter.io", url="https://hunter.io",
cat="identidad", coste="freemium", reg=True, amb="global", antibot=False,
tags=["email", "empresa"],
para="Encuentra y verifica direcciones de correo asociadas a un dominio corporativo, con el patron (nombre.apellido@).",
como="Introduce un dominio; devuelve correos conocidos + patron. Verificador de entregabilidad incluido.",
gotchas=["Plan gratis pocas busquedas/mes.",
"Solo correos corporativos publicos, no personales."]),
dict(slug="emailrep", nombre="EmailRep", url="https://emailrep.io",
cat="identidad", coste="freemium", reg=True, amb="global", antibot=False,
tags=["email", "reputacion"],
para="Perfil de reputacion de un email: antiguedad, perfiles sociales ligados, si aparece en brechas, si es desechable.",
como="Consulta el email via web o API; devuelve senales de riesgo y presencia.",
gotchas=["API key gratuita con cuota baja.",
"Datos agregados, no siempre actuales."]),
dict(slug="epieos", nombre="Epieos", url="https://epieos.com",
cat="identidad", coste="freemium", reg=False, amb="global", antibot=True,
tags=["email", "telefono", "google-account"],
para="A partir de un email o telefono revela cuenta de Google asociada (nombre, foto, reviews, calendario publico) y servicios vinculados.",
como="Introduce email o telefono; muestra Google account, Gravatar y servicios donde esta registrado.",
gotchas=["Protegido por DataDome (captcha); necesita navegador real.",
"Funciones avanzadas de pago."]),
dict(slug="holehe", nombre="holehe (CLI)", url="https://github.com/megadose/holehe",
cat="identidad", coste="free", reg=False, amb="global", antibot=False,
tags=["email", "cli"],
para="Comprueba en que 120+ servicios esta registrado un email, sin alertar al titular.",
como="`holehe email@dominio`; marca used/not-used por servicio via flujo de recuperacion.",
gotchas=["Algunos modulos quedan obsoletos cuando los sitios cambian su login.",
"Instalacion local requerida."]),
dict(slug="haveibeenpwned", nombre="Have I Been Pwned", url="https://haveibeenpwned.com",
cat="identidad", coste="freemium", reg=False, amb="global", antibot=False,
tags=["brechas", "email"],
para="Indica si un email aparece en brechas de datos publicas conocidas y en cuales.",
como="Introduce el email en la web; la API (para automatizar) es de pago.",
gotchas=["No muestra la contraseña, solo la brecha.",
"API key de pago; el chequeo web es gratis."]),
dict(slug="dehashed", nombre="DeHashed", url="https://dehashed.com",
cat="identidad", coste="pago", reg=True, amb="global", antibot=True,
tags=["brechas", "credenciales"],
para="Buscador de credenciales filtradas por email, usuario, nombre, telefono o IP, con el dato concreto de la brecha.",
como="Busca un selector; muestra registros (a veces con contraseña en claro/hash). Requiere suscripcion.",
gotchas=["Uso de credenciales filtradas: cuidado legal y etico.",
"De pago; WAF, navegador real."]),
dict(slug="leakcheck", nombre="LeakCheck", url="https://leakcheck.io",
cat="identidad", coste="freemium", reg=True, amb="global", antibot=False,
tags=["brechas", "credenciales"],
para="Alternativa a DeHashed: busca apariciones en brechas por email, usuario, telefono o dominio.",
como="Introduce el selector; vista parcial gratis, detalle con plan.",
gotchas=["Mismas precauciones legales que cualquier base de brechas."]),
dict(slug="phoneinfoga", nombre="PhoneInfoga (CLI)", url="https://github.com/sundowndev/phoneinfoga",
cat="identidad", coste="free", reg=False, amb="global", antibot=False,
tags=["telefono", "cli"],
para="Recon pasivo de numeros de telefono: operador, tipo de linea, pais y dorks automaticos para rastrear el numero.",
como="`phoneinfoga scan -n +34...`; genera footprint y enlaces de busqueda.",
gotchas=["No 'hackea' el numero; solo agrega datos publicos.",
"Instalacion local; algunos scanners requieren API keys."]),
dict(slug="truecaller", nombre="Truecaller", url="https://www.truecaller.com",
cat="identidad", coste="freemium", reg=True, amb="global", antibot=False,
tags=["telefono", "identificacion"],
para="Identifica el nombre asociado a un numero de telefono via su base colaborativa.",
como="Buscar el numero en web/app (requiere login). Util para name lookup.",
gotchas=["Requiere cuenta y a veces app movil.",
"Datos aportados por usuarios: pueden ser erroneos o estar desactualizados."]),
# ---------- Social ----------
dict(slug="social-searcher", nombre="Social Searcher", url="https://www.social-searcher.com",
cat="social", coste="freemium", reg=False, amb="global", antibot=False,
tags=["redes", "monitorizacion"],
para="Busca menciones publicas de una palabra/usuario en varias redes a la vez y analiza sentimiento.",
como="Introduce termino/handle; agrega posts publicos recientes.",
gotchas=["Solo contenido publico e indexado recientemente.",
"Cuota gratuita limitada por dia."]),
dict(slug="instaloader", nombre="Instaloader (CLI)", url="https://instaloader.github.io",
cat="social", coste="free", reg=False, amb="global", antibot=False,
tags=["instagram", "cli"],
para="Descarga posts, stories, bio y metadatos publicos de perfiles de Instagram de forma reproducible.",
como="`instaloader profile <usuario>`; baja media + JSON de metadatos.",
gotchas=["Instagram limita por rate y puede pedir login; usar con moderacion.",
"Loguearse con tu cuenta deja de ser pasivo y arriesga baneo."]),
dict(slug="x-advanced-search", nombre="X (Twitter) Advanced Search", url="https://x.com/search-advanced",
cat="social", coste="free", reg=True, amb="global", antibot=False,
tags=["twitter", "x"],
para="Filtra tweets por usuario, fecha, palabras exactas, ubicacion y tipo, sin scrapear.",
como="Usa operadores `from:`, `since:`, `until:`, `near:`, `geocode:`.",
gotchas=["X ahora exige login para ver resultados.",
"Busqueda geo (`geocode:`) cada vez mas limitada."]),
dict(slug="pimeyes", nombre="PimEyes", url="https://pimeyes.com",
cat="social", coste="pago", reg=True, amb="global", antibot=False,
tags=["rostro", "reverse-face"],
para="Buscador de caras: sube una foto y encuentra otras apariciones del mismo rostro en la web.",
como="Sube la imagen del rostro; muestra coincidencias. Ver donde aparecen requiere plan de pago.",
gotchas=["Implicaciones de privacidad serias; uso responsable.",
"Ver URLs de las coincidencias es de pago."]),
dict(slug="facecheck-id", nombre="FaceCheck.ID", url="https://facecheck.id",
cat="social", coste="freemium", reg=True, amb="global", antibot=False,
tags=["rostro", "reverse-face"],
para="Reverse face search orientado a redes y noticias; alternativa a PimEyes.",
como="Sube el rostro; devuelve coincidencias con score. Detalle completo con creditos.",
gotchas=["Resultados con ruido; verificar siempre.",
"Mismas cautelas de privacidad que PimEyes."]),
# ---------- Dominio ----------
dict(slug="shodan", nombre="Shodan", url="https://www.shodan.io",
cat="dominio", coste="freemium", reg=True, amb="global", antibot=False,
tags=["infra", "puertos", "iot"],
para="Motor de busqueda de dispositivos conectados: puertos, servicios, banners, camaras, ICS, ya indexados (no escaneas tu).",
como="Busca `hostname:`, `org:`, `port:`, `country:`. La consulta lee el indice de Shodan, es pasivo.",
gotchas=["Cuenta gratis con filtros/resultados limitados.",
"El dato puede estar cacheado/desactualizado."]),
dict(slug="censys", nombre="Censys Search", url="https://search.censys.io",
cat="dominio", coste="freemium", reg=True, amb="global", antibot=True,
tags=["infra", "certificados", "hosts"],
para="Inventario de hosts y certificados en internet; muy fuerte para mapear infraestructura y certificados de una org.",
como="Busca por host, certificado o dominio; pivota por fingerprint de cert.",
gotchas=["WAF: navegador real.",
"Free tier con cuota de consultas."]),
dict(slug="fofa", nombre="FOFA", url="https://fofa.info",
cat="dominio", coste="freemium", reg=True, amb="global", antibot=False,
tags=["infra", "ciberespacio"],
para="Buscador chino tipo Shodan/Censys; util para encontrar assets que los otros no indexan.",
como="Sintaxis `domain=`, `ip=`, `title=`. Resultados del indice (pasivo).",
gotchas=["Interfaz parcialmente en chino.",
"Free tier limitado."]),
dict(slug="crtsh", nombre="crt.sh", url="https://crt.sh",
cat="dominio", coste="free", reg=False, amb="global", antibot=True,
tags=["certificados", "subdominios"],
para="Consulta Certificate Transparency: revela subdominios de un dominio a partir de los certificados emitidos.",
como="`https://crt.sh/?q=%25.dominio.com` lista todos los subdominios certificados.",
gotchas=["Servidor a veces lento o caido (curl da timeout); reintentar.",
"Solo ve dominios con certificado emitido."]),
dict(slug="securitytrails", nombre="SecurityTrails", url="https://securitytrails.com",
cat="dominio", coste="freemium", reg=True, amb="global", antibot=True,
tags=["dns", "historico", "whois"],
para="DNS e historico WHOIS pasivo: registros DNS actuales e historicos, subdominios, cambios de propietario.",
como="Busca el dominio; ve historico A/MX/NS y subdominios. API para automatizar.",
gotchas=["Free tier muy limitado.",
"WAF: navegador real."]),
dict(slug="dnsdumpster", nombre="DNSDumpster", url="https://dnsdumpster.com",
cat="dominio", coste="free", reg=False, amb="global", antibot=False,
tags=["dns", "subdominios", "mapa"],
para="Mapa DNS gratuito de un dominio: subdominios, registros MX/NS y grafo de hosts.",
como="Introduce el dominio; genera tabla + grafo de la superficie DNS.",
gotchas=["Datos pasivos, no exhaustivos.",
"Limite de consultas por dia."]),
dict(slug="viewdns", nombre="ViewDNS.info", url="https://viewdns.info",
cat="dominio", coste="freemium", reg=False, amb="global", antibot=False,
tags=["dns", "whois", "reverse-ip"],
para="Caja de herramientas DNS/WHOIS: reverse IP (que dominios comparten IP), whois, propagacion, historico.",
como="Elige la herramienta (reverse IP lookup, whois history) e introduce dominio/IP.",
gotchas=["API de pago; web gratis con limites.",
"Reverse IP incompleto en hostings compartidos grandes."]),
dict(slug="urlscan-io", nombre="urlscan.io", url="https://urlscan.io",
cat="dominio", coste="freemium", reg=False, amb="global", antibot=False,
tags=["url", "scan", "screenshot"],
para="Analiza una URL en sandbox: capturas, recursos cargados, dominios contactados, tecnologias. La busqueda de scans previos es 100% pasiva.",
como="Busca el dominio en la pestaña Search para ver scans publicos previos sin lanzar uno nuevo.",
gotchas=["Lanzar un scan publico es visible para terceros; usa 'unlisted/private' si no quieres alertar.",
"Un scan activo toca el sitio objetivo (deja de ser pasivo)."]),
# ---------- Geolocalizacion ----------
dict(slug="yandex-images", nombre="Yandex Images (reverse)", url="https://yandex.com/images/",
cat="geolocalizacion", coste="free", reg=False, amb="global", antibot=False,
tags=["reverse-image", "rostro", "lugar"],
para="La mejor busqueda inversa de imagenes para lugares y rostros; clave para geolocalizar fotos.",
como="Sube la foto o pega su URL; Yandex sugiere imagenes y lugares visualmente similares.",
gotchas=["Sesgo a contenido del este; usar junto a Google Lens.",
"Puede pedir captcha tras varias subidas."]),
dict(slug="google-lens", nombre="Google Lens", url="https://lens.google.com",
cat="geolocalizacion", coste="free", reg=False, amb="global", antibot=False,
tags=["reverse-image", "objetos", "texto"],
para="Reconoce objetos, texto, productos y lugares en una imagen; bueno para identificar carteles, logos, edificios.",
como="Sube la imagen; recorta la zona de interes (cartel, fachada) para afinar.",
gotchas=["Mejor para objetos/texto que para coincidencia exacta de foto.",
"Combinar con Yandex para lugares."]),
dict(slug="bing-visual-search", nombre="Bing Visual Search", url="https://www.bing.com/visualsearch",
cat="geolocalizacion", coste="free", reg=False, amb="global", antibot=False,
tags=["reverse-image"],
para="Tercera busqueda inversa para contrastar Yandex y Google.",
como="Sube la imagen; revisa coincidencias y paginas que la contienen.",
gotchas=["Cobertura menor; usar como tercera opinion."]),
dict(slug="tineye", nombre="TinEye", url="https://tineye.com",
cat="geolocalizacion", coste="freemium", reg=False, amb="global", antibot=False,
tags=["reverse-image", "origen"],
para="Reverse image enfocado al ORIGEN y primera aparicion de una imagen (no a contenido similar).",
como="Sube la imagen; ordena por 'oldest' para encontrar la fuente original.",
gotchas=["No busca caras/objetos similares, solo la misma imagen y ediciones.",
"Indice mas pequeño que Yandex."]),
dict(slug="geospy", nombre="GeoSpy AI", url="https://geospy.ai",
cat="geolocalizacion", coste="freemium", reg=True, amb="global", antibot=False,
tags=["geolocalizacion", "ia", "lugar"],
para="Estima la ubicacion de una foto mediante IA, incluso sin metadatos, a partir de pistas visuales.",
como="Sube la imagen; devuelve una hipotesis de pais/ciudad con probabilidad.",
gotchas=["Estimacion probabilistica: verificar siempre con mapas/street view.",
"Acceso completo de pago."]),
dict(slug="google-earth", nombre="Google Earth Pro", url="https://earth.google.com",
cat="geolocalizacion", coste="free", reg=False, amb="global", antibot=False,
tags=["satelite", "historico", "3d"],
para="Imagenes satelitales con HISTORICO temporal y vista 3D; clave para datar cambios y medir.",
como="Usa la barra de tiempo para ver la misma zona en distintas fechas; mide distancias y alturas.",
gotchas=["Resolucion y fechas varian mucho por zona.",
"Version Pro de escritorio tiene mas herramientas que la web."]),
dict(slug="google-maps", nombre="Google Maps + Street View", url="https://maps.google.com",
cat="geolocalizacion", coste="free", reg=False, amb="global", antibot=False,
tags=["mapa", "street-view", "negocios"],
para="Mapa, vista de calle con historico, reseñas y fotos de usuarios para confirmar y datar una ubicacion.",
como="En Street View usa el reloj para ver capturas antiguas; revisa fotos de reseñas para interiores.",
gotchas=["Street View no cubre todo; combinar con Mapillary.",
"Las fotos de usuarios pueden estar mal ubicadas."]),
dict(slug="bing-maps", nombre="Bing Maps (Bird's Eye)", url="https://www.bing.com/maps",
cat="geolocalizacion", coste="free", reg=False, amb="global", antibot=False,
tags=["mapa", "oblicua", "satelite"],
para="Vista oblicua 'Bird's Eye' (45 grados) que muestra fachadas que el cenital no ve.",
como="Activa 'Bird's Eye'; rota la vista para ver los cuatro lados de un edificio.",
gotchas=["Cobertura oblicua limitada a ciertas ciudades."]),
dict(slug="yandex-maps", nombre="Yandex Maps", url="https://yandex.com/maps",
cat="geolocalizacion", coste="free", reg=False, amb="global", antibot=False,
tags=["mapa", "street-view"],
para="Street view (panoramas) y mapas con cobertura fuerte en Rusia, Turquia, Asia central y este de Europa.",
como="Activa panoramas; util donde Google Street View no llega.",
gotchas=["Cobertura floja en Europa occidental.",
"Interfaz parcialmente en ruso."]),
dict(slug="mapillary", nombre="Mapillary", url="https://www.mapillary.com",
cat="geolocalizacion", coste="free", reg=False, amb="global", antibot=True,
tags=["street-level", "crowdsourced"],
para="Imagenes a nivel de calle aportadas por usuarios; cubre rutas y zonas que Street View ignora.",
como="Navega el mapa; las lineas verdes son tramos con fotos. Filtra por fecha.",
gotchas=["Calidad y cobertura desiguales (depende de aportaciones).",
"WAF en la home; navegador real."]),
dict(slug="kartaview", nombre="KartaView", url="https://kartaview.org",
cat="geolocalizacion", coste="free", reg=False, amb="global", antibot=False,
tags=["street-level", "crowdsourced", "open"],
para="Alternativa abierta a Mapillary con imagenes a nivel de calle crowdsourced.",
como="Explora el mapa para tramos fotografiados; descarga libre.",
gotchas=["Cobertura menor que Mapillary."]),
dict(slug="openstreetmap", nombre="OpenStreetMap", url="https://www.openstreetmap.org",
cat="geolocalizacion", coste="free", reg=False, amb="global", antibot=False,
tags=["mapa", "open", "datos"],
para="Mapa libre con datos crudos etiquetados (tipo de edificio, comercio, altura) consultables.",
como="Usa el editor/inspector para ver tags de un elemento; base para Overpass.",
gotchas=["Detalle depende de la comunidad local."]),
dict(slug="overpass-turbo", nombre="Overpass Turbo", url="https://overpass-turbo.eu",
cat="geolocalizacion", coste="free", reg=False, amb="global", antibot=False,
tags=["consulta", "osm", "features"],
para="Consulta los datos de OSM por tipo de objeto: 'todas las gasolineras / antenas / iglesias en esta zona'.",
como="Escribe una query Overpass QL y ejecutala sobre el area visible; ideal para acotar candidatos al geolocalizar.",
gotchas=["Curva de aprendizaje de la sintaxis QL.",
"Solo encuentra lo que esta etiquetado en OSM."]),
dict(slug="wikimapia", nombre="Wikimapia", url="https://wikimapia.org",
cat="geolocalizacion", coste="free", reg=False, amb="global", antibot=True,
tags=["mapa", "crowdsourced", "etiquetas"],
para="Mapa colaborativo con descripciones de lugares y edificios que otros mapas no etiquetan.",
como="Navega y lee las descripciones de los poligonos dibujados por usuarios.",
gotchas=["Informacion sin verificar; tratar como pista.",
"WAF; navegador real."]),
dict(slug="sentinel-eo-browser", nombre="Sentinel Hub EO Browser", url="https://apps.sentinel-hub.com/eo-browser/",
cat="geolocalizacion", coste="free", reg=True, amb="global", antibot=False,
tags=["satelite", "sentinel", "multiespectral"],
para="Imagenes Sentinel/Landsat recientes y multiespectrales; ver cambios, incendios, agua, vegetacion por fecha.",
como="Selecciona zona y fecha; cambia entre bandas (true color, NDVI, etc.).",
gotchas=["Resolucion ~10m (no ve coches/personas).",
"Requiere cuenta gratuita para algunas capas."]),
dict(slug="zoom-earth", nombre="Zoom Earth", url="https://zoom.earth",
cat="geolocalizacion", coste="free", reg=False, amb="global", antibot=False,
tags=["satelite", "tiempo-real", "clima"],
para="Vista satelital casi en tiempo real con capas meteorologicas (nubes, tormentas, incendios).",
como="Navega el globo; util para contexto temporal/meteo de un evento.",
gotchas=["Resolucion baja; para contexto, no detalle."]),
dict(slug="nasa-worldview", nombre="NASA Worldview", url="https://worldview.earthdata.nasa.gov",
cat="geolocalizacion", coste="free", reg=False, amb="global", antibot=False,
tags=["satelite", "historico", "nasa"],
para="Imagenes satelitales diarias historicas de la NASA (MODIS/VIIRS) con barra temporal global.",
como="Elige fecha y capa; descarga la imagen del dia para una region.",
gotchas=["Resolucion baja (cientos de m); para fenomenos grandes."]),
dict(slug="suncalc", nombre="SunCalc", url="https://www.suncalc.org",
cat="geolocalizacion", coste="free", reg=False, amb="global", antibot=False,
tags=["chronolocation", "sol", "sombras"],
para="Calcula la posicion del sol y direccion de las sombras para una ubicacion y hora dadas (chronolocation).",
como="Fija el punto en el mapa y la fecha; ajusta la hora hasta que la sombra coincida con la de la foto.",
gotchas=["Necesitas estimar bien la fecha/estacion.",
"Funciona al reves: deducir la hora a partir de la sombra observada."]),
dict(slug="shadowmap", nombre="Shadowmap", url="https://shadowmap.org",
cat="geolocalizacion", coste="freemium", reg=False, amb="global", antibot=False,
tags=["chronolocation", "sombras", "3d"],
para="Simula sombras de edificios en 3D segun hora/fecha; util para chronolocation en ciudad.",
como="Coloca la vista en la zona y desliza la hora para comparar sombras de edificios.",
gotchas=["Modelo 3D solo en ciudades grandes.",
"Funciones avanzadas de pago."]),
dict(slug="exiftool", nombre="ExifTool (CLI)", url="https://exiftool.org",
cat="geolocalizacion", coste="free", reg=False, amb="global", antibot=False,
tags=["metadatos", "exif", "cli"],
para="Lee todos los metadatos de una imagen/archivo: GPS, fecha, camara, software. El estandar para EXIF.",
como="`exiftool foto.jpg`; busca GPSLatitude/Longitude y DateTimeOriginal.",
gotchas=["Las redes sociales borran EXIF al subir; suele haber GPS solo en originales.",
"Instalacion local (perl)."]),
dict(slug="metadata2go", nombre="Metadata2Go", url="https://www.metadata2go.com",
cat="geolocalizacion", coste="free", reg=False, amb="global", antibot=False,
tags=["metadatos", "exif", "online"],
para="Visor EXIF online sin instalar nada: sube el archivo y lee sus metadatos.",
como="Sube la imagen; muestra GPS, fecha y datos de camara.",
gotchas=["Subes el archivo a un tercero: no usar con material sensible.",
"Preferir ExifTool local para evidencia."]),
dict(slug="jimpl", nombre="Jimpl", url="https://jimpl.com",
cat="geolocalizacion", coste="free", reg=False, amb="global", antibot=False,
tags=["metadatos", "exif", "gps", "online"],
para="Visor EXIF online que situa la coordenada GPS de la foto directamente en un mapa.",
como="Sube la imagen; si tiene GPS, lo pinta en el mapa.",
gotchas=["Mismo aviso de privacidad: subes a un tercero."]),
dict(slug="ipinfo", nombre="IPinfo", url="https://ipinfo.io",
cat="geolocalizacion", coste="freemium", reg=True, amb="global", antibot=False,
tags=["ip-geo", "asn"],
para="Geolocalizacion de IP, ASN, organizacion y tipo de conexion (hosting/movil/vpn).",
como="`ipinfo.io/<ip>` en web o API; util para ubicar el origen aproximado de una IP.",
gotchas=["Geo de IP es aproximada (ciudad/region, no direccion).",
"VPN/proxy falsean la ubicacion."]),
dict(slug="ipgeolocation", nombre="ipgeolocation.io", url="https://ipgeolocation.io",
cat="geolocalizacion", coste="freemium", reg=True, amb="global", antibot=False,
tags=["ip-geo", "api"],
para="API de geolocalizacion de IP con zona horaria, moneda y datos de red.",
como="Consulta por IP via API; devuelve pais, region, ciudad aproximada.",
gotchas=["Precision limitada; varias fuentes discrepan.",
"Requiere API key."]),
dict(slug="maxmind", nombre="MaxMind GeoIP", url="https://www.maxmind.com",
cat="geolocalizacion", coste="freemium", reg=True, amb="global", antibot=False,
tags=["ip-geo", "base-datos"],
para="Base de datos GeoIP estandar de la industria (GeoLite2 gratis) para resolver IP a ubicacion offline.",
como="Descarga GeoLite2 (cuenta gratis) y consulta localmente; o usa su web demo.",
gotchas=["GeoLite2 menos precisa que la version de pago.",
"Requiere cuenta para descargar las DB."]),
# ---------- Imagen forense ----------
dict(slug="invid-weverify", nombre="InVID / WeVerify", url="https://www.invid-project.eu",
cat="imagen-forense", coste="free", reg=False, amb="global", antibot=False,
tags=["video", "verificacion", "frames", "extension"],
para="Extension del navegador para verificar videos/imagenes: extrae frames, hace reverse search, lee metadatos y detecta manipulaciones.",
como="Instala la extension (Fake News Debunker); pega la URL del video para sacar keyframes y buscarlos.",
gotchas=["Es una extension de navegador, no una web de consulta directa.",
"Algunas integraciones de redes se rompen cuando cambian sus APIs."]),
dict(slug="forensically", nombre="Forensically", url="https://29a.ch/photo-forensics/",
cat="imagen-forense", coste="free", reg=False, amb="global", antibot=False,
tags=["forense", "ela", "clonado"],
para="Suite forense de imagen en el navegador: ELA, deteccion de clonado, analisis de ruido, lupa de detalle.",
como="Sube la foto; usa Clone Detection y Error Level Analysis para detectar zonas editadas.",
gotchas=["ELA da pistas, no pruebas; interpretar con cuidado.",
"Funciona en local en el navegador (no sube la imagen)."]),
dict(slug="fotoforensics", nombre="FotoForensics", url="https://fotoforensics.com",
cat="imagen-forense", coste="free", reg=False, amb="global", antibot=False,
tags=["forense", "ela"],
para="Analisis ELA online clasico para detectar montajes en imagenes JPEG.",
como="Sube la imagen; las zonas con distinto nivel de error sugieren edicion.",
gotchas=["Subes la imagen a un tercero (queda publica un tiempo).",
"ELA tiene muchos falsos positivos en imagenes recomprimidas."]),
# ---------- Archivo ----------
dict(slug="wayback-machine", nombre="Wayback Machine", url="https://web.archive.org",
cat="archivo", coste="free", reg=False, amb="global", antibot=False,
tags=["archivo", "historico", "web"],
para="Archivo historico de paginas web: ver como era un sitio/perfil en una fecha pasada.",
como="Pega la URL; navega por las capturas en la linea de tiempo. Permite guardar una captura nueva.",
gotchas=["No todo esta archivado ni en todas las fechas.",
"robots.txt o peticiones de borrado pueden ocultar capturas."]),
dict(slug="archive-today", nombre="archive.today", url="https://archive.ph",
cat="archivo", coste="free", reg=False, amb="global", antibot=False,
tags=["archivo", "snapshot"],
para="Captura puntual de una pagina (incluye contenido dinamico) que queda congelada e inmune a borrados.",
como="Pega la URL para crear o buscar una captura; util para preservar evidencia antes de que se borre.",
gotchas=["Capturas puntuales, no rastreo continuo como Wayback.",
"Dominios espejo varios (.ph/.is/.today)."]),
dict(slug="cachedview", nombre="CachedView", url="https://cachedview.nl",
cat="archivo", coste="free", reg=False, amb="global", antibot=False,
tags=["cache", "google"],
para="Atajo para ver versiones cacheadas de una pagina (Google Cache, Wayback) en un sitio.",
como="Pega la URL; ofrece enlaces a las caches disponibles.",
gotchas=["Google Cache esta practicamente retirado; cada vez menos util.",
"Depende de que el tercero conserve la copia."]),
# ---------- Empresa ES ----------
dict(slug="catastro", nombre="Sede Catastro", url="https://www.sedecatastro.gob.es",
cat="empresa-es", coste="free", reg=False, amb="espana", antibot=False,
tags=["inmueble", "catastro", "geo"],
para="Catastro español: localiza inmuebles por direccion o referencia catastral, con superficie, uso, año y geometria sobre mapa.",
como="Busca por direccion; obtiene referencia catastral, plano y datos del inmueble (titular parcial sin certificado).",
gotchas=["El titular completo requiere certificado/identificacion y acreditar interes.",
"Cruzar referencia catastral con la direccion del objetivo."]),
dict(slug="borme", nombre="BORME (BOE)", url="https://www.boe.es/diario_borme/",
cat="empresa-es", coste="free", reg=False, amb="espana", antibot=False,
tags=["mercantil", "empresa", "boletin"],
para="Boletin Oficial del Registro Mercantil: constituciones, nombramientos, ceses y cambios de empresas españolas.",
como="Busca por nombre de persona o sociedad; revela en que empresas figura alguien como administrador.",
gotchas=["URL correcta es /diario_borme/ (la /borme/ da 404).",
"Datos historicos por fecha de publicacion."]),
dict(slug="libreborme", nombre="Libreborme", url="https://libreborme.net",
cat="empresa-es", coste="free", reg=False, amb="espana", antibot=True,
tags=["mercantil", "empresa", "buscador"],
para="Interfaz buscable sobre los datos del BORME: ficha de persona/empresa con sus cargos y relaciones.",
como="Busca el nombre; ve sus sociedades, cargos y co-administradores de un vistazo.",
gotchas=["Cobertura desde ~2009; datos antiguos pueden faltar.",
"WAF; navegador real."]),
dict(slug="einforma", nombre="eInforma", url="https://www.einforma.com",
cat="empresa-es", coste="freemium", reg=True, amb="espana", antibot=False,
tags=["mercantil", "empresa", "informe"],
para="Informes mercantiles de empresas españolas: cargos, cuentas, CIF, vinculaciones.",
como="Busca la empresa o el administrador; datos basicos gratis, informe completo de pago.",
gotchas=["Lo detallado es de pago.",
"Requiere cuenta."]),
dict(slug="axesor", nombre="Axesor", url="https://www.axesor.es",
cat="empresa-es", coste="freemium", reg=True, amb="espana", antibot=False,
tags=["mercantil", "empresa", "rating"],
para="Informacion mercantil y de riesgo de empresas españolas; alternativa a eInforma.",
como="Busca empresa/persona; ficha basica gratis, informe financiero de pago.",
gotchas=["Datos detallados de pago.",
"Requiere cuenta."]),
dict(slug="infoempresa", nombre="InfoEmpresa", url="https://www.infoempresa.com",
cat="empresa-es", coste="freemium", reg=False, amb="espana", antibot=False,
tags=["mercantil", "empresa"],
para="Buscador de empresas españolas con administradores, objeto social y datos de contacto.",
como="Busca por nombre de empresa o de persona para ver sus cargos.",
gotchas=["Parte de la ficha de pago.",
"Datos a veces desactualizados."]),
dict(slug="idealista", nombre="Idealista", url="https://www.idealista.com",
cat="empresa-es", coste="free", reg=False, amb="espana", antibot=True,
tags=["inmueble", "fotos", "geo"],
para="Portal inmobiliario: fotos y planos de viviendas que permiten cruzar una direccion con el interior y el entorno.",
como="Busca por zona/direccion; las fotos del anuncio revelan interior, vistas y referencias del lugar.",
gotchas=["Anuncios caducan; usar Wayback para los retirados.",
"WAF (DataDome); navegador real."]),
dict(slug="fotocasa", nombre="Fotocasa", url="https://www.fotocasa.es",
cat="empresa-es", coste="free", reg=False, amb="espana", antibot=False,
tags=["inmueble", "fotos", "geo"],
para="Segundo portal inmobiliario español; util para contrastar fotos/anuncios con Idealista.",
como="Busca por zona; cruza fotos y precios con el otro portal.",
gotchas=["Cobertura solapada con Idealista; usar ambos."]),
dict(slug="infobel", nombre="Infobel", url="https://www.infobel.com/es",
cat="empresa-es", coste="free", reg=False, amb="espana", antibot=True,
tags=["telefono", "directorio", "paginas-blancas"],
para="Paginas blancas/amarillas online: cruza nombre, telefono y direccion en España y otros paises.",
como="Busca por nombre o telefono para resolver el otro dato.",
gotchas=["Cobertura desigual; muchos numeros no listados (LOPD).",
"WAF; navegador real."]),
# ---------- Frameworks ----------
dict(slug="osint-framework", nombre="OSINT Framework", url="https://osintframework.com",
cat="framework", coste="free", reg=False, amb="global", antibot=False,
tags=["indice", "arbol", "recursos"],
para="Arbol navegable de cientos de recursos OSINT clasificados por tipo de dato (email, username, dominio, geo...).",
como="Navega el arbol por categoria para descubrir herramientas especificas para cada dato.",
gotchas=["Algunos enlaces estan muertos o desactualizados.",
"Es un indice, no ejecuta nada."]),
dict(slug="bellingcat-toolkit", nombre="Bellingcat's Online Toolkit", url="https://bellingcat.gitbook.io/toolkit",
cat="framework", coste="free", reg=False, amb="global", antibot=False,
tags=["indice", "investigacion", "geo"],
para="Coleccion curada por Bellingcat de herramientas para investigacion, con fuerte enfasis en geolocalizacion y verificacion.",
como="Busca por categoria (maps, satellite, social) la herramienta recomendada y probada por investigadores.",
gotchas=["Curada pero amplia; empezar por las marcadas como favoritas."]),
dict(slug="inteltechniques", nombre="IntelTechniques Tools", url="https://inteltechniques.com/tools/",
cat="framework", coste="free", reg=False, amb="global", antibot=False,
tags=["indice", "formularios", "bazzell"],
para="Conjunto de formularios de busqueda (Michael Bazzell) que automatizan consultas por email, username, telefono, etc.",
como="Abre la herramienta del dato que tengas; rellena y lanza busquedas en multiples servicios.",
gotchas=["Algunas herramientas se rompen cuando los servicios cambian sus URLs.",
"Parte del contenido premium esta en sus libros."]),
dict(slug="start-me-osint", nombre="Start.me — OSINT Collection", url="https://start.me/p/DPYPMz/the-ultimate-osint-collection",
cat="framework", coste="free", reg=False, amb="global", antibot=True,
tags=["indice", "enlaces", "dashboard"],
para="Dashboard comunitario con cientos de enlaces OSINT agrupados por tema; bueno para descubrir fuentes nuevas.",
como="Navega los paneles por categoria; marca los enlaces utiles.",
gotchas=["Calidad variable (es comunitario).",
"WAF; navegador real."]),
]
# ============================================================================
# Grupo recon: funciones del registry (NO sitios web) que se ejecutan con
# `fn run` y archivan su resultado por objetivo en el vault + DuckDB.
# docs/capabilities/recon.md + projects/osint/CONVENTIONS.md §9.
# ============================================================================
RECON_TOOLS = [
dict(slug="whois-lookup", nombre="whois_lookup", rid="whois_lookup_py_cybersecurity",
modo="pasivo", sudo=False, persiste=True,
comando="fn run recon_osint <target> whois",
para="Lookup WHOIS de un dominio o IP: registrar, pais del registrante, fechas (creacion/expiracion/actualizacion) y name servers, parseados best-effort sobre el raw.",
gotchas=["Pasivo (consulta el registro, no toca al objetivo).",
"El parseo depende del formato del registrar; el `raw` completo siempre se guarda."]),
dict(slug="rdap-lookup", nombre="rdap_lookup", rid="rdap_lookup_py_cybersecurity",
modo="pasivo", sudo=False, persiste=True,
comando="fn run recon_osint <target> rdap",
para="Lookup RDAP (sustituto JSON moderno de WHOIS) de dominio, IP o ASN (p.ej. AS15169); devuelve datos estructurados.",
gotchas=["Pasivo.",
"Mas fiable de parsear que WHOIS; preferir cuando el TLD lo soporta."]),
dict(slug="dns-records", nombre="dns_records", rid="dns_records_py_cybersecurity",
modo="pasivo", sudo=False, persiste=True,
comando="fn run recon_osint <target> dns",
para="Registros DNS via `dig +short` (A, AAAA, MX, NS, SOA, TXT, CNAME por defecto) en un dict por tipo.",
gotchas=["Pasivo (consulta a resolvers DNS, no al objetivo).",
"Resultados dependen del resolver/cache local."]),
dict(slug="ping-host", nombre="ping_host", rid="ping_host_py_cybersecurity",
modo="activo", sudo=False, persiste=True,
comando="fn run recon_osint <target> ping",
para="Sondeo ICMP: porcentaje de perdida y RTT (avg/min/max) hacia el host.",
gotchas=["ACTIVO: envia paquetes al objetivo.",
"Host filtrado por firewall = loss 100% pero status:ok (no es error)."]),
dict(slug="traceroute-host", nombre="traceroute_host", rid="traceroute_host_py_cybersecurity",
modo="activo", sudo=False, persiste=True,
comando="fn run recon_osint <target> traceroute",
para="Traza la ruta de red hasta el host: lista de hops con nombre/IP/RTT.",
gotchas=["ACTIVO: genera trafico hacia el objetivo y la ruta intermedia.",
"Hops sin respuesta (`* * *`) salen con hosts vacios."]),
dict(slug="nmap-scan", nombre="nmap_scan", rid="nmap_scan_py_cybersecurity",
modo="activo", sudo="perfiles os/udp-top/aggressive", persiste=True,
comando="fn run recon_osint <target> nmap # perfil quick por defecto",
para="Escaneo de puertos y servicios con nmap por perfiles (quick, top1000, service -sV -sC, vuln, udp-top, aggressive -A, discovery CIDR, os). Salida XML parseada a open_ports/hosts_up.",
gotchas=["ACTIVO E INTRUSIVO: solo contra objetivos PROPIOS o con autorizacion explicita. Escanear infra ajena puede ser ilegal.",
"Perfiles os/udp-top/aggressive requieren sudo.",
"Perfiles largos (vuln, aggressive) → lanzar en segundo plano (&/background).",
"discovery acepta CIDR: cuidado de no barrer rangos que no son tuyos."]),
dict(slug="save-scan-to-osint", nombre="save_scan_to_osint", rid="save_scan_to_osint_py_cybersecurity",
modo="sink", sudo=False, persiste=True,
comando="# se invoca dentro del pipeline; uso directo para archivar un raw externo",
para="Sink comun: archiva el resultado de CUALQUIER scan en el ecosistema OSINT. Dos capas: nota Markdown tipada en el vault (siempre) + POST a osint_db para registro DuckDB (best-effort).",
gotchas=["Si el service osint_db esta caido o el endpoint da 404, degrada a solo-nota (register_warning) sin fallar.",
"No lanza excepciones: devuelve dict de estado con note_path/registered/scan_id."]),
dict(slug="recon-osint", nombre="recon_osint (pipeline)", rid="recon_osint_py_pipelines",
modo="segun scan", sudo="si el scan lo pide", persiste=True,
comando="fn run recon_osint <target> <whois|rdap|dns|ping|traceroute|nmap>",
para="Pipeline one-shot: ejecuta un scan del tipo pedido y lo archiva en OSINT en una sola llamada. El camino canonico para recon + archivado por objetivo.",
gotchas=["Hereda el modo del scan elegido (whois/rdap/dns pasivos; ping/traceroute/nmap activos).",
"`save=False` ejecuta sin archivar (raro; por defecto archiva)."]),
]
RECON_PERSIST_LINES = [
"- **Nota** (siempre, fuente de verdad): `~/Obsidian/osint/dominios/<slug>/recon/<tipo>-<ts>.md` (`tipo: scan-red`, raw en bloque de codigo).",
"- **DuckDB** (best-effort): tabla `network_scans` en `osint_db` via `POST 127.0.0.1:8771/api/scan`.",
"- **Consultar** (code block `osintdb` del plugin): `SELECT * FROM network_scans WHERE target_slug='<slug>';`",
"- Service `osint_db` caido → degrada a solo-nota sin fallar.",
]
def build_recon_frontmatter(t):
return {
"tipo": "herramienta",
"nombre": t["nombre"],
"slug": t["slug"],
"registry_id": t["rid"],
"categoria": "recon",
"osint_modo": t["modo"],
"coste": "free",
"comando": t["comando"],
"requiere_sudo": t["sudo"],
"persiste_en_vault": bool(t["persiste"]),
"tags": ["herramienta", "osint", "recon", "registry", "red"],
}
def build_recon_body(t):
lines = []
lines.append("> Funcion del registry (no es un sitio web): se ejecuta con `fn run` y archiva el resultado por objetivo.")
lines.append("")
lines.append("## Para que")
lines.append("")
lines.append(t["para"])
lines.append("")
lines.append("## Como llamar")
lines.append("")
lines.append("```bash")
lines.append(t["comando"])
lines.append("```")
lines.append("")
lines.append(f"Registry ID: `{t['rid']}`. Inspeccionar: `mcp__registry__fn_show id=\"{t['rid']}\"`.")
lines.append("")
lines.append("## Persistencia (resultados por objetivo)")
lines.append("")
if t["modo"] == "sink":
lines.append("Es el sink que escribe la nota y registra en DuckDB:")
else:
lines.append("Al pasar por el pipeline `recon_osint` (o por `save_scan_to_osint`):")
lines.append("")
lines.extend(RECON_PERSIST_LINES)
lines.append("")
lines.append("## Gotchas")
lines.append("")
for g in t["gotchas"]:
lines.append(f"- {g}")
return "\n".join(lines)
def build_body(t):
lines = []
lines.append("## Para que")
lines.append("")
lines.append(t["para"])
lines.append("")
lines.append("## Como usar")
lines.append("")
lines.append(t["como"])
lines.append("")
lines.append("## Gotchas")
lines.append("")
for g in t["gotchas"]:
lines.append(f"- {g}")
return "\n".join(lines)
def build_frontmatter(t):
fm = {
"tipo": "herramienta",
"nombre": t["nombre"],
"slug": t["slug"],
"url": t["url"],
"categoria": t["cat"],
"osint_modo": "pasivo",
"coste": t["coste"],
"registro": bool(t["reg"]),
"ambito": t["amb"],
"anti_bot": bool(t["antibot"]),
"tags": ["herramienta", "osint", t["cat"]] + t["tags"],
}
return fm
def main():
created = []
# Validar slugs unicos (web + recon no colisionan).
slugs = [t["slug"] for t in TOOLS] + [t["slug"] for t in RECON_TOOLS]
assert len(slugs) == len(set(slugs)), "slugs duplicados: " + str(
[s for s in slugs if slugs.count(s) > 1]
)
for t in TOOLS:
rel = f"herramientas/{t['slug']}.md"
path = create_obsidian_note(
vault_dir=VAULT,
rel_path=rel,
body=build_body(t),
frontmatter=build_frontmatter(t),
overwrite=True,
)
created.append((t["cat"], t["slug"], path))
for t in RECON_TOOLS:
rel = f"herramientas/{t['slug']}.md"
path = create_obsidian_note(
vault_dir=VAULT,
rel_path=rel,
body=build_recon_body(t),
frontmatter=build_recon_frontmatter(t),
overwrite=True,
)
created.append(("recon", t["slug"], path))
# ----- MOC index -----
moc_lines = []
moc_lines.append("Indice de herramientas online para investigaciones OSINT pasivo.")
moc_lines.append("")
moc_lines.append(
"Cada herramienta tiene su ficha con `## Para que`, `## Como usar` y `## Gotchas`. "
"El campo `anti_bot: true` marca las que bloquean clientes simples y necesitan un navegador real "
"(perfil Chromium dedicado). Modo de uso por defecto: **pasivo** (no se interactua con los sistemas del objetivo)."
)
moc_lines.append("")
total = len(TOOLS) + len(RECON_TOOLS)
moc_lines.append(
f"**Total: {total} herramientas** ({len(TOOLS)} sitios/CLIs externos pasivos "
f"+ {len(RECON_TOOLS)} funciones del registry del grupo `recon`, estas ultimas ejecutables "
f"con `fn run` y con archivado por objetivo)."
)
moc_lines.append("")
for cat in CAT_ORDER:
items = [t for t in TOOLS if t["cat"] == cat]
if not items:
continue
moc_lines.append(f"## {CAT_LABELS[cat]}")
moc_lines.append("")
moc_lines.append("| Herramienta | Coste | Cuenta | Ambito | Para que |")
moc_lines.append("|---|---|---|---|---|")
for t in items:
cuenta = "si" if t["reg"] else "no"
ab = " ⚠️navegador" if t["antibot"] else ""
link = f"[[{t['slug']}\\|{t['nombre']}]]"
resumen = t["para"].strip()
moc_lines.append(
f"| {link}{ab} | {t['coste']} | {cuenta} | {t['amb']} | {resumen} |"
)
moc_lines.append("")
# ----- Seccion recon (funciones del registry, no sitios web) -----
moc_lines.append("## Recon de red (registry, ejecutable)")
moc_lines.append("")
moc_lines.append(
"Estas NO son sitios web: son funciones del registry (grupo `recon`, dominio `cybersecurity`) "
"que se ejecutan con `fn run` y que **archivan su resultado por objetivo** en el vault + DuckDB. "
"Doctrina: *todo escaneo se guarda siempre*. Pagina madre: `docs/capabilities/recon.md`."
)
moc_lines.append("")
moc_lines.append("| Herramienta | Registry ID | Modo | Sudo | Para que |")
moc_lines.append("|---|---|---|---|---|")
for t in RECON_TOOLS:
sudo = t["sudo"] if isinstance(t["sudo"], str) else ("si" if t["sudo"] else "no")
modo_tag = " ⚠️activo" if t["modo"] == "activo" else ""
link = f"[[{t['slug']}\\|{t['nombre']}]]"
moc_lines.append(
f"| {link}{modo_tag} | `{t['rid']}` | {t['modo']} | {sudo} | {t['para'].strip()} |"
)
moc_lines.append("")
moc_lines.append("**Patron canonico (recon + archivado por objetivo en 1 call):**")
moc_lines.append("")
moc_lines.append("```bash")
moc_lines.append("fn run recon_osint <target> <whois|rdap|dns|ping|traceroute|nmap>")
moc_lines.append("```")
moc_lines.append("")
moc_lines.append("Cada call deja:")
moc_lines.extend(RECON_PERSIST_LINES)
moc_lines.append("")
moc_lines.append("## Notas de uso")
moc_lines.append("")
moc_lines.append(
"- **Pasivo vs activo:** todo este indice es OSINT pasivo (fuentes publicas y de terceros). "
"Lanzar un scan que toque el sistema del objetivo (p.ej. un scan nuevo en urlscan, loguearse en una cuenta) "
"deja de ser pasivo."
)
moc_lines.append(
"- **Geolocalizacion de una foto (flujo tipico):** 1) `[[exiftool]]`/`[[jimpl]]` por si hay GPS en metadatos; "
"2) reverse image con `[[yandex-images]]` + `[[google-lens]]`; 3) confirmar en `[[google-maps]]`/`[[google-earth]]` + `[[mapillary]]`; "
"4) datar por sombras con `[[suncalc]]`/`[[shadowmap]]`; 5) acotar features con `[[overpass-turbo]]`."
)
moc_lines.append(
"- **España:** para inmuebles/direcciones cruzar `[[catastro]]` + `[[idealista]]`/`[[fotocasa]]`; "
"para empresas y cargos `[[borme]]`/`[[libreborme]]`."
)
moc_lines.append(
"- **Brechas y credenciales** (`[[dehashed]]`, `[[leakcheck]]`, `[[intelligence-x]]`): manejar con cautela legal/etica."
)
moc_lines.append(
"- **Recon de red:** `[[ping-host]]`, `[[traceroute-host]]` y sobre todo `[[nmap-scan]]` son ACTIVOS "
"(tocan al objetivo). Solo contra infra PROPIA o con autorizacion explicita. `[[whois-lookup]]`/`[[rdap-lookup]]`/`[[dns-records]]` "
"son pasivos. Todo lo que ejecutes con `[[recon-osint]]` queda archivado por objetivo en el vault."
)
moc_fm = {
"tipo": "index",
"tags": ["osint", "moc", "herramientas"],
}
moc_path = create_obsidian_note(
vault_dir=VAULT,
rel_path="herramientas/_indice.md",
body="\n".join(moc_lines),
frontmatter=moc_fm,
overwrite=True,
)
print(f"Fichas creadas: {len(created)}")
print(f"MOC: {moc_path}")
# Resumen por categoria
from collections import Counter
c = Counter(cat for cat, _, _ in created)
for cat in CAT_ORDER:
if c.get(cat):
print(f" {cat:18} {c[cat]}")
if __name__ == "__main__":
main()