Skip to main content

NoiseHound

NoiseHound

PyPI Release License Python 3.10+ CI Security policy X (Twitter): @warped_atom

Detection-aware Active Directory attack-path scoring. DreadHost Research | companion to OffsetInspect (PowerShell) and OffsetScan (Rust)

BloodHound (and PlumHound on top of it) finds a path to the objective. NoiseHound ingests the same graph data and re-ranks paths by expected detection cost instead of hop count, so an operator can ask "what is the quietest way to Domain Admin" instead of just "what is a way".

Project status (v1.0): stable and tested on real BloodHound data across multiple domains. 30 of 57 corpus edges are now lab-measured across four detection tiers (audit, Defender for Endpoint, Elastic SIEM, and MDI posture) - shipped as drop-in profiles in profiles/, with closed-loop proof that they change path rankings (docs/VALIDATION.md). The remaining ~28 edges still carry expert estimates; the calibration harness (noisehound-calibrate, docs/CALIBRATION.md) is how they, and your own environment, get measured. Treat uncalibrated rankings as well-reasoned guidance, not ground truth.

For authorized engagements only. This tool scores attack paths for OPSEC planning against systems you have written permission to test.

NoiseHound is an independent community project. It is not affiliated with, endorsed by, or associated with SpecterOps or the BloodHound project; it consumes BloodHound's open data format.


How it works

  1. Ingest a BloodHound CE export (.zip), a raw JSON file, or a directory of exports into an internal graph. A normalised {nodes, edges} JSON format is also accepted for offline analysis and tests. AD CS ESC1-8 escalation edges are synthesised at load time from the certificate-template and CA facts BloodHound collects (see below).
  2. Annotate every edge from the edge-telemetry corpus, attaching an effective_noise_score (0-100). Where several rights connect the same pair of nodes, the quietest is chosen. Edge types absent from the corpus default to a conservative score (60) so gaps fail safe rather than under-reporting. An optional environment profile adjusts scores for the target's declared detection posture (see below).
  3. Solve for the quietest paths. Because the path score is a bottleneck plus mean (not a simple sum), it cannot be optimised directly by Dijkstra. The solver combines a threshold sweep (for each distinct noise level, the quietest route that stays under it) with a bounded k-shortest-by-weight pass, then re-ranks the union by the real path score. The threshold sweep is the correctness backstop: it surfaces a long-but-uniformly-quiet path that a pure summed-weight search would rank below a short-but-loud one.
  4. Report as text, JSON (interoperable with the OffsetInspect result schema), or a self-contained HTML report styled to match the toolset.

Path scoring

Path noise is deliberately not a simple sum. Tripping the same detection twice is not twice as loud (SOC triage, not raw event count). NoiseHound uses:

path_score = max(edge_scores) * 0.6 + mean(edge_scores) * 0.4

This weights toward the loudest single step (one bad step often burns the whole op) while still accounting for cumulative exposure. The weights are configurable (--max-weight / --mean-weight) so they can be tuned empirically once real detection data is available from an APT29/Caldera lab.

Every path also reports a detection probability - the chance it trips a correlated alert - blending the loudest edge with the cumulative noisy-OR of all edges (tuned by --correlation). It answers a different question than the noise score: a short but loud path can have a lower overall probability of being caught than a long but quiet one. Rank by it with --rank-by probability.

Two-tier engine (DeadAir)

For large graphs the solve is dispatched to DeadAir, a companion Rust engine (the OffsetScan-to-OffsetInspect tier). NoiseHound stays the feature-rich frontend - ingestion, corpus, environment/Sigma, constraints, reporting - and hands the prepared graph to whichever engine solves it, so results are identical either way.

  • --engine auto (default): DeadAir when its binary is found and the graph is large (>= 5000 nodes); the built-in Python solver otherwise.
  • --engine python: force the built-in solver (no binary needed).
  • --engine rust: force DeadAir (errors if the binary is missing).

DeadAir is found via $NOISEHOUND_DEADAIR, then PATH, then the sibling ../deadair/target/{release,debug}/ build. It is 10-100x faster on large graphs (a 250k-node graph solves in ~2s vs ~30s in Python) while producing byte-identical rankings. The output records which engine ran.

Multi-objective and constrained pathing

