vamp-easm 1.1__tar.gz

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_easm-1.1/PKG-INFO ADDED
@@ -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,190 @@
1
+ # vamp-easm
2
+
3
+ **Continuous External Attack Surface Management with daily diff tracking**
4
+
5
+ Part of the [VampSecure Labs](https://github.com/Vampsecure-Labs) security toolkit.
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ `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.
12
+
13
+ ### Key Features
14
+
15
+ - **Subdomain enumeration** — queries crt.sh (Certificate Transparency) and HackerTarget via standard urllib (no external dependencies for this layer)
16
+ - **Port scanning** — async TCP connect scan over the top-100 most common ports using `asyncio` + stdlib `socket`; optional `nmap` backend for accuracy
17
+ - **TLS certificate inspection** — hostname, issuer, expiration date, SANs, self-signed detection and SHA-256 fingerprint via stdlib `ssl`
18
+ - **SQLite history** — every scan is stored; diffs are computed against the last completed scan for the same target
19
+ - **Structured findings** — diffs are normalized as VSL findings (prefix `EASM-NNN`) compatible with `vamp-penreport`
20
+ - **Webhook alerts** — POSTs a JSON payload (Slack/Discord/Mattermost compatible) when CRITICAL or HIGH diffs are found
21
+ - **Exit codes** — machine-friendly: `0` clean, `1` HIGH diffs, `2` CRITICAL diffs (CI/CD and monitoring ready)
22
+
23
+ ---
24
+
25
+ ## Diff Types and Severities
26
+
27
+ | Category | Severity | Description |
28
+ |---|---|---|
29
+ | `NUEVO_SUBDOMINIO` | HIGH | A subdomain not seen in the previous scan has appeared |
30
+ | `NUEVO_PUERTO` | MEDIUM | A TCP port is open that was closed in the previous scan |
31
+ | `CERT_EXPIRADO` | HIGH / CRITICAL | TLS certificate expires within 30 days (HIGH) or is already expired (CRITICAL) |
32
+ | `CERT_CAMBIADO` | CRITICAL | TLS fingerprint changed since the last scan — possible re-issue or MitM |
33
+ | `SERVICIO_DESAPARECIDO` | LOW | A previously open port is no longer reachable |
34
+ | `IP_CAMBIADA` | MEDIUM | DNS resolution for a known subdomain returned a different IP |
35
+
36
+ ---
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ # 1. Clone and enter the directory
42
+ git clone https://github.com/Vampsecure-Labs/vamp-easm.git
43
+ cd vamp-easm
44
+
45
+ # 2. Create and activate a virtual environment
46
+ python3 -m venv .venv
47
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
48
+
49
+ # 3. Install dependencies
50
+ pip install -r requirements.txt
51
+ ```
52
+
53
+ > **Optional:** install `nmap` on your system and use `--nmap` for more reliable port scanning.
54
+
55
+ ---
56
+
57
+ ## Usage
58
+
59
+ ### Scan a target
60
+
61
+ ```bash
62
+ # Default: top-100 ports, stdlib async scanner
63
+ python vamp_easm.py scan --target example.com
64
+
65
+ # Custom port list
66
+ python vamp_easm.py scan --target example.com --ports 22,80,443,8080,8443
67
+
68
+ # Use nmap as port-scan backend (requires nmap in PATH)
69
+ python vamp_easm.py scan --target example.com --nmap
70
+
71
+ # Export findings as HTML and JSON reports
72
+ python vamp_easm.py scan --target example.com --html --json
73
+
74
+ # Send alerts to a webhook on CRITICAL/HIGH diffs
75
+ python vamp_easm.py scan --target example.com --alert-webhook https://hooks.slack.com/...
76
+
77
+ # Full example
78
+ python vamp_easm.py scan \
79
+ --target example.com \
80
+ --ports top100 \
81
+ --html \
82
+ --alert-webhook "$SLACK_WEBHOOK" \
83
+ --client "AcmeCorp" \
84
+ --engagement "Q3-2026-EASM"
85
+ ```
86
+
87
+ ### View scan history
88
+
89
+ ```bash
90
+ python vamp_easm.py history --target example.com
91
+ python vamp_easm.py history --target example.com --limit 20
92
+ ```
93
+
94
+ ### List known assets
95
+
96
+ ```bash
97
+ python vamp_easm.py assets --target example.com
98
+ ```
99
+
100
+ ### Export a snapshot
101
+
102
+ ```bash
103
+ # Export last scan as both JSON and HTML
104
+ python vamp_easm.py export --target example.com
105
+
106
+ # JSON only
107
+ python vamp_easm.py export --target example.com --json
108
+ ```
109
+
110
+ ---
111
+
112
+ ## Cron Example
113
+
114
+ Run a daily EASM scan with Slack alerts and log output:
115
+
116
+ ```bash
117
+ 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
118
+ ```
119
+
120
+ For CI/CD, use the exit code to gate pipelines:
121
+
122
+ ```bash
123
+ python vamp_easm.py scan --target example.com
124
+ EXIT=$?
125
+ if [ $EXIT -eq 2 ]; then
126
+ echo "CRITICAL diffs detected — blocking pipeline"
127
+ exit 1
128
+ elif [ $EXIT -eq 1 ]; then
129
+ echo "HIGH diffs detected — review required"
130
+ fi
131
+ ```
132
+
133
+ ---
134
+
135
+ ## Environment Variables
136
+
137
+ | Variable | Description |
138
+ |---|---|
139
+ | `EASM_ALERT_WEBHOOK` | Webhook URL for alerts (alternative to `--alert-webhook`) |
140
+
141
+ ---
142
+
143
+ ## Database
144
+
145
+ The SQLite database `vamp_easm.db` is created automatically in the working directory. It contains three tables:
146
+
147
+ - **`scans`** — one row per scan run (UUID, target, timestamps, asset/diff counts)
148
+ - **`assets`** — discovered hosts, subdomains, open ports and services with first/last-seen timestamps
149
+ - **`certs`** — TLS certificate snapshots (issuer, expiration, SANs, SHA-256 fingerprint)
150
+
151
+ The database file is excluded from version control (`.gitignore`). Back it up if you want to preserve historical data.
152
+
153
+ ---
154
+
155
+ ## Integration with vamp-penreport
156
+
157
+ `vamp-easm` exports findings in the VSL standard format used across all VampSecure Labs tools. To include EASM findings in a pentest report:
158
+
159
+ ```bash
160
+ # 1. Generate JSON snapshot from the last scan
161
+ python vamp_easm.py export --target example.com --json
162
+
163
+ # 2. Merge with vamp-penreport (pass the JSON as an additional findings source)
164
+ python ../vamp-penreport/vamp_penreport.py \
165
+ --findings easm_export_example.com_*.json \
166
+ --client "AcmeCorp" \
167
+ --engagement "Pentest-2026-Q3" \
168
+ --html report_acmecorp.html
169
+ ```
170
+
171
+ The `EASM-NNN` finding IDs are stable within a single scan run and can be referenced in report narratives.
172
+
173
+ ---
174
+
175
+ ## Dependencies
176
+
177
+ | Package | Purpose |
178
+ |---|---|
179
+ | `aiohttp>=3.9.0` | Async HTTP client for subdomain source queries |
180
+ | `rich>=13.7.0` | Terminal output tables and panels |
181
+ | stdlib only | Port scanning, TLS inspection, DNS queries, alerts |
182
+
183
+ ---
184
+
185
+ ## License
186
+
187
+ MIT License — see [LICENSE](LICENSE) for details.
188
+
189
+ © VampSecure Studios — VampSecure Labs Security Research Division
190
+ For authorized penetration testing use only.
@@ -0,0 +1,5 @@
1
+ # © VampSecure Studios — VampSecure Labs Security Research Division
2
+ """
3
+ easm — Paquete interno de vamp-easm
4
+ Módulos: scanner, differ, storage, alerter
5
+ """
@@ -0,0 +1,207 @@
1
+ # © VampSecure Studios — VampSecure Labs Security Research Division
2
+ """
3
+ alerter.py — Sistema de alertas para vamp-easm
4
+ ================================================
5
+ Envía notificaciones cuando se detectan diffs de severidad CRITICAL o HIGH.
6
+
7
+ Métodos de alerta disponibles:
8
+ · Webhook HTTP — POST JSON al endpoint configurado
9
+ · Variable de entorno EASM_ALERT_WEBHOOK como alternativa al flag --alert-webhook
10
+
11
+ El payload enviado al webhook sigue el esquema estándar VSL compatible
12
+ con Slack (incoming webhooks), Discord, Mattermost y cualquier receptor
13
+ que acepte JSON.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import os
20
+ import urllib.request
21
+ import urllib.error
22
+ from datetime import datetime, timezone
23
+ from typing import List, Optional
24
+
25
+ from .differ import Diff
26
+
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Severidades que disparan alertas
30
+ # ---------------------------------------------------------------------------
31
+
32
+ SEVERIDADES_ALERTA = {"CRITICAL", "HIGH"}
33
+
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Construcción del payload de alerta
37
+ # ---------------------------------------------------------------------------
38
+
39
+ def _construir_payload(
40
+ target: str,
41
+ diffs: List[Diff],
42
+ scan_id: str,
43
+ ) -> dict:
44
+ """
45
+ Construye el payload JSON que se envía al webhook.
46
+
47
+ El formato es compatible con Slack incoming webhooks mediante el campo
48
+ 'text' y 'attachments', y también incluye un campo estructurado 'data'
49
+ con todos los diffs para integraciones personalizadas.
50
+
51
+ Parameters
52
+ ----------
53
+ target : Dominio escaneado
54
+ diffs : Lista de diffs CRITICAL/HIGH a alertar
55
+ scan_id : UUID del escaneo que generó los diffs
56
+
57
+ Returns
58
+ -------
59
+ dict : Payload listo para serializar a JSON
60
+ """
61
+ ahora = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
62
+
63
+ # Resumen para Slack/Discord (campo 'text')
64
+ resumen_lineas = [f"*[EASM] Alerta de superficie de ataque — {target}*"]
65
+ for d in diffs:
66
+ emoji = "🚨" if d.severidad == "CRITICAL" else "⚠️"
67
+ resumen_lineas.append(f"{emoji} `{d.categoria}` [{d.severidad}] — {d.activo}")
68
+ resumen = "\n".join(resumen_lineas)
69
+
70
+ # Detalles estructurados
71
+ detalles = []
72
+ for d in diffs:
73
+ detalles.append({
74
+ "categoria": d.categoria,
75
+ "severidad": d.severidad,
76
+ "activo": d.activo,
77
+ "descripcion": d.descripcion,
78
+ "evidencia": d.evidencia,
79
+ "finding_id": d.finding.id if d.finding else None,
80
+ })
81
+
82
+ return {
83
+ "text": resumen,
84
+ "username": "vamp-easm",
85
+ "attachments": [
86
+ {
87
+ "color": "#c0392b" if any(d.severidad == "CRITICAL" for d in diffs) else "#d35400",
88
+ "title": f"EASM — {target} — {ahora}",
89
+ "text": f"scan_id: `{scan_id}`\n{len(diffs)} diffs alertables",
90
+ "footer": "VampSecure Labs · vamp-easm",
91
+ }
92
+ ],
93
+ "data": {
94
+ "tool": "vamp-easm",
95
+ "version": "1.0",
96
+ "target": target,
97
+ "scan_id": scan_id,
98
+ "ts": ahora,
99
+ "diffs": detalles,
100
+ },
101
+ }
102
+
103
+
104
+ # ---------------------------------------------------------------------------
105
+ # Envío de alertas
106
+ # ---------------------------------------------------------------------------
107
+
108
+ def enviar_webhook(
109
+ url: str,
110
+ target: str,
111
+ diffs: List[Diff],
112
+ scan_id: str,
113
+ timeout: int = 10,
114
+ ) -> bool:
115
+ """
116
+ Envía un POST JSON al webhook especificado con los diffs alertables.
117
+
118
+ Solo procesa diffs de severidad CRITICAL o HIGH. Si no hay ninguno,
119
+ no envía nada.
120
+
121
+ Parameters
122
+ ----------
123
+ url : URL del webhook receptor
124
+ target : Dominio escaneado
125
+ diffs : Todos los diffs del escaneo (se filtra por severidad)
126
+ scan_id : UUID del escaneo
127
+ timeout : Segundos de timeout para la petición HTTP
128
+
129
+ Returns
130
+ -------
131
+ bool : True si el envío fue exitoso (HTTP 2xx), False si falló
132
+ """
133
+ diffs_alertables = [d for d in diffs if d.severidad in SEVERIDADES_ALERTA]
134
+ if not diffs_alertables:
135
+ return True # nada que alertar → éxito
136
+
137
+ payload = _construir_payload(target, diffs_alertables, scan_id)
138
+ body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
139
+
140
+ req = urllib.request.Request(
141
+ url,
142
+ data=body,
143
+ headers={
144
+ "Content-Type": "application/json",
145
+ "User-Agent": "vamp-easm/1.0",
146
+ },
147
+ method="POST",
148
+ )
149
+
150
+ try:
151
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
152
+ return 200 <= resp.status < 300
153
+ except urllib.error.HTTPError as exc:
154
+ return False
155
+ except Exception:
156
+ return False
157
+
158
+
159
+ def resolver_webhook_url(flag_url: Optional[str]) -> Optional[str]:
160
+ """
161
+ Resuelve la URL del webhook: prioriza el flag CLI, luego la variable
162
+ de entorno EASM_ALERT_WEBHOOK.
163
+
164
+ Parameters
165
+ ----------
166
+ flag_url : Valor del flag --alert-webhook (None si no se proporcionó)
167
+
168
+ Returns
169
+ -------
170
+ str | None : URL del webhook, o None si no está configurado
171
+ """
172
+ if flag_url:
173
+ return flag_url
174
+ return os.environ.get("EASM_ALERT_WEBHOOK")
175
+
176
+
177
+ def gestionar_alertas(
178
+ webhook_url: Optional[str],
179
+ target: str,
180
+ diffs: List[Diff],
181
+ scan_id: str,
182
+ ) -> None:
183
+ """
184
+ Punto de entrada principal del sistema de alertas.
185
+
186
+ Comprueba si hay diffs alertables y, si existe un webhook configurado,
187
+ intenta el envío. Los errores de envío no interrumpen la ejecución
188
+ del programa principal.
189
+
190
+ Parameters
191
+ ----------
192
+ webhook_url : URL del webhook (None → solo log en consola)
193
+ target : Dominio escaneado
194
+ diffs : Lista de diffs del escaneo
195
+ scan_id : UUID del escaneo
196
+ """
197
+ diffs_criticos = [d for d in diffs if d.severidad == "CRITICAL"]
198
+ diffs_altos = [d for d in diffs if d.severidad == "HIGH"]
199
+
200
+ if not diffs_criticos and not diffs_altos:
201
+ return
202
+
203
+ if not webhook_url:
204
+ # Sin webhook configurado: el usuario verá los diffs en la salida estándar
205
+ return
206
+
207
+ enviar_webhook(webhook_url, target, diffs, scan_id)