vamp-darkweb-intel 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,229 @@
1
+ Metadata-Version: 2.4
2
+ Name: vamp-darkweb-intel
3
+ Version: 1.0.0
4
+ Summary: Darkweb & threat intelligence CLI — cross-source IOC lookup with automatic correlation across ThreatFox, URLhaus, MalwareBazaar, RansomLook, GreyNoise, Shodan and more
5
+ Author-email: VampSecure Studios <contact@vampsecurestudios.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Vampsecure-Labs/vamp-darkweb-intel
8
+ Project-URL: Repository, https://github.com/Vampsecure-Labs/vamp-darkweb-intel
9
+ Keywords: threat-intelligence,darkweb,ioc,ransomware,malware,osint,security,pentest
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Information Technology
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Security
17
+ Classifier: Topic :: Internet
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: rich>=13.7.0
22
+ Provides-Extra: tor
23
+ Requires-Dist: requests[socks]>=2.31.0; extra == "tor"
24
+ Requires-Dist: stem>=1.8.2; extra == "tor"
25
+ Dynamic: license-file
26
+
27
+ <h1 align="center">vamp-darkweb-intel</h1>
28
+
29
+ <p align="center">
30
+ <img src="https://img.shields.io/badge/python-3.9%2B-blue?logo=python&logoColor=white" alt="Python 3.9+"/>
31
+ <img src="https://img.shields.io/badge/platform-linux%20%7C%20macOS%20%7C%20windows-lightgrey" alt="Platform"/>
32
+ <img src="https://img.shields.io/badge/license-MIT-green" alt="License MIT"/>
33
+ <img src="https://img.shields.io/badge/VampSecure-Labs-magenta" alt="VampSecure Labs"/>
34
+ <img src="https://img.shields.io/badge/API%20keys-optional-brightgreen" alt="API keys optional"/>
35
+ </p>
36
+
37
+ ## Overview
38
+
39
+ `vamp-darkweb-intel` is a threat intelligence and darkweb research CLI that queries a target (domain, IP, hash, or URL) across multiple public feeds in parallel and **automatically correlates signals from independent sources**. When two or more sources flag the same indicator, the engine escalates severity — a C2 confirmed in both ThreatFox and OTX is more actionable than a single mention.
40
+
41
+ It works **without any API keys** using a curated set of free public feeds. Optional API keys (OTX, HIBP) unlock higher rate limits and additional data sources.
42
+
43
+ ## Sources
44
+
45
+ | Source | Type | API key? | Target |
46
+ |--------|------|----------|--------|
47
+ | ThreatFox (abuse.ch) | IOC / C2 | No | Domain · IP · Hash · URL |
48
+ | URLhaus (abuse.ch) | Malware distribution | No | Domain · IP · URL |
49
+ | MalwareBazaar (abuse.ch) | Malware samples | No | Hash |
50
+ | RansomLook | Ransomware victims | No | Domain |
51
+ | Ransomware.live | Ransomware victims (recent) | No | Domain |
52
+ | GreyNoise Community | IP context (scanner / malicious) | No | IP |
53
+ | Shodan InternetDB | Open ports · CVEs | No | IP |
54
+ | AlienVault OTX | Threat intel pulses | Optional | Domain · IP · Hash |
55
+ | HackerTarget | IP geolocation | No | IP |
56
+ | Have I Been Pwned | Domain breach check | `HIBP_API_KEY` | Domain |
57
+
58
+ ## Features
59
+
60
+ - Automatic target type detection: domain, IP, MD5/SHA1/SHA256 hash, or URL
61
+ - Parallel queries across all relevant sources (configurable workers)
62
+ - **Correlation engine**: if ≥2 independent sources flag the same category, severity escalates by one level
63
+ - Zero mandatory dependencies — only Python stdlib and `rich`
64
+ - Export to Console, JSON, HTML (dark-theme), Markdown, and CSV
65
+ - Exit codes suited for CI/CD pipelines (0 = clean, 1 = HIGH, 2 = CRITICAL)
66
+
67
+ ## Requirements
68
+
69
+ - Python 3.9 or later
70
+ - `rich >= 13.7.0`
71
+
72
+ ## Installation
73
+
74
+ ```bash
75
+ git clone https://github.com/Vampsecure-Labs/vamp-darkweb-intel.git
76
+ cd vamp-darkweb-intel
77
+ python3 -m venv .venv
78
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
79
+ pip install -r requirements.txt
80
+ ```
81
+
82
+ Or via PyPI:
83
+
84
+ ```bash
85
+ pip install vamp-darkweb-intel
86
+ ```
87
+
88
+ ## Try it now
89
+
90
+ No setup required — these queries go to public free-tier feeds:
91
+
92
+ ```bash
93
+ # Investigate an IP (GreyNoise + Shodan InternetDB + OTX + ThreatFox)
94
+ python3 vamp_darkweb_intel.py -t 1.1.1.1
95
+
96
+ # Check a domain for threat feed presence and ransomware victims
97
+ python3 vamp_darkweb_intel.py -t example.com
98
+
99
+ # Look up a known malware hash
100
+ python3 vamp_darkweb_intel.py -t 44d88612fea8a8f36de82e1278abb02f
101
+ ```
102
+
103
+ > These targets will show INFO or CLEAN results — they are safe, well-known hosts and hashes used only to demonstrate the tool workflow.
104
+
105
+ ## Examples
106
+
107
+ ```bash
108
+ # Single domain investigation
109
+ python3 vamp_darkweb_intel.py -t suspicious-domain.com
110
+
111
+ # Multiple targets in one run
112
+ python3 vamp_darkweb_intel.py -t 185.220.101.1 -t evil-corp.biz
113
+
114
+ # Bulk investigation from file
115
+ python3 vamp_darkweb_intel.py --file iocs.txt --workers 10
116
+
117
+ # Force target type (useful for ambiguous inputs)
118
+ python3 vamp_darkweb_intel.py -t abc123def456... --type hash
119
+
120
+ # Export full results to all formats
121
+ python3 vamp_darkweb_intel.py -t 192.0.2.1 \
122
+ --json results.json \
123
+ --html report.html \
124
+ --markdown report.md \
125
+ --csv report.csv
126
+ ```
127
+
128
+ ## Optional API keys
129
+
130
+ Set these environment variables before running to unlock additional sources:
131
+
132
+ ```bash
133
+ export OTX_API_KEY=your_key_here # AlienVault OTX — extended rate limit
134
+ export HIBP_API_KEY=your_key_here # Have I Been Pwned domain breach check
135
+ ```
136
+
137
+ Copy `.env.example` to `.env` and fill in the keys you have:
138
+
139
+ ```bash
140
+ cp .env.example .env
141
+ ```
142
+
143
+ ## Correlation engine
144
+
145
+ The engine cross-references findings by category across all sources. When two or more independent sources report the same finding category for a target, the highest-severity finding in that category is escalated by one level (up to CRITICAL).
146
+
147
+ **Example**: if ThreatFox reports `Threat Feed / MEDIUM` (confidence 50%) and OTX reports the same domain in 4 pulses (`Threat Intelligence / LOW`), the correlation engine adds a `MEDIUM → HIGH` escalated finding with a note listing both sources.
148
+
149
+ This reduces noise from low-confidence single-source hits while surfacing genuine threats confirmed by independent intelligence.
150
+
151
+ ## CI/CD Integration
152
+
153
+ ```yaml
154
+ name: Threat Intelligence Check
155
+ on:
156
+ schedule:
157
+ - cron: '0 8 * * 1'
158
+ workflow_dispatch:
159
+
160
+ jobs:
161
+ darkweb-intel:
162
+ runs-on: ubuntu-latest
163
+ steps:
164
+ - uses: actions/checkout@v4
165
+ - uses: actions/setup-python@v5
166
+ with: { python-version: '3.12' }
167
+ - run: pip install vamp-darkweb-intel
168
+ - run: |
169
+ vamp-darkweb-intel \
170
+ -t yourdomain.com \
171
+ --json intel-results.json \
172
+ --markdown intel-report.md
173
+ # Exit 1 = HIGH findings, exit 2 = CRITICAL findings
174
+ env:
175
+ OTX_API_KEY: ${{ secrets.OTX_API_KEY }}
176
+ HIBP_API_KEY: ${{ secrets.HIBP_API_KEY }}
177
+ - uses: actions/upload-artifact@v4
178
+ if: always()
179
+ with:
180
+ name: intel-report
181
+ path: intel-report.md
182
+ ```
183
+
184
+ ## CLI Reference
185
+
186
+ | Flag | Default | Description |
187
+ |------|---------|-------------|
188
+ | `-t / --target TARGET` | — | Investigation target: domain, IP, hash, or URL (repeatable) |
189
+ | `--file FILE` | — | Text file with one target per line |
190
+ | `--type TYPE` | auto | Force target type: `domain`, `ip`, `hash`, `url`, `email` |
191
+ | `--timeout N` | 12 | Per-source request timeout in seconds |
192
+ | `--workers N` | 6 | Parallel source queries |
193
+ | `--json FILE` | — | Export results to JSON |
194
+ | `--html FILE` | — | Export dark-theme HTML report |
195
+ | `--markdown FILE` | — | Export Markdown report |
196
+ | `--csv FILE` | — | Export CSV results |
197
+
198
+ ## Exit Codes
199
+
200
+ | Code | Meaning | CI/CD Behavior |
201
+ |------|---------|----------------|
202
+ | `0` | Clean — no HIGH or CRITICAL findings | Pipeline passes |
203
+ | `1` | HIGH-severity findings detected | Pipeline fails — review required |
204
+ | `2` | CRITICAL findings detected | Pipeline fails — immediate action required |
205
+
206
+ ## Severity levels
207
+
208
+ | Level | Examples |
209
+ |-------|---------|
210
+ | CRITICAL | Active C2/botnet confirmed · malware hash confirmed · CVEs on exposed service |
211
+ | HIGH | IOC in threat feed · ransomware victim confirmed · IP classified malicious |
212
+ | MEDIUM | Low-confidence IOC · IP active scanner · multi-source correlation upgrade |
213
+ | LOW | Mentioned in old intelligence · minor open port surface |
214
+ | INFO | Geolocation data · known-good infrastructure (RIOT) |
215
+
216
+ ## Legal Notice
217
+
218
+ Use exclusively on systems you own or for which you hold explicit written authorization from the system owner. VampSecure Studios assumes no liability for unauthorized use.
219
+
220
+ ## Part of VampSecure Labs Toolkit
221
+
222
+ `vamp-darkweb-intel` is part of the VampSecure Labs security research toolkit.
223
+
224
+ - Portfolio: [github.com/belky-me](https://github.com/belky-me)
225
+ - Orchestrator: [github.com/belky-me/vamp-orchestrator](https://github.com/belky-me/vamp-orchestrator)
226
+
227
+ ---
228
+
229
+ © VampSecure Studios — VampSecure Labs Security Research Division
@@ -0,0 +1,7 @@
1
+ vamp_darkweb_intel.py,sha256=Mh23UdSe-wxWzzZOcYQRj20fKdyL3GmXYwrkJi0Hmms,37852
2
+ vamp_darkweb_intel-1.0.0.dist-info/licenses/LICENSE,sha256=VBh17V_iQ-1p85u_EIqlVcP2-FCFgS0Jw-EykIAhq0c,1075
3
+ vamp_darkweb_intel-1.0.0.dist-info/METADATA,sha256=kuXO4mkcelbgxGaLyxxsorTUGQDblX8jqD3-rKEDhJw,8792
4
+ vamp_darkweb_intel-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
5
+ vamp_darkweb_intel-1.0.0.dist-info/entry_points.txt,sha256=NCzyW1-w0VN9wixrkbUezWECE8LfeaC_xBmMfFpDQXw,63
6
+ vamp_darkweb_intel-1.0.0.dist-info/top_level.txt,sha256=Mx0FjjakXyfK2ndQIqLKWkDuXMicPcUxOOqC3i9eJ3w,19
7
+ vamp_darkweb_intel-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ vamp-darkweb-intel = vamp_darkweb_intel:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 VampSecure Studios
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ vamp_darkweb_intel
vamp_darkweb_intel.py ADDED
@@ -0,0 +1,996 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ vamp_darkweb_intel.py — Motor de Inteligencia Darkweb y Threat Intelligence
4
+ ============================================================================
5
+ VampSecure Labs · VampSecure Studios
6
+ Para Uso Exclusivo en Investigaciones de Seguridad Autorizadas — v1.0
7
+
8
+ DESCRIPCIÓN
9
+ -----------
10
+ Consulta cruzada de un objetivo (dominio, IP, hash, URL) contra múltiples
11
+ fuentes públicas de threat intelligence. El motor de correlación escala
12
+ automáticamente la severidad cuando una IOC aparece en varias fuentes.
13
+
14
+ FUENTES GRATUITAS (sin clave API)
15
+ ----------------------------------
16
+ ThreatFox (abuse.ch) — IOCs activos (dominio/IP/URL/hash/C2)
17
+ URLhaus (abuse.ch) — URLs maliciosas activas y archivadas
18
+ MalwareBazaar (abuse.ch) — Muestras de malware por hash o firma
19
+ RansomLook — Víctimas de ransomware por dominio/nombre
20
+ Ransomware.live — Víctimas recientes de ransomware
21
+ GreyNoise Community — Contexto de IP (escáner/malicioso/benigno)
22
+ Shodan InternetDB — Puertos, etiquetas y CVEs de IP (sin auth)
23
+ AlienVault OTX — Pulsos de inteligencia (límite reducido sin clave)
24
+ HackerTarget — Geolocalización y DNS inverso de IP
25
+
26
+ FUENTES OPCIONALES (con clave API en variables de entorno)
27
+ -----------------------------------------------------------
28
+ OTX_API_KEY — AlienVault OTX con límite extendido
29
+ HIBP_API_KEY — Have I Been Pwned (dominios y emails)
30
+
31
+ AUTORÍA
32
+ -------
33
+ © VampSecure Studios — VampSecure Labs Security Research Division
34
+ Todos los derechos reservados. Uso exclusivo en entornos autorizados.
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import argparse
40
+ import csv
41
+ import ipaddress
42
+ import json
43
+ import os
44
+ import re
45
+ import sys
46
+ import urllib.parse
47
+ import urllib.request
48
+ import urllib.error
49
+ from concurrent.futures import ThreadPoolExecutor, as_completed
50
+ from dataclasses import dataclass, field
51
+ from datetime import datetime, timezone
52
+ from typing import Dict, List, Optional, Tuple
53
+
54
+ from rich.console import Console
55
+ from rich.panel import Panel
56
+ from rich.table import Table
57
+ from rich import box
58
+
59
+ # ---------------------------------------------------------------------------
60
+ # Constantes
61
+ # ---------------------------------------------------------------------------
62
+
63
+ VERSION = "1.0"
64
+ TOOL_NAME = "vamp-darkweb-intel"
65
+
66
+ _TIMEOUT = 12 # segundos por petición
67
+
68
+ # Endpoints
69
+ _THREATFOX_API = "https://threatfox-api.abuse.ch/api/v1/"
70
+ _URLHAUS_HOST_API = "https://urlhaus-api.abuse.ch/v1/host/"
71
+ _URLHAUS_URL_API = "https://urlhaus-api.abuse.ch/v1/url/"
72
+ _BAZAAR_API = "https://mb-api.abuse.ch/api/v1/"
73
+ _RANSOMLOOK_RECENT = "https://www.ransomlook.io/api/recent"
74
+ _RANSOMWARELIVE_API = "https://api.ransomware.live/v2/recentvictims"
75
+ _GREYNOISE_COMM = "https://api.greynoise.io/v3/community/{ip}"
76
+ _SHODAN_INTERNETDB = "https://internetdb.shodan.io/{ip}"
77
+ _OTX_DOMAIN = "https://otx.alienvault.com/api/v1/indicators/domain/{target}/general"
78
+ _OTX_IP = "https://otx.alienvault.com/api/v1/indicators/IPv4/{target}/general"
79
+ _OTX_HASH = "https://otx.alienvault.com/api/v1/indicators/file/{target}/general"
80
+ _HACKERTARGET_GEO = "https://api.hackertarget.com/ipgeo/?q={ip}"
81
+ _HIBP_DOMAIN = "https://haveibeenpwned.com/api/v3/breacheddomain/{domain}"
82
+
83
+ # Paleta de severidades (colores Rich)
84
+ _SEV_COLOR = {
85
+ "CRITICAL": "bold red",
86
+ "HIGH": "red",
87
+ "MEDIUM": "yellow",
88
+ "LOW": "cyan",
89
+ "INFO": "dim white",
90
+ }
91
+
92
+ # Orden numérico de severidades
93
+ _SEV_RANK = {"CRITICAL": 5, "HIGH": 4, "MEDIUM": 3, "LOW": 2, "INFO": 1}
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # Modelos de datos
98
+ # ---------------------------------------------------------------------------
99
+
100
+ @dataclass
101
+ class Finding:
102
+ severity: str # CRITICAL | HIGH | MEDIUM | LOW | INFO
103
+ source: str # nombre de la fuente (ThreatFox, GreyNoise…)
104
+ category: str # tipo de hallazgo (C2, Ransomware, Malware…)
105
+ name: str # título corto
106
+ detail: str # descripción extendida
107
+ remediation: str # acción recomendada
108
+
109
+
110
+ @dataclass
111
+ class TargetResult:
112
+ target: str
113
+ target_type: str # domain | ip | hash | url
114
+ findings: List[Finding] = field(default_factory=list)
115
+ raw_sources: Dict[str, object] = field(default_factory=dict)
116
+ error: Optional[str] = None
117
+ scan_time: str = ""
118
+
119
+ # ---------- helpers ----------
120
+ def add(self, f: Finding) -> None:
121
+ self.findings.append(f)
122
+
123
+ def critical(self) -> List[Finding]:
124
+ return [f for f in self.findings if f.severity == "CRITICAL"]
125
+
126
+ def high(self) -> List[Finding]:
127
+ return [f for f in self.findings if f.severity == "HIGH"]
128
+
129
+ def summary_grade(self) -> str:
130
+ """Riesgo general: CRITICAL > HIGH > MEDIUM > LOW > CLEAN."""
131
+ if self.critical(): return "CRITICAL"
132
+ if self.high(): return "HIGH"
133
+ if any(f.severity == "MEDIUM" for f in self.findings): return "MEDIUM"
134
+ if any(f.severity == "LOW" for f in self.findings): return "LOW"
135
+ return "CLEAN"
136
+
137
+ def risk_color(self) -> str:
138
+ g = self.summary_grade()
139
+ return _SEV_COLOR.get(g, "green")
140
+
141
+
142
+ # ---------------------------------------------------------------------------
143
+ # Utilidades de red
144
+ # ---------------------------------------------------------------------------
145
+
146
+ def _get(url: str, headers: Optional[Dict] = None, timeout: int = _TIMEOUT) -> dict | str | None:
147
+ """GET HTTP → JSON o texto plano; None en caso de error."""
148
+ req = urllib.request.Request(url, headers=headers or {})
149
+ req.add_header("User-Agent", f"VampSecure-Labs/{TOOL_NAME}/{VERSION}")
150
+ try:
151
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
152
+ body = resp.read().decode("utf-8", errors="replace")
153
+ ct = resp.headers.get("Content-Type", "")
154
+ if "json" in ct:
155
+ return json.loads(body)
156
+ return body
157
+ except Exception:
158
+ return None
159
+
160
+
161
+ def _post_json(url: str, payload: dict, timeout: int = _TIMEOUT) -> dict | None:
162
+ """POST JSON → JSON; None en caso de error."""
163
+ data = json.dumps(payload).encode("utf-8")
164
+ req = urllib.request.Request(url, data=data, method="POST")
165
+ req.add_header("Content-Type", "application/json")
166
+ req.add_header("User-Agent", f"VampSecure-Labs/{TOOL_NAME}/{VERSION}")
167
+ try:
168
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
169
+ return json.loads(resp.read().decode("utf-8", errors="replace"))
170
+ except Exception:
171
+ return None
172
+
173
+
174
+ def _post_form(url: str, fields: Dict[str, str], timeout: int = _TIMEOUT) -> dict | None:
175
+ """POST form-urlencoded → JSON; None en caso de error."""
176
+ data = urllib.parse.urlencode(fields).encode("utf-8")
177
+ req = urllib.request.Request(url, data=data, method="POST")
178
+ req.add_header("Content-Type", "application/x-www-form-urlencoded")
179
+ req.add_header("User-Agent", f"VampSecure-Labs/{TOOL_NAME}/{VERSION}")
180
+ try:
181
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
182
+ return json.loads(resp.read().decode("utf-8", errors="replace"))
183
+ except Exception:
184
+ return None
185
+
186
+
187
+ # ---------------------------------------------------------------------------
188
+ # Detección de tipo de objetivo
189
+ # ---------------------------------------------------------------------------
190
+
191
+ _HASH_RE = re.compile(r'^[0-9a-fA-F]{32}$|^[0-9a-fA-F]{40}$|^[0-9a-fA-F]{64}$')
192
+ _EMAIL_RE = re.compile(r'^[^@]+@[^@]+\.[^@]+$')
193
+
194
+
195
+ def detect_target_type(target: str) -> str:
196
+ """Devuelve 'domain', 'ip', 'hash', 'url' o 'email'."""
197
+ if target.startswith(("http://", "https://")):
198
+ return "url"
199
+ if _EMAIL_RE.match(target):
200
+ return "email"
201
+ if _HASH_RE.match(target):
202
+ return "hash"
203
+ try:
204
+ ipaddress.ip_address(target)
205
+ return "ip"
206
+ except ValueError:
207
+ pass
208
+ return "domain"
209
+
210
+
211
+ # ---------------------------------------------------------------------------
212
+ # Fuentes de threat intelligence
213
+ # ---------------------------------------------------------------------------
214
+
215
+ def _source_threatfox(target: str, target_type: str) -> List[Finding]:
216
+ """ThreatFox (abuse.ch) — IOCs por dominio, IP, URL o hash."""
217
+ findings: List[Finding] = []
218
+ payload = {"query": "search_ioc", "search_term": target}
219
+ data = _post_json(_THREATFOX_API, payload)
220
+ if not data or data.get("query_status") not in ("ok", "no_result"):
221
+ return findings
222
+
223
+ iocs = data.get("data") or []
224
+ if not iocs:
225
+ return findings
226
+
227
+ for ioc in iocs[:10]: # máximo 10 entradas
228
+ threat_type = ioc.get("threat_type", "")
229
+ malware = ioc.get("malware", "")
230
+ confidence = ioc.get("confidence_level", 0)
231
+ last_seen = ioc.get("last_seen", "")
232
+
233
+ if threat_type in ("botnet_cc", "c2"):
234
+ sev = "CRITICAL"
235
+ name = f"C2/Botnet confirmado ({malware})"
236
+ elif confidence >= 75:
237
+ sev = "HIGH"
238
+ name = f"IOC en ThreatFox — {threat_type} ({malware})"
239
+ else:
240
+ sev = "MEDIUM"
241
+ name = f"IOC en ThreatFox — {threat_type} ({malware})"
242
+
243
+ findings.append(Finding(
244
+ severity = sev,
245
+ source = "ThreatFox",
246
+ category = "Threat Feed",
247
+ name = name,
248
+ detail = (
249
+ f"Tipo de amenaza: {threat_type} · Malware: {malware} · "
250
+ f"Confianza: {confidence}% · Última vez visto: {last_seen}"
251
+ ),
252
+ remediation = "Bloquear la IOC en el perímetro (firewall, DNS sinkhole). "
253
+ "Investigar sistemas que hayan contactado este indicador.",
254
+ ))
255
+
256
+ return findings
257
+
258
+
259
+ def _source_urlhaus(target: str, target_type: str) -> List[Finding]:
260
+ """URLhaus (abuse.ch) — URLs y hosts maliciosos."""
261
+ findings: List[Finding] = []
262
+
263
+ if target_type == "url":
264
+ data = _post_form(_URLHAUS_URL_API, {"url": target})
265
+ else:
266
+ data = _post_form(_URLHAUS_HOST_API, {"host": target})
267
+
268
+ if not data or data.get("query_status") not in ("ok", "is_host"):
269
+ return findings
270
+
271
+ urls = data.get("urls") or []
272
+ active_count = sum(1 for u in urls if u.get("url_status") == "online")
273
+ total_count = len(urls)
274
+
275
+ if total_count == 0:
276
+ return findings
277
+
278
+ sev = "CRITICAL" if active_count > 0 else "HIGH"
279
+ name = (
280
+ f"{'Activo en' if active_count else 'Histórico en'} URLhaus "
281
+ f"({active_count} activas / {total_count} total)"
282
+ )
283
+ findings.append(Finding(
284
+ severity = sev,
285
+ source = "URLhaus",
286
+ category = "Malware Distribution",
287
+ name = name,
288
+ detail = (
289
+ f"URLs maliciosas asociadas: {total_count} "
290
+ f"({active_count} actualmente activas)"
291
+ ),
292
+ remediation = "Bloquear dominio/IP en firewall y proxy. "
293
+ "Notificar usuarios que hayan accedido al host.",
294
+ ))
295
+ return findings
296
+
297
+
298
+ def _source_malwarebazaar(target: str, target_type: str) -> List[Finding]:
299
+ """MalwareBazaar (abuse.ch) — búsqueda por hash."""
300
+ if target_type != "hash":
301
+ return []
302
+ findings: List[Finding] = []
303
+ data = _post_form(_BAZAAR_API, {"query": "get_info", "hash": target})
304
+ if not data or data.get("query_status") != "ok":
305
+ return findings
306
+
307
+ samples = data.get("data") or []
308
+ for s in samples[:5]:
309
+ malware = s.get("signature") or s.get("tags", ["?"])[0]
310
+ file_type = s.get("file_type", "")
311
+ first_seen = s.get("first_seen", "")
312
+ findings.append(Finding(
313
+ severity = "CRITICAL",
314
+ source = "MalwareBazaar",
315
+ category = "Malware",
316
+ name = f"Hash de malware confirmado ({malware})",
317
+ detail = (
318
+ f"Tipo: {file_type} · Familia: {malware} · "
319
+ f"Primera vez visto: {first_seen}"
320
+ ),
321
+ remediation = "Aislar inmediatamente cualquier sistema que ejecute este hash. "
322
+ "Iniciar proceso de respuesta a incidentes.",
323
+ ))
324
+ return findings
325
+
326
+
327
+ def _source_ransomlook(target: str, target_type: str) -> List[Finding]:
328
+ """RansomLook — víctimas de ransomware por dominio o nombre."""
329
+ if target_type not in ("domain", "url"):
330
+ return []
331
+
332
+ findings: List[Finding] = []
333
+ domain = target.lower().split("/")[0]
334
+ data = _get(_RANSOMLOOK_RECENT)
335
+
336
+ if not isinstance(data, dict):
337
+ return findings
338
+
339
+ # RansomLook devuelve un dict de grupos con listas de posts
340
+ matches: List[Tuple[str, str, str]] = [] # (grupo, descripción, fecha)
341
+ for group_name, posts in data.items():
342
+ if not isinstance(posts, list):
343
+ continue
344
+ for post in posts:
345
+ desc = str(post.get("description", "") or post.get("post_title", "")).lower()
346
+ website = str(post.get("website", "")).lower()
347
+ title = str(post.get("post_title", "")).lower()
348
+ date = post.get("discovered", "") or post.get("date", "")
349
+ base = domain.replace("www.", "").split(".")[0]
350
+ if base in desc or base in website or domain in website:
351
+ matches.append((group_name, title or desc[:120], date))
352
+
353
+ for group, desc, date in matches[:3]:
354
+ findings.append(Finding(
355
+ severity = "HIGH",
356
+ source = "RansomLook",
357
+ category = "Ransomware",
358
+ name = f"Posible víctima de ransomware — grupo {group}",
359
+ detail = f"Grupo: {group} · Descripción: {desc[:200]} · Fecha: {date}",
360
+ remediation = "Verificar si la organización fue afectada. "
361
+ "Revisar logs de EDR/SIEM en las fechas indicadas.",
362
+ ))
363
+ return findings
364
+
365
+
366
+ def _source_ransomwarelive(target: str, target_type: str) -> List[Finding]:
367
+ """Ransomware.live — víctimas recientes por dominio."""
368
+ if target_type not in ("domain", "url"):
369
+ return []
370
+
371
+ findings: List[Finding] = []
372
+ domain = target.lower().split("/")[0].replace("www.", "")
373
+ base = domain.split(".")[0]
374
+ data = _get(_RANSOMWARELIVE_API)
375
+
376
+ if not isinstance(data, list):
377
+ return findings
378
+
379
+ for victim in data:
380
+ victim_name = str(victim.get("victim", "") or "").lower()
381
+ victim_website = str(victim.get("website", "") or "").lower()
382
+ group = victim.get("group", "?")
383
+ country = victim.get("country", "?")
384
+ published = victim.get("published", "")
385
+ if base in victim_name or domain in victim_website:
386
+ findings.append(Finding(
387
+ severity = "HIGH",
388
+ source = "Ransomware.live",
389
+ category = "Ransomware",
390
+ name = f"Víctima confirmada en Ransomware.live — {group}",
391
+ detail = (
392
+ f"Grupo: {group} · País: {country} · "
393
+ f"Publicado: {published} · Web: {victim_website}"
394
+ ),
395
+ remediation = "Verificar alcance real del incidente. "
396
+ "Contactar con la organización si procede.",
397
+ ))
398
+ return findings
399
+
400
+
401
+ def _source_greynoise(target: str, target_type: str) -> List[Finding]:
402
+ """GreyNoise Community — contexto de IP (sin clave API)."""
403
+ if target_type != "ip":
404
+ return []
405
+
406
+ findings: List[Finding] = []
407
+ data = _get(_GREYNOISE_COMM.format(ip=target))
408
+
409
+ if not isinstance(data, dict):
410
+ return findings
411
+
412
+ noise = data.get("noise", False)
413
+ riot = data.get("riot", False)
414
+ classification = data.get("classification", "unknown")
415
+ name = data.get("name", "")
416
+ last_seen = data.get("last_seen", "")
417
+
418
+ if riot:
419
+ findings.append(Finding(
420
+ severity = "INFO",
421
+ source = "GreyNoise",
422
+ category = "IP Context",
423
+ name = f"IP de infraestructura legítima conocida ({name})",
424
+ detail = f"RIOT: proveedor o infraestructura benigna — {name}",
425
+ remediation = "Sin acción requerida.",
426
+ ))
427
+ return findings
428
+
429
+ if classification == "malicious":
430
+ findings.append(Finding(
431
+ severity = "HIGH",
432
+ source = "GreyNoise",
433
+ category = "Malicious IP",
434
+ name = f"IP clasificada como maliciosa en GreyNoise ({name or target})",
435
+ detail = (
436
+ f"Clasificación: {classification} · Noise: {noise} · "
437
+ f"Última actividad: {last_seen}"
438
+ ),
439
+ remediation = "Bloquear IP en firewall perimetral. "
440
+ "Revisar logs para detectar conexiones previas.",
441
+ ))
442
+ elif noise:
443
+ findings.append(Finding(
444
+ severity = "MEDIUM",
445
+ source = "GreyNoise",
446
+ category = "Scanner",
447
+ name = "IP activa en internet scanning (GreyNoise)",
448
+ detail = (
449
+ f"Clasificación: {classification} · Nombre: {name} · "
450
+ f"Última actividad: {last_seen}"
451
+ ),
452
+ remediation = "Valorar bloqueo proactivo si no hay relación de negocio.",
453
+ ))
454
+ return findings
455
+
456
+
457
+ def _source_shodan_internetdb(target: str, target_type: str) -> List[Finding]:
458
+ """Shodan InternetDB — puertos, etiquetas y CVEs de IP (sin auth)."""
459
+ if target_type != "ip":
460
+ return []
461
+
462
+ findings: List[Finding] = []
463
+ data = _get(_SHODAN_INTERNETDB.format(ip=target))
464
+
465
+ if not isinstance(data, dict) or "ip" not in data:
466
+ return findings
467
+
468
+ vulns = data.get("vulns") or []
469
+ tags = data.get("tags") or []
470
+ ports = data.get("open_ports") or []
471
+
472
+ if vulns:
473
+ sev = "CRITICAL" if any("critical" in v.lower() for v in vulns) else "HIGH"
474
+ findings.append(Finding(
475
+ severity = sev,
476
+ source = "Shodan InternetDB",
477
+ category = "Vulnerabilidades",
478
+ name = f"CVEs conocidos en IP: {', '.join(vulns[:5])}",
479
+ detail = (
480
+ f"CVEs: {', '.join(vulns)} · "
481
+ f"Puertos abiertos: {', '.join(str(p) for p in ports[:10])}"
482
+ ),
483
+ remediation = "Aplicar parches o mitigaciones para los CVEs listados. "
484
+ "Revisar si los puertos abiertos son necesarios.",
485
+ ))
486
+ elif ports:
487
+ findings.append(Finding(
488
+ severity = "LOW",
489
+ source = "Shodan InternetDB",
490
+ category = "Surface de ataque",
491
+ name = f"Puertos abiertos detectados: {', '.join(str(p) for p in ports[:8])}",
492
+ detail = f"Tags: {', '.join(tags)} · Puertos: {', '.join(str(p) for p in ports)}",
493
+ remediation = "Revisar que los servicios expuestos sean intencionales.",
494
+ ))
495
+
496
+ if "honeypot" in tags:
497
+ findings.append(Finding(
498
+ severity = "INFO",
499
+ source = "Shodan InternetDB",
500
+ category = "Honeypot",
501
+ name = "IP identificada como honeypot (Shodan)",
502
+ detail = "Shodan ha etiquetado esta IP como honeypot.",
503
+ remediation = "Sin acción requerida, pero evitar conectarse a ella.",
504
+ ))
505
+ return findings
506
+
507
+
508
+ def _source_otx(target: str, target_type: str) -> List[Finding]:
509
+ """AlienVault OTX — pulsos de inteligencia."""
510
+ findings: List[Finding] = []
511
+
512
+ api_key = os.getenv("OTX_API_KEY", "")
513
+ headers = {"X-OTX-API-KEY": api_key} if api_key else {}
514
+
515
+ if target_type == "domain":
516
+ url = _OTX_DOMAIN.format(target=target)
517
+ elif target_type == "ip":
518
+ url = _OTX_IP.format(target=target)
519
+ elif target_type == "hash":
520
+ url = _OTX_HASH.format(target=target)
521
+ else:
522
+ return findings
523
+
524
+ data = _get(url, headers=headers)
525
+ if not isinstance(data, dict):
526
+ return findings
527
+
528
+ pulse_count = data.get("pulse_info", {}).get("count", 0)
529
+ validation = data.get("validation", [])
530
+ is_whitelisted = any(v.get("source") == "whitelist" for v in validation)
531
+
532
+ if is_whitelisted:
533
+ return findings
534
+
535
+ if pulse_count >= 10:
536
+ sev = "HIGH"
537
+ elif pulse_count >= 3:
538
+ sev = "MEDIUM"
539
+ elif pulse_count >= 1:
540
+ sev = "LOW"
541
+ else:
542
+ return findings
543
+
544
+ findings.append(Finding(
545
+ severity = sev,
546
+ source = "AlienVault OTX",
547
+ category = "Threat Intelligence",
548
+ name = f"Presente en {pulse_count} pulso(s) de inteligencia (OTX)",
549
+ detail = (
550
+ f"Pulsos: {pulse_count} · Indicador: {target}"
551
+ ),
552
+ remediation = "Revisar los pulsos OTX para contexto adicional: "
553
+ "https://otx.alienvault.com",
554
+ ))
555
+ return findings
556
+
557
+
558
+ def _source_hackertarget_geo(target: str, target_type: str) -> List[Finding]:
559
+ """HackerTarget — geolocalización de IP (contexto INFO)."""
560
+ if target_type != "ip":
561
+ return []
562
+ findings: List[Finding] = []
563
+ raw = _get(_HACKERTARGET_GEO.format(ip=target))
564
+ if not isinstance(raw, str) or "error" in raw.lower():
565
+ return findings
566
+ findings.append(Finding(
567
+ severity = "INFO",
568
+ source = "HackerTarget",
569
+ category = "Geolocalización",
570
+ name = "Datos geográficos de IP",
571
+ detail = raw.strip().replace("\n", " · "),
572
+ remediation = "",
573
+ ))
574
+ return findings
575
+
576
+
577
+ def _source_hibp(target: str, target_type: str) -> List[Finding]:
578
+ """Have I Been Pwned — brechas de dominio (requiere HIBP_API_KEY)."""
579
+ if target_type != "domain":
580
+ return []
581
+ api_key = os.getenv("HIBP_API_KEY", "")
582
+ if not api_key:
583
+ return []
584
+ findings: List[Finding] = []
585
+ url = _HIBP_DOMAIN.format(domain=target)
586
+ data = _get(url, headers={"hibp-api-key": api_key, "user-agent": f"vamp-darkweb-intel/{VERSION}"})
587
+ if not isinstance(data, dict):
588
+ return findings
589
+ breaches = list(data.keys())
590
+ if breaches:
591
+ findings.append(Finding(
592
+ severity = "HIGH",
593
+ source = "HIBP",
594
+ category = "Breach",
595
+ name = f"Dominio presente en {len(breaches)} brecha(s) conocida(s)",
596
+ detail = f"Brechas: {', '.join(breaches[:10])}",
597
+ remediation = "Forzar cambio de contraseñas para cuentas del dominio afectado. "
598
+ "Revisar si se expusieron otros datos sensibles.",
599
+ ))
600
+ return findings
601
+
602
+
603
+ # ---------------------------------------------------------------------------
604
+ # Motor de correlación
605
+ # ---------------------------------------------------------------------------
606
+
607
+ def _correlate(findings: List[Finding]) -> List[Finding]:
608
+ """
609
+ Escala severidad de hallazgos duplicados entre fuentes.
610
+ Si una IOC aparece en ≥2 fuentes independientes, el hallazgo de mayor
611
+ severidad sube un nivel (max: CRITICAL).
612
+ """
613
+ categories: Dict[str, List[Finding]] = {}
614
+ for f in findings:
615
+ key = f.category
616
+ categories.setdefault(key, []).append(f)
617
+
618
+ upgraded: List[Finding] = []
619
+ for cat_findings in categories.values():
620
+ # Fuentes únicas que reportan esta categoría
621
+ sources = {f.source for f in cat_findings}
622
+ if len(sources) >= 2:
623
+ # El hallazgo de mayor severidad sube un nivel
624
+ ranked = sorted(cat_findings, key=lambda x: _SEV_RANK.get(x.severity, 0), reverse=True)
625
+ top = ranked[0]
626
+ levels = ["INFO", "LOW", "MEDIUM", "HIGH", "CRITICAL"]
627
+ current_idx = levels.index(top.severity) if top.severity in levels else 0
628
+ new_sev = levels[min(current_idx + 1, 4)]
629
+ if new_sev != top.severity:
630
+ upgraded.append(Finding(
631
+ severity = new_sev,
632
+ source = "Correlación",
633
+ category = cat_findings[0].category,
634
+ name = f"Señal multi-fuente: {top.name}",
635
+ detail = (
636
+ f"Detectado en {len(sources)} fuentes independientes: "
637
+ f"{', '.join(sorted(sources))}. Severidad escalada de "
638
+ f"{top.severity} a {new_sev}."
639
+ ),
640
+ remediation = top.remediation,
641
+ ))
642
+
643
+ return findings + upgraded
644
+
645
+
646
+ # ---------------------------------------------------------------------------
647
+ # Motor principal
648
+ # ---------------------------------------------------------------------------
649
+
650
+ # Mapeo fuente → función
651
+ _ALL_SOURCES = [
652
+ _source_threatfox,
653
+ _source_urlhaus,
654
+ _source_malwarebazaar,
655
+ _source_ransomlook,
656
+ _source_ransomwarelive,
657
+ _source_greynoise,
658
+ _source_shodan_internetdb,
659
+ _source_otx,
660
+ _source_hackertarget_geo,
661
+ _source_hibp,
662
+ ]
663
+
664
+
665
+ class DarkwebIntelEngine:
666
+ """Orquesta la consulta paralela a todas las fuentes y genera TargetResult."""
667
+
668
+ def __init__(self, timeout: int = _TIMEOUT, workers: int = 6) -> None:
669
+ self._timeout = timeout
670
+ self._workers = workers
671
+
672
+ def scan(self, target: str, target_type: Optional[str] = None) -> TargetResult:
673
+ target = target.strip()
674
+ target_type = target_type or detect_target_type(target)
675
+ result = TargetResult(
676
+ target = target,
677
+ target_type = target_type,
678
+ scan_time = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC"),
679
+ )
680
+
681
+ all_findings: List[Finding] = []
682
+
683
+ with ThreadPoolExecutor(max_workers=self._workers) as pool:
684
+ futures = {pool.submit(fn, target, target_type): fn.__name__ for fn in _ALL_SOURCES}
685
+ for fut in as_completed(futures):
686
+ try:
687
+ findings = fut.result(timeout=self._timeout + 2)
688
+ all_findings.extend(findings)
689
+ except Exception:
690
+ pass
691
+
692
+ # Correlacionar y ordenar por severidad
693
+ all_findings = _correlate(all_findings)
694
+ all_findings.sort(key=lambda f: _SEV_RANK.get(f.severity, 0), reverse=True)
695
+ result.findings = all_findings
696
+
697
+ return result
698
+
699
+
700
+ # ---------------------------------------------------------------------------
701
+ # Salida por consola (Rich)
702
+ # ---------------------------------------------------------------------------
703
+
704
+ _console = Console()
705
+
706
+
707
+ def _print_result(result: TargetResult) -> None:
708
+ grade = result.summary_grade()
709
+ grade_color = result.risk_color()
710
+
711
+ # Cabecera
712
+ _console.rule(f"[bold]{result.target}[/bold] — [italic]{result.target_type}[/italic]")
713
+
714
+ grade_text = f"[{grade_color}]{grade}[/{grade_color}]"
715
+ _console.print(
716
+ f" Nivel de riesgo: {grade_text} · "
717
+ f"Hallazgos: {len(result.findings)} · "
718
+ f"Escaneado: {result.scan_time}",
719
+ )
720
+
721
+ if not result.findings:
722
+ _console.print(" [green]Sin hallazgos en fuentes consultadas.[/green]\n")
723
+ return
724
+
725
+ tbl = Table(
726
+ "SEV", "Fuente", "Categoría", "Hallazgo", "Detalle",
727
+ box=box.SIMPLE_HEAD,
728
+ show_header=True,
729
+ header_style="bold white",
730
+ )
731
+
732
+ for f in result.findings:
733
+ color = _SEV_COLOR.get(f.severity, "white")
734
+ tbl.add_row(
735
+ f"[{color}]{f.severity}[/{color}]",
736
+ f.source,
737
+ f.category,
738
+ f.name,
739
+ f.detail[:120] + ("…" if len(f.detail) > 120 else ""),
740
+ )
741
+
742
+ _console.print(tbl)
743
+ _console.print()
744
+
745
+
746
+ # ---------------------------------------------------------------------------
747
+ # Exportadores
748
+ # ---------------------------------------------------------------------------
749
+
750
+ def _to_json(results: List[TargetResult]) -> str:
751
+ def _finding_dict(f: Finding) -> dict:
752
+ return {
753
+ "severity": f.severity, "source": f.source, "category": f.category,
754
+ "name": f.name, "detail": f.detail, "remediation": f.remediation,
755
+ }
756
+ out = []
757
+ for r in results:
758
+ out.append({
759
+ "target": r.target,
760
+ "target_type": r.target_type,
761
+ "risk_grade": r.summary_grade(),
762
+ "scan_time": r.scan_time,
763
+ "findings": [_finding_dict(f) for f in r.findings],
764
+ })
765
+ return json.dumps(out, indent=2, ensure_ascii=False)
766
+
767
+
768
+ def _to_markdown(results: List[TargetResult]) -> str:
769
+ lines: List[str] = [
770
+ "# vamp-darkweb-intel — Informe de Threat Intelligence",
771
+ "",
772
+ f"Generado: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')} ",
773
+ f"Herramienta: {TOOL_NAME} v{VERSION}",
774
+ "",
775
+ "---",
776
+ "",
777
+ ]
778
+ for r in results:
779
+ lines += [
780
+ f"## {r.target} ({r.target_type})",
781
+ "",
782
+ f"**Nivel de riesgo:** {r.summary_grade()} ",
783
+ f"**Hallazgos:** {len(r.findings)} ",
784
+ f"**Escaneado:** {r.scan_time}",
785
+ "",
786
+ ]
787
+ if not r.findings:
788
+ lines += ["Sin hallazgos en fuentes consultadas.", ""]
789
+ continue
790
+
791
+ lines += [
792
+ "| Severidad | Fuente | Categoría | Hallazgo |",
793
+ "|-----------|--------|-----------|----------|",
794
+ ]
795
+ for f in r.findings:
796
+ lines.append(f"| {f.severity} | {f.source} | {f.category} | {f.name} |")
797
+
798
+ lines += [""]
799
+ for f in r.findings:
800
+ if f.remediation:
801
+ lines += [
802
+ f"**{f.name}** ",
803
+ f"Detalle: {f.detail} ",
804
+ f"Remediación: {f.remediation}",
805
+ "",
806
+ ]
807
+ lines += ["---", ""]
808
+
809
+ return "\n".join(lines)
810
+
811
+
812
+ def _to_csv(results: List[TargetResult]) -> str:
813
+ import io
814
+ buf = io.StringIO()
815
+ w = csv.writer(buf)
816
+ w.writerow(["target", "target_type", "risk_grade", "severity", "source",
817
+ "category", "name", "detail", "remediation"])
818
+ for r in results:
819
+ for f in r.findings:
820
+ w.writerow([
821
+ r.target, r.target_type, r.summary_grade(),
822
+ f.severity, f.source, f.category, f.name, f.detail, f.remediation,
823
+ ])
824
+ return buf.getvalue()
825
+
826
+
827
+ def _to_html(results: List[TargetResult]) -> str:
828
+ """Informe HTML dark-theme standalone."""
829
+ rows_html = ""
830
+ for r in results:
831
+ grade = r.summary_grade()
832
+ grade_colors = {
833
+ "CRITICAL": "#ff4444", "HIGH": "#ff8800",
834
+ "MEDIUM": "#ffcc00", "LOW": "#44aaff", "CLEAN": "#44cc44",
835
+ }
836
+ gc = grade_colors.get(grade, "#888")
837
+
838
+ rows_html += f"""
839
+ <div class="target-block">
840
+ <div class="target-header">
841
+ <span class="target-name">{r.target}</span>
842
+ <span class="target-type">{r.target_type}</span>
843
+ <span class="risk-badge" style="background:{gc}">{grade}</span>
844
+ <span class="meta">{r.scan_time} · {len(r.findings)} hallazgos</span>
845
+ </div>
846
+ """
847
+ if r.findings:
848
+ rows_html += """
849
+ <table class="findings-table">
850
+ <thead><tr><th>SEV</th><th>Fuente</th><th>Categoría</th>
851
+ <th>Hallazgo</th><th>Detalle</th></tr></thead><tbody>
852
+ """
853
+ sev_colors = {
854
+ "CRITICAL": "#ff4444", "HIGH": "#ff8800",
855
+ "MEDIUM": "#ffcc00", "LOW": "#44aaff", "INFO": "#888888",
856
+ }
857
+ for f in r.findings:
858
+ sc = sev_colors.get(f.severity, "#888")
859
+ rows_html += f"""
860
+ <tr>
861
+ <td><span class="sev-tag" style="color:{sc}">{f.severity}</span></td>
862
+ <td>{f.source}</td>
863
+ <td>{f.category}</td>
864
+ <td>{f.name}</td>
865
+ <td class="detail-col">{f.detail[:200]}</td>
866
+ </tr>
867
+ """
868
+ rows_html += "</tbody></table>"
869
+ else:
870
+ rows_html += '<p class="clean-msg">Sin hallazgos en fuentes consultadas.</p>'
871
+
872
+ rows_html += "</div>"
873
+
874
+ ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
875
+ return f"""<!DOCTYPE html>
876
+ <html lang="es">
877
+ <head>
878
+ <meta charset="UTF-8">
879
+ <title>vamp-darkweb-intel — Informe</title>
880
+ <style>
881
+ body{{background:#0d1117;color:#c9d1d9;font-family:'Segoe UI',Helvetica,Arial,sans-serif;margin:0;padding:20px}}
882
+ h1{{color:#58a6ff;font-size:1.4rem;margin-bottom:4px}}
883
+ .meta-header{{color:#8b949e;font-size:.85rem;margin-bottom:24px}}
884
+ .target-block{{background:#161b22;border:1px solid #30363d;border-radius:8px;padding:16px;margin-bottom:20px}}
885
+ .target-header{{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:12px}}
886
+ .target-name{{font-size:1.1rem;font-weight:700;color:#e6edf3}}
887
+ .target-type{{background:#21262d;color:#8b949e;border-radius:4px;padding:2px 8px;font-size:.78rem}}
888
+ .risk-badge{{border-radius:4px;padding:3px 10px;font-size:.82rem;font-weight:700;color:#0d1117}}
889
+ .meta{{color:#8b949e;font-size:.8rem;margin-left:auto}}
890
+ .findings-table{{width:100%;border-collapse:collapse;font-size:.84rem;margin-top:8px}}
891
+ .findings-table th{{background:#21262d;color:#8b949e;text-align:left;padding:8px;border-bottom:1px solid #30363d}}
892
+ .findings-table td{{padding:8px;border-bottom:1px solid #21262d;vertical-align:top}}
893
+ .findings-table tr:hover td{{background:#1c2128}}
894
+ .sev-tag{{font-weight:700}}
895
+ .detail-col{{color:#8b949e;font-size:.8rem}}
896
+ .clean-msg{{color:#3fb950;font-size:.9rem;margin:8px 0}}
897
+ footer{{margin-top:32px;color:#484f58;font-size:.78rem;text-align:center}}
898
+ </style>
899
+ </head>
900
+ <body>
901
+ <h1>vamp-darkweb-intel — Informe de Threat Intelligence</h1>
902
+ <p class="meta-header">Generado: {ts} · Herramienta: {TOOL_NAME} v{VERSION} · VampSecure Labs</p>
903
+ {rows_html}
904
+ <footer>© VampSecure Studios — VampSecure Labs Security Research Division | Uso exclusivo en entornos autorizados</footer>
905
+ </body>
906
+ </html>"""
907
+
908
+
909
+ # ---------------------------------------------------------------------------
910
+ # CLI
911
+ # ---------------------------------------------------------------------------
912
+
913
+ def _build_parser() -> argparse.ArgumentParser:
914
+ p = argparse.ArgumentParser(
915
+ prog="vamp-darkweb-intel",
916
+ description=(
917
+ "vamp-darkweb-intel — Motor de Threat Intelligence y Darkweb\n"
918
+ "VampSecure Labs · Consulta cruzada multi-fuente con correlación automática"
919
+ ),
920
+ formatter_class=argparse.RawDescriptionHelpFormatter,
921
+ )
922
+ p.add_argument("-t", "--target",
923
+ metavar="TARGET", action="append", dest="targets", default=[],
924
+ help="Objetivo a investigar: dominio, IP, hash (MD5/SHA1/SHA256) o URL. "
925
+ "Repetible para múltiples objetivos.")
926
+ p.add_argument("--file",
927
+ metavar="FILE",
928
+ help="Fichero con un objetivo por línea.")
929
+ p.add_argument("--type",
930
+ choices=["domain", "ip", "hash", "url", "email"],
931
+ help="Forzar tipo de objetivo (por defecto: autodetectado).")
932
+ p.add_argument("--timeout", type=int, default=12, metavar="N",
933
+ help="Timeout por petición en segundos (default: 12).")
934
+ p.add_argument("--workers", type=int, default=6, metavar="N",
935
+ help="Consultas paralelas a fuentes (default: 6).")
936
+ p.add_argument("--json", metavar="FILE", dest="out_json",
937
+ help="Exportar resultados a JSON.")
938
+ p.add_argument("--html", metavar="FILE", dest="out_html",
939
+ help="Exportar informe HTML dark-theme.")
940
+ p.add_argument("--markdown", metavar="FILE", dest="out_md",
941
+ help="Exportar informe Markdown.")
942
+ p.add_argument("--csv", metavar="FILE", dest="out_csv",
943
+ help="Exportar resumen CSV.")
944
+ p.add_argument("--version", action="version", version=f"%(prog)s {VERSION}")
945
+ return p
946
+
947
+
948
+ def main() -> None:
949
+ p = _build_parser()
950
+ args = p.parse_args()
951
+
952
+ # Recoger objetivos
953
+ targets: List[str] = list(args.targets)
954
+ if args.file:
955
+ fp = Path(args.file) if 'Path' in dir() else __import__('pathlib').Path(args.file)
956
+ targets += [l.strip() for l in fp.read_text().splitlines() if l.strip() and not l.startswith("#")]
957
+
958
+ if not targets:
959
+ p.print_help()
960
+ sys.exit(0)
961
+
962
+ engine = DarkwebIntelEngine(timeout=args.timeout, workers=args.workers)
963
+ results: List[TargetResult] = []
964
+
965
+ for target in targets:
966
+ result = engine.scan(target, args.type)
967
+ results.append(result)
968
+ _print_result(result)
969
+
970
+ # Exportar
971
+ if args.out_json:
972
+ Path(args.out_json).write_text(_to_json(results), encoding="utf-8")
973
+ _console.print(f"[dim]→ JSON: {args.out_json}[/dim]")
974
+ if args.out_md:
975
+ Path(args.out_md).write_text(_to_markdown(results), encoding="utf-8")
976
+ _console.print(f"[dim]→ Markdown: {args.out_md}[/dim]")
977
+ if args.out_csv:
978
+ Path(args.out_csv).write_text(_to_csv(results), encoding="utf-8")
979
+ _console.print(f"[dim]→ CSV: {args.out_csv}[/dim]")
980
+ if args.out_html:
981
+ Path(args.out_html).write_text(_to_html(results), encoding="utf-8")
982
+ _console.print(f"[dim]→ HTML: {args.out_html}[/dim]")
983
+
984
+ # Código de salida
985
+ has_critical = any(r.critical() for r in results)
986
+ has_high = any(r.high() for r in results)
987
+ if has_critical:
988
+ sys.exit(2)
989
+ elif has_high:
990
+ sys.exit(1)
991
+ sys.exit(0)
992
+
993
+
994
+ if __name__ == "__main__":
995
+ from pathlib import Path
996
+ main()