Noise, hop count, and detection probability pull in different directions, so --pareto returns the Pareto frontier - every path that no other beats on all three at once - instead of forcing a single winner. And real operations have constraints: --avoid NODE keeps a path off a specific host (an EDR-monitored jump box, a honeypot), and --avoid-edge TYPE refuses a technique (e.g. --avoid-edge DCSync). Both are repeatable and re-solve on the fly.

python -m noisehound -i export.zip -s jdoe -o "Domain Admins" --pareto
python -m noisehound -i export.zip -s jdoe -o "Domain Admins" --avoid FILESERVER01 --avoid-edge HasSession

How NoiseHound compares

Weighted BloodHound pathfinding is not new, so here is the honest positioning:

  • BloodHound / BloodHound CE find a path by unweighted hop count. No noise model.
  • GoodHound assigns edge costs and finds cheapest paths, but its cost model is exploitation difficulty and business risk, not detection noise.
  • PlumHound / ImproHound do reporting and tier-violation analysis; neither re-solves for a quietest-to-objective path.
  • Detection mappings (Sigma, DeTT&CT, event-ID-to-ATT&CK references) are rich but human-oriented and not keyed to BloodHound edge kinds.

NoiseHound's contribution is the combination: a machine-readable BloodHound-edge-to-detection-telemetry corpus, a noise-weighted re-solve framed as operator OPSEC ("quietest way to DA"), an environment model that adapts to a target's declared posture, and a calibration loop that turns lab detections into measured scores. The graph math is commodity; the corpus and the framing are the point. Its value is only as good as the corpus, which is why calibration and community contribution are first-class - see below.

Install

cd NoiseHound
python -m pip install -r requirements.txt   # networkx>=3.0
# optional editable install to get the `noisehound` command on PATH:
python -m pip install -e .

Requires Python 3.10+.

Usage

# Text summary (default)
python -m noisehound --input export.zip --objective "Domain Admins" --source jdoe

# JSON, for downstream tooling / correlation across the DreadHost suite
python -m noisehound -i export.zip -o "Domain Admins" -s jdoe -f json --out paths.json

# Self-contained HTML report
python -m noisehound -i export.zip -o "Domain Admins" -s jdoe -f html --out report.html

First, sanity-check the parser on your export (histograms + corpus coverage, no pathing) - the fastest way to validate NoiseHound on real-world data:

noisehound-inspect -i export.zip

Live BloodHound CE / Neo4j

Instead of a zip, point --input at the Neo4j database BloodHound CE populates and NoiseHound reads the (already analysed) graph directly over Bolt:

pip install 'noisehound[neo4j]'
export NEO4J_PASSWORD=bloodhoundcommunityedition   # match your BHCE compose
noisehound-inspect -i bolt://localhost:7687
python -m noisehound -i bolt://localhost:7687 -s jdoe -o "Domain Admins"

Stand up BloodHound CE (bundles Neo4j on 7687) with its official compose: curl -L https://ghst.ly/getbhce | docker compose -f - up.

Try it against the bundled samples:

python -m noisehound -i samples/sample_graph.json -s jdoe -o "Domain Admins" -d CONTOSO.LOCAL
python -m noisehound -i samples/sample_bloodhound_ce.zip -s jdoe -o "Domain Admins"
python -m noisehound -i samples/sample_adcs_ce.zip -s jdoe -o "Domain Admins"   # ADCS ESC1
# Full-spectrum export exercising every edge family (sessions, delegation, DCSync, ADCS, trusts):
python -m noisehound -i samples/sample_fullspectrum_ce.zip -s ALICE -o "Domain Admins" -d CONTOSO.LOCAL -k 3

That last one is the clearest demo of the thesis: the quietest route to Domain Admins is the 4-hop session path, ranked above the 3-hop RDP path and the 1-hop ADCS ESC1 - most hops, least noise.

The sample demonstrates the core value: the quietest route is a 4-hop session path (score 19.9), ranked above a 2-hop ForceChangePassword shortcut (36.4). Fewer hops does not mean quieter.

Blue-team detection-gap mode

The quietest path is where detection is weakest, so add --defensive to flip the output for defenders: it flags the edges that are quiet only because their telemetry is off or absent, maps each to the control that would catch it, and ranks those controls by how much they raise the quietest-path score.

python -m noisehound -i export.zip -s jdoe -o "Domain Admins" --defensive

