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.
- vamp_darkweb_intel-1.0.0.dist-info/METADATA +229 -0
- vamp_darkweb_intel-1.0.0.dist-info/RECORD +7 -0
- vamp_darkweb_intel-1.0.0.dist-info/WHEEL +5 -0
- vamp_darkweb_intel-1.0.0.dist-info/entry_points.txt +2 -0
- vamp_darkweb_intel-1.0.0.dist-info/licenses/LICENSE +21 -0
- vamp_darkweb_intel-1.0.0.dist-info/top_level.txt +1 -0
- vamp_darkweb_intel.py +996 -0
|
@@ -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,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()
|