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/__init__.py +5 -0
- easm/alerter.py +207 -0
- easm/differ.py +374 -0
- easm/scanner.py +517 -0
- easm/storage.py +295 -0
- vamp_easm-1.1.dist-info/METADATA +210 -0
- vamp_easm-1.1.dist-info/RECORD +12 -0
- vamp_easm-1.1.dist-info/WHEEL +5 -0
- vamp_easm-1.1.dist-info/entry_points.txt +2 -0
- vamp_easm-1.1.dist-info/top_level.txt +3 -0
- vamp_easm.py +563 -0
- vampsec_report.py +959 -0
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,,
|