On the full-spectrum sample it finds that the quietest path to Domain Admins hinges on undetected LSASS access (HasSession, 20 -> 65 if instrumented) and recommends deploying Sysmon Event 10 - closing that one gap lifts the quietest path from 19.9 to 48.4. See docs/ROADMAP.md for where this and the rest of the model are headed.

Key options

Option Meaning
--input, -i BloodHound .zip, .json, or directory of exports
--source, -s Starting principal (jdoe or an object id)
--objective, -o Target node (Domain Admins or an object id)
--paths, -k Number of quietest paths to return (default 5)
--format, -f text (default), json, or html
--defensive Blue-team view: detection gaps on the quietest paths + fixes
--rank-by noise (default) or probability (P of a correlated alert)
--correlation SOC correlation coefficient for P(detected), 0..1 (default 0.5)
--pareto Return the Pareto frontier over noise/hops/P(detect)
--engine auto (default), python, or rust (the DeadAir engine)
--avoid NODE Exclude a node from all paths (repeatable)
--avoid-edge TYPE Exclude an edge type from all paths (repeatable)
--corpus Override the edge-mapping corpus directory
--environment, -e Operator-declared target posture JSON (adjusts scores)
--max-weight / --mean-weight Scoring weights (must sum to 1.0)
--default-noise Score for edge types absent from the corpus (default 60)

Environment profiles

A static corpus score cannot know whether a given target has 4662 object auditing on, ships Sysmon, or runs an ITDR like MDI - yet those move an edge's real noise enormously (DCSync is near-silent without 4662 auditing and near-certain-detection with it). Instead of pretending one number fits every environment, declare the target's posture in a small JSON file and NoiseHound adjusts scores transparently against the corpus's own telemetry annotations:

{
  "name": "CONTOSO.LOCAL-prod",
  "object_auditing_4662": true,
  "ds_change_auditing_5136": false,
  "edr": "MDI",
  "sysmon": true,
  "powershell_logging_4104": true,
  "adjustments": { "HasSession": 65 }
}

Adjustments only ever raise a score toward a detection floor implied by the declared posture. adjustments are hard per-edge overrides - the place to record values you have calibrated against your own lab. This is operator-supplied, not measured; it does not replace Phase 2 live validation, but it turns the static corpus from "one number for all environments" into "the number for the environment you are actually in". On the bundled sample, declaring the profile above flips the quietest route from the LSASS-dump session path to a directory-write path - which is the correct call once host telemetry is live.

python -m noisehound -i samples/sample_graph.json -s jdoe -o "Domain Admins" \
    -e samples/env_profile.example.json

Score precedence: static -> environment-adjusted -> live (Phase 2).


Calibration harness

Environment profiles are only as good as the numbers you put in them. noisehound-calibrate closes the loop: run the techniques in a detection lab, record what fired, and it emits a calibrated environment profile.

This has been done. profiles/ ships three measured profiles from a real Hyper-V Vulnerable-AD range - audit, EDR (Defender for Endpoint), and Elastic SIEM tiers, 30 edges - produced by the automated harness (lab/) and this tool. Use them directly, or measure your own:

# Use a shipped measured profile:
noisehound -i export.zip -s jdoe -o "Domain Admins" -e profiles/vulnad-hyperv-audit.json

# Or measure your own lab:
# 1. noisehound-calibrate --plan -o plan.json   (per-edge detection events)
# 2. lab/Invoke-NoiseHoundCalibration.ps1        (run + auto-count -> lab_detections.json)
noisehound-calibrate -i lab_detections.json -o env.calibrated.json
noisehound -i export.zip -s jdoe -o "Domain Admins" -e env.calibrated.json

The score model is a shrinkage estimator, honest about sample size:

p          = detections / runs                       (detection probability)
lab_score  = p * severity_loudness + (1 - p) * residual
w          = runs / (runs + smoothing)               (confidence in the lab)
calibrated = w * lab_score + (1 - w) * corpus_static

lab_score is the expected detection cost - the SOC severity when it fires, a small residual when it does not. The weight w keeps a single run from overriding the corpus while letting a well-sampled result dominate. On the example, HasSession climbs from a static 20 to 52 (the lab caught the LSASS dump 4 of 5 runs) while Kerberoast drops from 60 to 34 (it never fired). Use --merge existing.json to overlay new calibration onto a profile while keeping its posture flags, and --smoothing / --residual to tune the model.


