Flussi di lavoro per l'analisi dell'output, l'integrazione e il monitoraggio continuo
Analisi Programmatica dell'Output XML di Nmap
L'output XML di Nmap (-oX) è l'unico formato sufficientemente strutturato per un'automazione affidabile. Lo schema è semplice: un elemento radice nmaprun contenente nodi host, ciascuno con figli address, hostnames, ports e os. Per Python, hai due percorsi pratici: la libreria python-nmap per l'orchestrazione diretta delle scansioni con accesso agli oggetti, oppure libnmap (che include parser, report e backend MongoDB/Elastic) per il post-processing di file esistenti. Quando devi solo analizzare—specialmente in un worker CI che non ha eseguito la scansione—evita il wrapper e usa la libreria standard.
Ecco un parser rodato in produzione che estrae le porte nuovamente aperte effettuando il diff rispetto a una baseline di scansione precedente:
#!/usr/bin/env python3
"""Extract new open ports from Nmap XML compared to a baseline."""
import xml.etree.ElementTree as ET
from pathlib import Path
from datetime import datetime
import sys
def parse_ports(xml_path):
"""Return dict: {(ip, port, proto): service_banner}."""
ports = {}
tree = ET.parse(xml_path)
for host in tree.findall('host'):
status = host.find('status')
if status is None or status.get('state') != 'up':
continue
ip = host.find('address').get('addr')
ports_elem = host.find('ports')
if ports_elem is None:
continue
for port in ports_elem.findall('port'):
if port.find('state').get('state') != 'open':
continue
portid = port.get('portid')
proto = port.get('protocol')
service = port.find('service')
banner = ''
if service is not None:
banner = service.get('name', '')
if service.get('product'):
banner += f" {service.get('product')}"
if service.get('version'):
banner += f" {service.get('version')}"
ports[(ip, portid, proto)] = banner.strip()
return ports
def diff_scans(baseline_path, current_path):
baseline = parse_ports(baseline_path)
current = parse_ports(current_path)
new = {k: v for k, v in current.items() if k not in baseline}
closed = {k: v for k, v in baseline.items() if k not in current}
return new, closed
if __name__ == '__main__':
new, closed = diff_scans(sys.argv[1], sys.argv[2])
print(f";; Delta report generated {datetime.utcnow().isoformat()}Z")
for (ip, port, proto), banner in sorted(new):
print(f"[NEW] {ip}:{port}/{proto} {banner}")
for (ip, port, proto), banner in sorted(closed):
print(f"[CLOSED] {ip}:{port}/{proto} {banner}")
Cosa fa: Confronta due file XML di Nmap, riportando le porte apparse o scomparse. Quando usarlo: Job cron notturni, gate CI, o triage incident-response quando servono delta actionable automaticamente. Rischi: XML senza
--service-versionproduce banner vuoti; abbinalo sempre a-sVper diff significativi. Output atteso: Righe prefissate con[NEW]o[CLOSED]con IP, porta, protocollo e fingerprint del servizio.
La libreria python-nmap incapsula il binario ed espone i risultati come dizionari; libnmap offre oggetti di reporting più sofisticati e serializzazione integrata. Entrambe si basano sullo stesso XML sottostante. Per Go o Rust, genera struct dallo schema con xsdgen o serde—la community ha pubblicato diverse implementazioni corrette.
Scansione Differenziale con Ndiff
Per confronti ad-hoc senza scrivere codice, Nmap include ndiff, un diff semantico che comprende la logica di scansione invece di effettuare un semplice diff testuale. Ignora il rumore di timestamp e runtime, concentrandosi sui cambiamenti di stato degli host, delle porte e del SO.
# Lab: Confronto settimanale del segmento lab interno
ndiff /scans/baseline-192.0.2.0-24.xml /scans/weekly-192.0.2.0-24.xml
# Produzione: Scansione a intensità ridotta, stesso flusso di diff
nmap -sS -p- --max-rate 100 --max-retries 2 -oX /scans/prod-weekly.xml 192.0.2.0/24
ndiff /scans/baseline-prod.xml /scans/prod-weekly.xml
Cosa fa: Confronta due file XML di Nmap e restituisce solo i cambiamenti significativi in formato human-readable. Quando usarlo: Revisioni operative settimanali, validazione del change-control, o dopo finestre di manutenzione per rilevare esposizioni non intenzionali. Rischi:
ndiffnon allerta su host mancanti che non hanno risposto; gli host down spariscono silenziosamente da entrambi i report. Output atteso: Righe prefissate con+e-che mostrano servizi acquisiti o persi;=per elementi invariati.
Output realistico di ndiff:
- Nmap 7.94 scan initiated Mon Jan 15 06:00:00 2024 as: nmap -sS -sV -p- -oX baseline.xml 192.0.2.0/24
+ Nmap 7.94 scan initiated Mon Jan 22 06:00:00 2024 as: nmap -sS -sV -p- -oX weekly.xml 192.0.2.0/24
192.0.2.10:
+ 8080/tcp open http Apache Tomcat 9.0.82
- 3306/tcp open mysql MySQL 8.0.34
192.0.2.55:
+ Host is up.
+ 22/tcp open ssh OpenSSH 9.3p1
La riga + 8080/tcp è il tuo segnale: o è avvenuto un deployment senza ticket di change, o è in esecuzione un servizio non autorizzato. La riga - 3306/tcp è altrettanto importante—la scomparsa di un servizio può indicare una risposta a un compromesso (attaccante che copre le tracce) o una misconfigurazione che ha rotto una dipendenza.
Integrazione con Database e Trending
L'archiviazione delle scansioni in PostgreSQL abilita l'analisi longitudinale: "Mostrami tutti gli host dove la porta 3389/RDP è apparsa negli ultimi 90 giorni" o "Conta le istanze SMB esposte per subnet nel tempo." Schema minimale:
CREATE TABLE scans (
scan_id SERIAL PRIMARY KEY,
started_at TIMESTAMPTZ NOT NULL,
target_cidr CIDR,
nmap_version TEXT,
xml_checksum BYTEA UNIQUE
);
CREATE TABLE hosts (
host_id BIGSERIAL PRIMARY KEY,
scan_id INT REFERENCES scans(scan_id),
ip INET NOT NULL,
mac TEXT,
hostname TEXT,
os_guess TEXT,
state TEXT CHECK (state IN ('up', 'down', 'unknown'))
);
CREATE TABLE ports (
port_id BIGSERIAL PRIMARY KEY,
host_id BIGINT REFERENCES hosts(host_id),
port INT,
protocol TEXT,
state TEXT,
service_name TEXT,
product TEXT,
version TEXT,
extrainfo TEXT
);
CREATE INDEX ON ports(port, protocol, state) WHERE state = 'open';
CREATE INDEX ON hosts(ip, scan_id);
Importa tramite COPY da un CSV prodotto dal tuo parser Python, o usa l'estensione xml2 di PostgreSQL per lo shredding diretto dell'XML. Partiziona scans per started_at mensilmente; gli archivi di scansione crescono rapidamente.
Architettura di Scansione Continua
┌─────────────┐ ┌─────────────┐ ┌─────────────────┐
│ Scheduler │────▶│ Scan Jobs │────▶│ Nmap Workers │
│ (cron/ │ │ (temporal/ │ │ (dedicated │
│ Airflow) │ │ ephemeral) │ │ network seg) │
└─────────────┘ └─────────────┘ └─────────────────┘
│
▼
┌─────────────┐
│ XML Output │
│ (S3/nfs) │
└─────────────┘
│
┌──────────────────────────┼──────────────────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────────┐ ┌─────────────┐
│ Parser │ │ Ndiff │ │ Zenmap/ │
│ (Python)│ │ Engine │ │ Faraday │
└────┬────┘ └──────┬──────┘ └─────────────┘
│ │
▼ ▼
┌─────────┐ ┌─────────────┐
│PostgreSQL│ │ Alerting │
│(trends) │ │ (PagerDuty │
└─────────┘ │ /Slack/API) │
└─────────────┘
Decisioni operative chiave in questa pipeline:
| Decisione | Implicazione pratica |
|---|---|
| Frequenza di scansione dal CI | Scansioni baseline ad ogni deployment; sweep completi settimanali. Troppo frequenti e si desensibilizzano i responder; troppo radi e si accumula drift. |
| Segmentazione dei worker | Esegui Nmap da una NIC/VLAN dedicata con regole firewall esplicite. Un worker compromesso è una miniera d'oro per un attaccante. |
| Ritenzione XML | L'XML raw è 10-50× le righe compresse del DB. Mantieni 90 giorni hot, archivio glacier per il periodo di compliance. |
| Deduplicazione per checksum | XML identici (nessun cambiamento di rete) saltano il parsing; risparmia I/O, rivela infrastruttura stagnante. |
Integrazione con Pipeline CI/CD
Incorpora la scansione baseline nelle pipeline di deployment per intercettare il drift dell'infrastructure-as-code prima che raggiunga la produzione. Esempio GitLab CI:
network-baseline:
image: nmap:latest # pin digest, not tag
variables:
TARGET: "198.51.100.0/24"
RATE: "500" # Production: reduce to 100 or less
script:
- nmap -sS -sV -p- --max-rate $RATE -oX scan-$(date +%s).xml $TARGET
- python3 /scripts/parse_and_alert.py scan-*.xml --baseline /baselines/prod.xml
artifacts:
paths: ["scan-*.xml"]
expire_in: 30 days
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
Cosa fa: La pipeline schedulata esegue Nmap, analizza i risultati, e fallisce se appaiono nuove porte rispetto alla baseline. Quando usarlo: Ad ogni deployment su infrastruttura network-adjacent, o notturnamente per ambienti statici. Rischi: Fallimenti della pipeline da jitter di rete causano alert fatigue; implementa retry con backoff e alerting basato su soglia (es. 3 delta consecutivi). Output atteso: Log del job CI con le nuove porte analizzate, o pass verde se la baseline corrisponde.
Il parser dovrebbe emettere exit code 2 su drift, exit code 0 su match, e exit code 1 su fallimento della scansione—convenzioni standard Nagios che la maggior parte dei sistemi CI e degli hook di monitoring comprendono nativamente.
Visualizzazione e Integrazione con Strumenti di Terze Parti
La vista topologica di Zenmap è adeguata per la comprensione di singole reti ma non scala oltre poche centinaia di host. Per dashboard operative, esporta verso:
| Strumento | Ruolo | Percorso di integrazione |
|---|---|---|
| Faraday | Workspace collaborativo per pentest | Upload XML via API; correlazione con finding di exploit |
| Dradis | Generazione report | Importa XML come evidenza; template per executive summary |
| Grafana | Dashboard time-series | Backend PostgreSQL con query di esposizione porte |
| nmap-vulners | Contesto vulnerabilità | --script nmap-vulners arricchisce i dati porta con riferimenti CVE al momento della scansione |
# Lab: Arricchisci la scansione con lo script vulners per contesto di triage immediato
nmap -sV --script nmap-vulners -p 22,80,443 -oX vuln-enriched.xml 192.0.2.0/24
Cosa fa: Interroga l'API Vulners per CVE associati alle versioni di servizio rilevate. Quando usarlo: Prioritizzazione di servizi esposti; mai come unica valutazione di vulnerabilità. Rischi: Il rilevamento versione è probabilistico; falsi positivi su patch backportate comuni in Linux enterprise. Output atteso: Lista CVE appesa all'output script di ciascuna porta nell'XML.
RustScan come Livello di Accelerazione
Per il discovery iniziale su grandi patrimoni, la velocità ispirata a masscan di RustScan con fallback a Nmap migliora il throughput della pipeline. Non è un sostituto—il rilevamento servizi e gli script richiedono Nmap propriamente detto—ma collassa la fase di discovery da ore a minuti.
# Lab: RustScan per discovery porte, Nmap per ispezione approfondita
rustscan -a 192.0.2.0/24 --range 1-65535 --scan-order random \
-- -sV -sC -oX deep.xml
Il -- passa gli argomenti rimanenti a Nmap. In produzione richiede rate limiting: i default di RustScan sono aggressivi e sovraccaricheranno firewall stateful o triggereranno soglie IDS.
Ritenzione Dati per Archivi Sensibili di Topologia
Gli archivi di scansione contengono una mappa di rete completa—indirizzamento IP, host attivi, versioni servizi, guess SO. Trattali come confidenziali al livello di sensibilità della tua documentazione di rete, non semplicemente come dati di log.
Elementi pratici di policy:
- Ritenzione: 90 giorni online in PostgreSQL, 1-3 anni XML compresso in object storage, poi distruzione crittografica. Il legal hold sospende la cancellazione.
- Accesso: Solo service account per il parser; l'accesso umano richiede break-glass con riferimento ticket loggato.
- Crittografia: XML a riposo (AES-256-GCM via S3 o crittografia filesystem); TLS 1.3 per il trasporto parser-to-database.
- Geografia: Archivia nella stessa giurisdizione della rete; il trasferimento transfrontaliero di mappe infrastrutturali può violare la data residency o attivare review di export control.
Intuizione conquistata sul campo: Una porta marcata
filterednon è un risultato pulito—è una domanda senza risposta. Molti operatori archiviano solo le porteopene perdono il segnale rilevante per la sicurezza dei cambiamenti nelle regole firewall. Archivia gli statifilteredeclosed; il delta dafilteredaopensenza ticket di change è spesso il tuo primo indicatore di intrusione.