vamp-easm 1.1__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.
easm/storage.py ADDED
@@ -0,0 +1,295 @@
1
+ # © VampSecure Studios — VampSecure Labs Security Research Division
2
+ """
3
+ storage.py — Capa de persistencia SQLite para vamp-easm
4
+ =========================================================
5
+ Gestiona el historial de escaneos, activos descubiertos y certificados TLS.
6
+ La base de datos se crea automáticamente en el directorio de trabajo.
7
+
8
+ Tablas:
9
+ · scans — registro de cada ejecución (UUID, target, timestamps, contadores)
10
+ · assets — activos descubiertos: subdominios, IPs, puertos, servicios
11
+ · certs — certificados TLS con metadatos de validez y huella digital
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import sqlite3
17
+ import uuid
18
+ from datetime import datetime, timezone
19
+ from pathlib import Path
20
+ from typing import List, Optional
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Nombre del fichero de base de datos (relativo al directorio de trabajo)
25
+ # ---------------------------------------------------------------------------
26
+ DB_FILE = "vamp_easm.db"
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # DDL — creación de tablas si no existen
30
+ # ---------------------------------------------------------------------------
31
+
32
+ _DDL = """
33
+ CREATE TABLE IF NOT EXISTS scans (
34
+ id TEXT PRIMARY KEY,
35
+ target TEXT NOT NULL,
36
+ ts_start TEXT NOT NULL,
37
+ ts_end TEXT,
38
+ assets_found INTEGER DEFAULT 0,
39
+ diffs_found INTEGER DEFAULT 0
40
+ );
41
+
42
+ -- Inventario deduplicado: una fila por activo único (target, subdominio, puerto).
43
+ -- primera_vista / ultima_vista registran cuándo se vio por primera y última vez.
44
+ CREATE TABLE IF NOT EXISTS assets (
45
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
46
+ target TEXT NOT NULL,
47
+ subdominio TEXT NOT NULL,
48
+ ip TEXT,
49
+ puerto INTEGER,
50
+ protocolo TEXT DEFAULT 'tcp',
51
+ servicio TEXT,
52
+ primera_vista TEXT NOT NULL,
53
+ ultima_vista TEXT NOT NULL,
54
+ UNIQUE(target, subdominio, puerto)
55
+ );
56
+
57
+ -- Log por escaneo: registra exactamente qué activos se encontraron en cada scan.
58
+ -- Permite calcular diffs entre el escaneo actual y el anterior sin ambigüedad.
59
+ CREATE TABLE IF NOT EXISTS scan_asset_log (
60
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
61
+ scan_id TEXT NOT NULL,
62
+ target TEXT NOT NULL,
63
+ subdominio TEXT NOT NULL,
64
+ ip TEXT,
65
+ puerto INTEGER,
66
+ protocolo TEXT DEFAULT 'tcp',
67
+ servicio TEXT,
68
+ UNIQUE(scan_id, subdominio, puerto)
69
+ );
70
+
71
+ CREATE TABLE IF NOT EXISTS certs (
72
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
73
+ target TEXT NOT NULL,
74
+ host TEXT NOT NULL,
75
+ issued_to TEXT,
76
+ issuer TEXT,
77
+ not_after TEXT,
78
+ sans TEXT,
79
+ fingerprint TEXT,
80
+ autofirmado INTEGER DEFAULT 0,
81
+ ts TEXT NOT NULL,
82
+ scan_id TEXT
83
+ );
84
+
85
+ CREATE INDEX IF NOT EXISTS idx_assets_target ON assets(target);
86
+ CREATE INDEX IF NOT EXISTS idx_sal_scan ON scan_asset_log(scan_id);
87
+ CREATE INDEX IF NOT EXISTS idx_sal_target ON scan_asset_log(target);
88
+ CREATE INDEX IF NOT EXISTS idx_certs_target ON certs(target);
89
+ CREATE INDEX IF NOT EXISTS idx_scans_target ON scans(target);
90
+ """
91
+
92
+
93
+ def _conectar() -> sqlite3.Connection:
94
+ """Abre (o crea) la base de datos y retorna una conexión con row_factory."""
95
+ conn = sqlite3.connect(DB_FILE)
96
+ conn.row_factory = sqlite3.Row
97
+ conn.execute("PRAGMA journal_mode=WAL")
98
+ conn.execute("PRAGMA foreign_keys=ON")
99
+ return conn
100
+
101
+
102
+ def inicializar_db() -> None:
103
+ """Crea las tablas si no existen. Llamar al inicio de cada ejecución."""
104
+ with _conectar() as conn:
105
+ conn.executescript(_DDL)
106
+
107
+
108
+ # ---------------------------------------------------------------------------
109
+ # Operaciones sobre scans
110
+ # ---------------------------------------------------------------------------
111
+
112
+ def nuevo_scan(target: str) -> str:
113
+ """
114
+ Registra el inicio de un nuevo escaneo y devuelve su UUID.
115
+
116
+ Parameters
117
+ ----------
118
+ target : Dominio objetivo del escaneo
119
+
120
+ Returns
121
+ -------
122
+ str : UUID del escaneo recién creado
123
+ """
124
+ scan_id = str(uuid.uuid4())
125
+ ahora = _ahora()
126
+ with _conectar() as conn:
127
+ conn.execute(
128
+ "INSERT INTO scans (id, target, ts_start) VALUES (?, ?, ?)",
129
+ (scan_id, target, ahora),
130
+ )
131
+ return scan_id
132
+
133
+
134
+ def cerrar_scan(scan_id: str, assets_found: int, diffs_found: int) -> None:
135
+ """Actualiza el escaneo con timestamp de fin y contadores."""
136
+ with _conectar() as conn:
137
+ conn.execute(
138
+ """UPDATE scans
139
+ SET ts_end=?, assets_found=?, diffs_found=?
140
+ WHERE id=?""",
141
+ (_ahora(), assets_found, diffs_found, scan_id),
142
+ )
143
+
144
+
145
+ def ultimo_scan(target: str) -> Optional[sqlite3.Row]:
146
+ """Devuelve el registro del último escaneo completado para el target."""
147
+ with _conectar() as conn:
148
+ return conn.execute(
149
+ """SELECT * FROM scans
150
+ WHERE target=? AND ts_end IS NOT NULL
151
+ ORDER BY ts_end DESC LIMIT 1""",
152
+ (target,),
153
+ ).fetchone()
154
+
155
+
156
+ def historial_scans(target: str, limit: int = 10) -> List[sqlite3.Row]:
157
+ """Lista los últimos N escaneos completados de un target, de más reciente a más antiguo."""
158
+ with _conectar() as conn:
159
+ return conn.execute(
160
+ """SELECT * FROM scans
161
+ WHERE target=? AND ts_end IS NOT NULL
162
+ ORDER BY ts_end DESC LIMIT ?""",
163
+ (target, limit),
164
+ ).fetchall()
165
+
166
+
167
+ # ---------------------------------------------------------------------------
168
+ # Operaciones sobre assets
169
+ # ---------------------------------------------------------------------------
170
+
171
+ def upsert_asset(
172
+ target: str,
173
+ subdominio: str,
174
+ scan_id: str,
175
+ ip: Optional[str] = None,
176
+ puerto: Optional[int] = None,
177
+ protocolo: str = "tcp",
178
+ servicio: Optional[str] = None,
179
+ ) -> None:
180
+ """
181
+ Inserta o actualiza el inventario deduplicado de activos y registra el
182
+ activo en el log del escaneo actual (scan_asset_log).
183
+
184
+ El inventario (tabla assets) mantiene primera_vista/ultima_vista.
185
+ El log por escaneo (tabla scan_asset_log) registra exactamente qué se
186
+ encontró en cada scan_id, lo que permite calcular diffs correctamente.
187
+
188
+ Parameters
189
+ ----------
190
+ target : Dominio raíz objetivo
191
+ subdominio : Subdominio o host encontrado
192
+ scan_id : UUID del escaneo actual
193
+ ip : Dirección IP resuelta (None si no resuelve)
194
+ puerto : Puerto TCP abierto (None para registrar solo el subdominio)
195
+ protocolo : Protocolo (por defecto 'tcp')
196
+ servicio : Nombre del servicio detectado
197
+ """
198
+ ahora = _ahora()
199
+ with _conectar() as conn:
200
+ # Actualizar inventario deduplicado
201
+ conn.execute(
202
+ """INSERT INTO assets
203
+ (target, subdominio, ip, puerto, protocolo, servicio, primera_vista, ultima_vista)
204
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
205
+ ON CONFLICT(target, subdominio, puerto) DO UPDATE SET
206
+ ip=excluded.ip,
207
+ servicio=excluded.servicio,
208
+ ultima_vista=excluded.ultima_vista""",
209
+ (target, subdominio, ip, puerto, protocolo, servicio, ahora, ahora),
210
+ )
211
+ # Registrar en el log del escaneo actual (snapshot por scan)
212
+ conn.execute(
213
+ """INSERT OR IGNORE INTO scan_asset_log
214
+ (scan_id, target, subdominio, ip, puerto, protocolo, servicio)
215
+ VALUES (?, ?, ?, ?, ?, ?, ?)""",
216
+ (scan_id, target, subdominio, ip, puerto, protocolo, servicio),
217
+ )
218
+
219
+
220
+ def todos_los_assets(target: str) -> List[sqlite3.Row]:
221
+ """Devuelve todos los activos conocidos para un target (inventario completo)."""
222
+ with _conectar() as conn:
223
+ return conn.execute(
224
+ "SELECT * FROM assets WHERE target=? ORDER BY subdominio, puerto",
225
+ (target,),
226
+ ).fetchall()
227
+
228
+
229
+ def assets_del_scan(target: str, scan_id: str) -> List[sqlite3.Row]:
230
+ """
231
+ Devuelve los activos registrados en un escaneo concreto desde scan_asset_log.
232
+ Usado por el motor de diffs para comparar dos escaneos.
233
+ """
234
+ with _conectar() as conn:
235
+ return conn.execute(
236
+ """SELECT * FROM scan_asset_log
237
+ WHERE target=? AND scan_id=?
238
+ ORDER BY subdominio, puerto""",
239
+ (target, scan_id),
240
+ ).fetchall()
241
+
242
+
243
+ # ---------------------------------------------------------------------------
244
+ # Operaciones sobre certificados
245
+ # ---------------------------------------------------------------------------
246
+
247
+ def insertar_cert(
248
+ target: str,
249
+ host: str,
250
+ scan_id: str,
251
+ issued_to: Optional[str] = None,
252
+ issuer: Optional[str] = None,
253
+ not_after: Optional[str] = None,
254
+ sans: Optional[str] = None,
255
+ fingerprint: Optional[str] = None,
256
+ autofirmado: bool = False,
257
+ ) -> None:
258
+ """Inserta un registro de certificado TLS para el escaneo actual."""
259
+ with _conectar() as conn:
260
+ conn.execute(
261
+ """INSERT INTO certs
262
+ (target, host, issued_to, issuer, not_after, sans, fingerprint, autofirmado, ts, scan_id)
263
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
264
+ (target, host, issued_to, issuer, not_after, sans, fingerprint,
265
+ 1 if autofirmado else 0, _ahora(), scan_id),
266
+ )
267
+
268
+
269
+ def certs_del_scan(target: str, scan_id: str) -> List[sqlite3.Row]:
270
+ """Devuelve los certificados registrados en un escaneo concreto."""
271
+ with _conectar() as conn:
272
+ return conn.execute(
273
+ "SELECT * FROM certs WHERE target=? AND scan_id=? ORDER BY host",
274
+ (target, scan_id),
275
+ ).fetchall()
276
+
277
+
278
+ def cert_anterior(target: str, host: str, scan_id_actual: str) -> Optional[sqlite3.Row]:
279
+ """Devuelve el último certificado registrado para este host antes del escaneo actual."""
280
+ with _conectar() as conn:
281
+ return conn.execute(
282
+ """SELECT * FROM certs
283
+ WHERE target=? AND host=? AND scan_id != ?
284
+ ORDER BY ts DESC LIMIT 1""",
285
+ (target, host, scan_id_actual),
286
+ ).fetchone()
287
+
288
+
289
+ # ---------------------------------------------------------------------------
290
+ # Utilidades internas
291
+ # ---------------------------------------------------------------------------
292
+
293
+ def _ahora() -> str:
294
+ """Devuelve el timestamp actual en formato ISO-8601 UTC."""
295
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
@@ -0,0 +1,210 @@
1
+ Metadata-Version: 2.4
2
+ Name: vamp-easm
3
+ Version: 1.1
4
+ Summary: External Attack Surface Management (EASM) scanner for authorized audits
5
+ Author-email: VampSecure Studios <contact@vampsecurestudios.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Vampsecure-Labs/vamp-easm
8
+ Project-URL: Repository, https://github.com/Vampsecure-Labs/vamp-easm
9
+ Keywords: security,pentest,audit,cybersecurity,vampsecure,easm,attack-surface,external,reconnaissance
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
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: rich>=13.7.0
20
+
21
+ # vamp-easm
22
+
23
+ **Continuous External Attack Surface Management with daily diff tracking**
24
+
25
+ Part of the [VampSecure Labs](https://github.com/Vampsecure-Labs) security toolkit.
26
+
27
+ ---
28
+
29
+ ## Overview
30
+
31
+ `vamp-easm` is a lightweight EASM engine designed to run continuously (via cron or CI/CD) against one or more external domains. Unlike point-in-time scanners, its core value is **delta detection**: each run is compared against the previous snapshot stored in a local SQLite database, surfacing only what changed.
32
+
33
+ ### Key Features
34
+
35
+ - **Subdomain enumeration** — queries crt.sh (Certificate Transparency) and HackerTarget via standard urllib (no external dependencies for this layer)
36
+ - **Port scanning** — async TCP connect scan over the top-100 most common ports using `asyncio` + stdlib `socket`; optional `nmap` backend for accuracy
37
+ - **TLS certificate inspection** — hostname, issuer, expiration date, SANs, self-signed detection and SHA-256 fingerprint via stdlib `ssl`
38
+ - **SQLite history** — every scan is stored; diffs are computed against the last completed scan for the same target
39
+ - **Structured findings** — diffs are normalized as VSL findings (prefix `EASM-NNN`) compatible with `vamp-penreport`
40
+ - **Webhook alerts** — POSTs a JSON payload (Slack/Discord/Mattermost compatible) when CRITICAL or HIGH diffs are found
41
+ - **Exit codes** — machine-friendly: `0` clean, `1` HIGH diffs, `2` CRITICAL diffs (CI/CD and monitoring ready)
42
+
43
+ ---
44
+
45
+ ## Diff Types and Severities
46
+
47
+ | Category | Severity | Description |
48
+ |---|---|---|
49
+ | `NUEVO_SUBDOMINIO` | HIGH | A subdomain not seen in the previous scan has appeared |
50
+ | `NUEVO_PUERTO` | MEDIUM | A TCP port is open that was closed in the previous scan |
51
+ | `CERT_EXPIRADO` | HIGH / CRITICAL | TLS certificate expires within 30 days (HIGH) or is already expired (CRITICAL) |
52
+ | `CERT_CAMBIADO` | CRITICAL | TLS fingerprint changed since the last scan — possible re-issue or MitM |
53
+ | `SERVICIO_DESAPARECIDO` | LOW | A previously open port is no longer reachable |
54
+ | `IP_CAMBIADA` | MEDIUM | DNS resolution for a known subdomain returned a different IP |
55
+
56
+ ---
57
+
58
+ ## Installation
59
+
60
+ ```bash
61
+ # 1. Clone and enter the directory
62
+ git clone https://github.com/Vampsecure-Labs/vamp-easm.git
63
+ cd vamp-easm
64
+
65
+ # 2. Create and activate a virtual environment
66
+ python3 -m venv .venv
67
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
68
+
69
+ # 3. Install dependencies
70
+ pip install -r requirements.txt
71
+ ```
72
+
73
+ > **Optional:** install `nmap` on your system and use `--nmap` for more reliable port scanning.
74
+
75
+ ---
76
+
77
+ ## Usage
78
+
79
+ ### Scan a target
80
+
81
+ ```bash
82
+ # Default: top-100 ports, stdlib async scanner
83
+ python vamp_easm.py scan --target example.com
84
+
85
+ # Custom port list
86
+ python vamp_easm.py scan --target example.com --ports 22,80,443,8080,8443
87
+
88
+ # Use nmap as port-scan backend (requires nmap in PATH)
89
+ python vamp_easm.py scan --target example.com --nmap
90
+
91
+ # Export findings as HTML and JSON reports
92
+ python vamp_easm.py scan --target example.com --html --json
93
+
94
+ # Send alerts to a webhook on CRITICAL/HIGH diffs
95
+ python vamp_easm.py scan --target example.com --alert-webhook https://hooks.slack.com/...
96
+
97
+ # Full example
98
+ python vamp_easm.py scan \
99
+ --target example.com \
100
+ --ports top100 \
101
+ --html \
102
+ --alert-webhook "$SLACK_WEBHOOK" \
103
+ --client "AcmeCorp" \
104
+ --engagement "Q3-2026-EASM"
105
+ ```
106
+
107
+ ### View scan history
108
+
109
+ ```bash
110
+ python vamp_easm.py history --target example.com
111
+ python vamp_easm.py history --target example.com --limit 20
112
+ ```
113
+
114
+ ### List known assets
115
+
116
+ ```bash
117
+ python vamp_easm.py assets --target example.com
118
+ ```
119
+
120
+ ### Export a snapshot
121
+
122
+ ```bash
123
+ # Export last scan as both JSON and HTML
124
+ python vamp_easm.py export --target example.com
125
+
126
+ # JSON only
127
+ python vamp_easm.py export --target example.com --json
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Cron Example
133
+
134
+ Run a daily EASM scan with Slack alerts and log output:
135
+
136
+ ```bash
137
+ 0 6 * * * cd /opt/vamp-easm && .venv/bin/python vamp_easm.py scan --target midominio.com --alert-webhook $SLACK_URL >> /var/log/vamp-easm.log 2>&1
138
+ ```
139
+
140
+ For CI/CD, use the exit code to gate pipelines:
141
+
142
+ ```bash
143
+ python vamp_easm.py scan --target example.com
144
+ EXIT=$?
145
+ if [ $EXIT -eq 2 ]; then
146
+ echo "CRITICAL diffs detected — blocking pipeline"
147
+ exit 1
148
+ elif [ $EXIT -eq 1 ]; then
149
+ echo "HIGH diffs detected — review required"
150
+ fi
151
+ ```
152
+
153
+ ---
154
+
155
+ ## Environment Variables
156
+
157
+ | Variable | Description |
158
+ |---|---|
159
+ | `EASM_ALERT_WEBHOOK` | Webhook URL for alerts (alternative to `--alert-webhook`) |
160
+
161
+ ---
162
+
163
+ ## Database
164
+
165
+ The SQLite database `vamp_easm.db` is created automatically in the working directory. It contains three tables:
166
+
167
+ - **`scans`** — one row per scan run (UUID, target, timestamps, asset/diff counts)
168
+ - **`assets`** — discovered hosts, subdomains, open ports and services with first/last-seen timestamps
169
+ - **`certs`** — TLS certificate snapshots (issuer, expiration, SANs, SHA-256 fingerprint)
170
+
171
+ The database file is excluded from version control (`.gitignore`). Back it up if you want to preserve historical data.
172
+
173
+ ---
174
+
175
+ ## Integration with vamp-penreport
176
+
177
+ `vamp-easm` exports findings in the VSL standard format used across all VampSecure Labs tools. To include EASM findings in a pentest report:
178
+
179
+ ```bash
180
+ # 1. Generate JSON snapshot from the last scan
181
+ python vamp_easm.py export --target example.com --json
182
+
183
+ # 2. Merge with vamp-penreport (pass the JSON as an additional findings source)
184
+ python ../vamp-penreport/vamp_penreport.py \
185
+ --findings easm_export_example.com_*.json \
186
+ --client "AcmeCorp" \
187
+ --engagement "Pentest-2026-Q3" \
188
+ --html report_acmecorp.html
189
+ ```
190
+
191
+ The `EASM-NNN` finding IDs are stable within a single scan run and can be referenced in report narratives.
192
+
193
+ ---
194
+
195
+ ## Dependencies
196
+
197
+ | Package | Purpose |
198
+ |---|---|
199
+ | `aiohttp>=3.9.0` | Async HTTP client for subdomain source queries |
200
+ | `rich>=13.7.0` | Terminal output tables and panels |
201
+ | stdlib only | Port scanning, TLS inspection, DNS queries, alerts |
202
+
203
+ ---
204
+
205
+ ## License
206
+
207
+ MIT License — see [LICENSE](LICENSE) for details.
208
+
209
+ © VampSecure Studios — VampSecure Labs Security Research Division
210
+ For authorized penetration testing use only.
@@ -0,0 +1,12 @@
1
+ vamp_easm.py,sha256=-ZgcRIi7tM6QEY5pud4hyz3KeKBhxtHQauFXYjRc6V4,22141
2
+ vampsec_report.py,sha256=tpQS1RMleLa6TvlQrXnUOc7OZUKcimd2x-KF2FsUObY,38980
3
+ easm/__init__.py,sha256=QFq6MMSxBSh_a2R2sIF80sWRqkJsvXHMlubYFGttRKg,161
4
+ easm/alerter.py,sha256=pcQ7pFZfreUNYDnIsa8cqw-HfYI-Ih01yvTI2VA_3BI,6322
5
+ easm/differ.py,sha256=4OpGYpE8bntApzGm4SAx3_HKO22oThS7BbdmfbT0UFw,16335
6
+ easm/scanner.py,sha256=9E7S6eUGs_29U8pXG99fHqyAnTmSgO0yZ3JpfvoP2Gg,16859
7
+ easm/storage.py,sha256=bckyOodN8K8VmdtNu5OavcXykbM_UaWTaNboogx0Fo0,10505
8
+ vamp_easm-1.1.dist-info/METADATA,sha256=Kr0JpcugZS1Kg8blqk53wYd5T9_dcM9WWPNihjOmjGI,6914
9
+ vamp_easm-1.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
10
+ vamp_easm-1.1.dist-info/entry_points.txt,sha256=bo12sf_rxAM4p1FiraTAYPZU8QbX3QJXiCT0Odrk0iE,45
11
+ vamp_easm-1.1.dist-info/top_level.txt,sha256=LgGCVN7KonRu1OSW0XYhJrnjnOBM7ETnwTtXukY3eH8,30
12
+ vamp_easm-1.1.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-easm = vamp_easm:main
@@ -0,0 +1,3 @@
1
+ easm
2
+ vamp_easm
3
+ vampsec_report