Score against deployed detections (Sigma)

Environment profiles and calibration are self-declared. noisehound-sigma scores against the detections a defender has actually written: point it at a Sigma rule set and it works out which corpus edges each rule would fire on (matching the edge's telemetry event IDs and ATT&CK technique), then emits an environment profile that raises the covered edges - a drop-in for --environment.

noisehound-sigma -r ./sigma-rules/ -o env.sigma.json
python -m noisehound -i export.zip -s jdoe -o "Domain Admins" -e env.sigma.json --defensive

Matching is deliberately conservative so it never hides a gap: a rule only counts if it references an event ID the edge generates and, when the rule is ATT&CK-tagged, its technique agrees - so a DS-Access (4662) DCSync rule is not miscredited with covering LAPS reads that merely share the event ID. The report lists both what your rules cover and, more usefully, the attack edges no rule covers. Combined with --defensive, this answers "given the detections I have deployed, where is my quietest attack path still invisible?"


AD CS ESC1-8

At load time NoiseHound performs a focused version of BloodHound's ADCS post-processing, synthesising escalation edges from the retained template/CA facts. An Enroll right on a vulnerable template becomes a direct ADCSESCn edge from the principal to the domain's Domain Admins group (RID 512), so certificate escalation is pathable and scored like any other edge:

Edge Condition
ADCSESC1 Enrollee-supplies-subject + client-auth EKU, no approval/RA sigs
ADCSESC2 Any-Purpose / SubCA EKU, no approval
ADCSESC3 Enrollment-agent template + an auth template on the same CA
ADCSESC4 Dangerous write control over a published template
ADCSESC5 Control of the CA object or its hosting computer
ADCSESC6 CA has EDITF_ATTRIBUTESUBJECTALTNAME2 set
ADCSESC7 ManageCA / ManageCertificates on the CA
ADCSESC8 Vulnerable HTTP web-enrollment endpoint (coerce + NTLM relay)

Try it: python -m noisehound -i samples/sample_adcs_ce.zip -s jdoe -o "Domain Admins".

Documented simplifications (they fail safe toward showing more paths): CA-level enrollment restrictions are not modelled (template enrollment is treated as sufficient); ESC5 covers the CA object and its host, not every PKI container; ESC9/10/13 are out of scope. Synthetic ESC edges already present in a post-processed export are preserved.


The corpus (edge_mappings/)

The edge-telemetry corpus is the actual IP of this tool; the code is comparatively simple graph math on top of it. Each edge_mappings/<Edge>.json maps a BloodHound edge type to its detection surface:

  • expected telemetry sources (Windows Security Event IDs, Sysmon Event IDs, network, EDR/ITDR heuristics) with per-source reliability and whether the relevant auditing is on by default
  • a static noise score (0-100)
  • MITRE technique, prerequisite privilege, the usual abuse primitive, and notes
{
  "edge_type": "DCSync",
  "static_noise_score": 85,
  "telemetry": [
    {"source": "windows_security", "event_id": 4662,
     "detail": "Directory Service Access - requires object auditing (default OFF)",
     "reliability": "high_if_auditing_enabled", "default_enabled": false},
    {"source": "edr_heuristic", "product_class": "MDI",
     "detail": "Non-DC hosts issuing DRSGetNCChanges - high fidelity",
     "reliability": "high"}
  ],
  "mitre_technique": "T1003.006",
  "notes": "Assumes default audit policy (mostly OFF) but high EDR/MDI coverage."
}

Extending the corpus is where most ongoing effort should go. Add a new JSON file, keep the schema (validated at load time), and it is picked up automatically. v0.3 ships 43 edge types covering the ACL-abuse, Kerberos, delegation, ADCS ESC1-8, LAPS/gMSA, GPO, trust, and access-right edges that matter for path-finding.

Scores are seeded from the DreadHost Red Team Operator Playbook noise matrix (Windows/Sysmon event mappings, technique noise ratings) and standard AD detection facts. Tune them against your own lab detection data.


Roadmap

  • Calibration - done for 30/57 edges, continuing. Three measured profiles ship in profiles/ (audit / EDR / Elastic tiers) with closed-loop validation. Remaining: the other ~28 edges (coercion/relay, ADCS ESC2-13, CanRDP), the MDI runtime-alert tier (posture works; the alert path needs a bare-metal/ Ludus DC - see docs/CALIBRATION.md), and a selectable tooling-profile axis (docs/TOOLING_AXIS.md) so scores reflect off-the-shelf vs native tradecraft.
  • Phase 2 - live detection validation. Replace static scores with live_noise_score pulled from a real target's Defender/Sysmon/audit config, reusing OffsetInspect's detection-boundary logic. annotate() already accepts a live_scores override; the CLI hook lands in Phase 2.
  • Live Neo4j ingestion over Bolt against the same DB SharpHound populates (offline zip ingestion ships now).
  • ADCS ESC9/10/13 synthesis (ESC1-8 ship now).
  • Rust port of the scoring engine (petgraph), mirroring the OffsetInspect -> OffsetScan pattern, once the data model is proven.

Contributing

The corpus is community-extensible and that is where contributions matter most. Adding an edge is one JSON file validated on load and in CI:

noisehound-validate              # schema + consistency checks over the corpus
python -m pytest tests/ -q

See CONTRIBUTING.md for the schema, the scoring guide, and PR guidance, and docs/edge_schema.json for the formal edge schema.

Calibrating against a lab

30 edges are measured (see profiles/); the rest are estimates until you measure them, and every environment differs. docs/CALIBRATION.md is a full playbook: lab topology, the exact Windows audit policy and Sysmon config to make the corpus's event IDs fire, a per-edge exercise runbook, an APT29-via-Caldera realism layer, and how to compile results into a calibrated profile. Start with noisehound-calibrate --template -o lab_detections.json.

The lab/ kit automates the detection instrumentation: Enable-Telemetry.ps1 turns on the audit policy, script-block logging, DCSync SACL, and Sysmon; Collect-Detections.ps1 tallies what fired in a window. It leans on GOAD or Vulnerable-AD (or your CRTP/CRTO lab) for the vulnerable domain itself rather than reimplementing them.

Responsible use

NoiseHound is for authorized security testing, purple-team exercises, detection engineering, and research. It reads BloodHound data you already collected and computes rankings; it executes nothing against a target. Use it only where you have explicit written authorization. Contributions must not include target-specific or real-engagement data.

Testing

python -m pytest tests/          # with pytest
python tests/test_noisehound.py  # dependency-light smoke run

Layout

noisehound/        engine: schema, corpus, ingest, adcs, annotate, environment,
                   solver, report, cli, calibrate
edge_mappings/     the telemetry corpus (one JSON per edge type) - the IP
samples/           sample_graph.json, sample_bloodhound_ce.zip, sample_adcs_ce.zip,
                   sample_fullspectrum_ce.zip, env_profile.example.json,
                   lab_detections.example.json, sample_report.html
tests/             unit + end-to-end tests

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

noisehound-1.0.0.tar.gz (87.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

noisehound-1.0.0-py3-none-any.whl (103.7 kB view details)

Uploaded Python 3

File details

Details for the file noisehound-1.0.0.tar.gz.

File metadata

  • Download URL: noisehound-1.0.0.tar.gz
  • Upload date:
  • Size: 87.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for noisehound-1.0.0.tar.gz
Algorithm Hash digest
SHA256 c8444445ef922e27c9cbdd3e1d49cc945c98e6dd50c7a56a615db2fff4d5ee55
MD5 fdd3ed123b95c5ffddc73221e56aa6c5
BLAKE2b-256 d233da8652a2a4e76bb6ba26c1fb123dbfde40ccf3718149d42c9c05d2a01699

See more details on using hashes here.

Provenance

The following attestation bundles were made for noisehound-1.0.0.tar.gz:

Publisher: publish.yml on warpedatom/NoiseHound

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file noisehound-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: noisehound-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 103.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for noisehound-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0b68cd593ac3e4a5a0396ac3fa96f3e381422cb15dfdc7fc4ca204a526a6760f
MD5 a82edfbddf859422caf9f115f8b5d6ca
BLAKE2b-256 a2f090f1bfe95ca2f084af89563fe51b872c80e9427e8f08e4ea559668a91de1

See more details on using hashes here.

Provenance

The following attestation bundles were made for noisehound-1.0.0-py3-none-any.whl:

Publisher: publish.yml on warpedatom/NoiseHound

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page