dmap (delta map) is a command-line tool for running nmap against one host and comparing historical scan data to detect anomalies. Use it to see what changed since the last comparable scan, or since a baseline scan of your choice. With enough prior runs it can also call out when the current result looks unlike your recent pattern for that host. Use only on hosts and networks you own or are explicitly authorized to test.
Source: github.com/dsdugal/dmap
The package is not published on PyPI yet. See Installation for how to get started.
- Single-target runs — One host per run; extra nmap arguments via
-e. - History and diff — SQLite snapshots under a configurable data directory; each run has a stable
scan_id(UUID) and profile. Compare to the previous run for the same target/profile or pin--baseline-scan-id. See Evidence bundle JSON. - IsolationForest — With enough prior scans, attaches
ml_score/ml_is_outlier; training window and minimum history via Environment. - Scripting — Versioned stdout JSON (
schema_version1.0);-qhides it without changing exits. Codes0/1/2—Exit codes.
- Python 3.11+ (see
requires-pythoninpyproject.toml) nmaponPATH(override withNMAP_BINARYor otherDMAP_settings)
git clone https://github.com/dsdugal/dmap.git
cd dmap
python3 -m venv .venv && source .venv/bin/activate # Windows: .\.venv\Scripts\activate
pip install .That installs the dmap CLI and runtime dependencies only.
Optional — isolated CLI without managing a venv: pipx can install from the default branch of the repository:
pipx install "git+https://github.com/dsdugal/dmap.git"Pin a tag or commit in the URL (e.g. @v0.1.0) when you need a reproducible install.
dmap run 203.0.113.10 -e "-p 22,80 -sV"
echo $? # see Exit codes| Variable | Description |
|---|---|
DMAP_DATA_DIR |
Directory containing snapshots.sqlite3 (default: ~/.local/share/dmap) |
NMAP_BINARY |
Path to the nmap executable |
DMAP_HISTORY_K |
Max prior snapshots used to fit the isolation forest (default: 32) |
DMAP_MIN_TRAIN |
Minimum history rows before ml_score is computed (default: 5) |
DMAP_NMAP_TIMEOUT_SEC |
Subprocess timeout for nmap in seconds (default: 600) |
--baseline-scan-id UUID— diff against that stored scan instead of the latest snapshot--fail-on-diff-rule— exit2if a new open TCP port appears (useful when ML is cold)-q/--quiet— suppress JSON on stdout (exit code unchanged)
Exit codes separate a clean outcome, failures, and cases that should be treated as a meaningful change. The dmap run process uses the following exit codes:
| Code | Meaning |
|---|---|
0 |
Clean — no material diff and isolation forest did not flag an outlier (and --fail-on-diff-rule did not force material). |
1 |
Error — invalid target, baseline not found, nmap failure, I/O error, or other runtime failure (message on stderr). |
2 |
Material — at least one deterministic diff row, or ml_is_outlier is true, or --fail-on-diff-rule fired on a new open TCP port. |
Each successful dmap run prints one JSON object (unless -q). Fields are defined in src/dmap/schema.py.
| Field | Type | Description |
|---|---|---|
schema_version |
string | Always "1.0" for this contract. |
scan_id |
string | UUID for this scan row (also primary key in SQLite). |
target_key |
string | Validated single target (hostname or IP). |
profile_id |
string | Stable hash of profile_extra_args (scan “profile”). |
profile_extra_args |
string | Extra nmap flags passed with -e (normalized trim). |
wall_time_utc |
string | ISO 8601 UTC timestamp when the bundle was built. |
nmap_version |
string or null |
From nmap XML when present. |
ml_score |
number or null |
Anomaly score: higher = more anomalous; null when history is shorter than DMAP_MIN_TRAIN or empty. |
ml_is_outlier |
boolean or null |
Isolation forest outlier prediction; null when ml_score is null. |
model_id |
string or null |
Hash id of training row ids + hyperparameters; null when no model was fit. |
n_train_samples |
integer | Number of prior snapshots used as training rows for the forest. |
severity |
string | "clean" or "material" (aligned with exit semantics). The schema allows "informational" for future use; current runs emit only clean or material. |
exit_code |
integer | Same convention as Exit codes above (0 or 2 on success path). |
deterministic_diff |
array | List of diff objects (see below). |
diff_summary |
object | Counts per diff kind (e.g. {"new": 1, "gone": 0}). |
new_tcp_open |
boolean | true if any diff row is a new TCP open port. |
fail_on_diff_rule_triggered |
boolean | true if --fail-on-diff-rule was set and a new TCP open port was present. |
Each element describes one change vs the baseline (previous scan or pinned scan_id):
| Field | Type | Description |
|---|---|---|
kind |
string | "new" (new open port), "gone" (was open, no longer open), or "service_drift" (same open port, service identity changed). |
proto |
string | e.g. "tcp". |
port |
integer | Port number. |
before |
object or null |
Port row before change (null for new). |
after |
object or null |
Port row after change (null for gone). |
detail |
string | Short human-readable reason. |
When before / after are objects, they use the same keys as stored port rows: proto, port, state, service_name, product, version, extrainfo (with redaction applied to service fields before storage).
{
"schema_version": "1.0",
"scan_id": "…",
"target_key": "203.0.113.10",
"profile_id": "…",
"profile_extra_args": "-p 22,80 -sV",
"wall_time_utc": "2026-04-28T12:00:00+00:00",
"nmap_version": "7.94",
"ml_score": null,
"ml_is_outlier": null,
"model_id": null,
"n_train_samples": 0,
"severity": "clean",
"exit_code": 0,
"deterministic_diff": [],
"diff_summary": {},
"new_tcp_open": false,
"fail_on_diff_rule_triggered": false
}See CHANGELOG.md.
This project is distributed under the MIT License.