HostHound is a defensive local privilege-escalation graph explorer inspired by BloodHound-style path analysis. It ingests local host facts, derives relationships, and lets analysts query paths from a real user context to privileged targets such as NT AUTHORITY\\SYSTEM or local Administrators.
This MVP now has a module-first design for community contributions, similar in spirit to WINPEAS checks:
- collector output is normalized graph JSON;
- the graph server is completely Python: Flask + NetworkX + Python analyzer modules;
- the browser graph visualization uses D3.js force-directed rendering;
- built-in and community analyzer modules use the same contract;
- community checks can be dropped into a plugin directory and loaded with
--plugin-dir.
hosthound/
app/
server.py # Python Flask server
requirements.txt
hosthound/
context.py # ctx API passed to modules
plugins.py # Python module loader
modules/ # built-in Python analyzer modules
always_install_elevated.py
dangerous_token_privileges.py
inherited_group_edges.py
writable_privileged_execution.py
static/index.html # D3.js browser UI served by Flask
collector/
Invoke-HostHoundCollector.ps1 # Existing Windows collector
modules/ # PowerShell collector modules
docs/
MODULE_DEVELOPMENT.md # Legacy PowerShell collector module notes
PYTHON_MODULE_DEVELOPMENT.md # Python server/analyzer module guide
templates/
example_python_analyzer_module.py
samples/
sample-windows.json
cd hosthound/app
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python server.py --input ../samples/sample-windows.jsonOpen:
http://127.0.0.1:8000
The server exposes:
GET /api/health
GET /api/graph
GET /api/findings
GET /api/modules
GET /api/search?q=alice
GET /api/paths?source=<node-id>&target=<optional-node-id>
POST /api/reload
Create a directory outside the app, copy in a Python analyzer module, and run:
python app/server.py \
--input samples/sample-windows.json \
--plugin-dir community-modulesA minimal module looks like this:
MODULE = {
"name": "community.example_check",
"kind": "analyzer",
"version": "0.1.0",
"description": "Example community check.",
}
def analyze(ctx):
for source, target, data in list(ctx.graph.edges(data=True)):
if data.get("type") == "CAN_WRITE":
ctx.finding(
severity="medium",
title="Writable object observed",
description="A collected principal can write to an object.",
evidence=f"{source} can write {target}.",
remediation="Review and remove unnecessary write permissions.",
related=[source, target],
)See docs/PYTHON_MODULE_DEVELOPMENT.md and templates/example_python_analyzer_module.py.
builtin.writable_privileged_execution
Derives CAN_ESCALATE_TO when a writable object is executed/loaded by a privileged runtime.
builtin.inherited_group_edges
Propagates group capabilities to group members for accurate user-to-admin path queries.
builtin.dangerous_token_privileges
Flags SeImpersonatePrivilege, SeDebugPrivilege, SeBackupPrivilege, and similar Windows token rights.
builtin.always_install_elevated
Flags AlwaysInstallElevated policy relationships emitted by collectors.
Set-ExecutionPolicy -Scope Process Bypass -Force
.\collector\Invoke-HostHoundCollector.ps1 -OutFile .\hosthound-host.json -PrettyThen ingest the result:
python app/server.py --input hosthound-host.jsonHostHound is intentionally an audit and explanation tool. Modules should gather or analyze facts, emit graph relationships and findings, and provide remediation. They should not exploit paths, deploy payloads, or modify host state.
The Flask server reads collection files with utf-8-sig, so JSON exported from Windows PowerShell with a UTF-8 BOM and JSON exported without a BOM both import correctly. The PowerShell collector now writes UTF-8 without BOM when possible.
HostHound can export its native collection JSON into a BloodHound OpenGraph payload. BloodHound OpenGraph payloads use this top-level shape:
{
"graph": {
"nodes": [],
"edges": []
},
"metadata": {
"source_kind": "HostHound"
}
}OpenGraph support is intended for BloodHound Enterprise or BloodHound Community v8+ with a PostgreSQL graph database. Save the exported payload as .json and upload/import it as OpenGraph data in BloodHound.
cd hosthound/app
python export_opengraph.py \
--input ../collector/hosthound-WINDEV.json \
--output ../collector/hosthound-WINDEV-opengraph.jsonYou can also convert a directory of HostHound JSON files into one OpenGraph payload:
python export_opengraph.py \
--input ../collections \
--output ../collections/hosthound-fleet-opengraph.jsoncd hosthound/app
python server.py \
--input ../collector/hosthound-WINDEV.json \
--export-opengraph ../collector/hosthound-WINDEV-opengraph.jsonStart the server normally, then request:
GET http://127.0.0.1:8000/api/opengraph
The OpenGraph exporter maps HostHound objects like this:
HostHound native node type -> OpenGraph kinds
Host -> ["HostHoundHost", "HostHound"]
User -> ["HostHoundUser", "HostHound"]
Group -> ["HostHoundGroup", "HostHound"]
Service -> ["HostHoundService", "HostHound"]
ScheduledTask -> ["HostHoundScheduledTask", "HostHound"]
File -> ["HostHoundFile", "HostHound"]
Privilege -> ["HostHoundPrivilege", "HostHound"]
Finding -> ["HostHoundFinding", "HostHound"]
Relationship types are mapped to namespaced OpenGraph kinds such as:
MEMBER_OF -> HostHoundMemberOf
CAN_WRITE -> HostHoundCanWrite
EXECUTED_BY -> HostHoundExecutedBy
RUNS_AS -> HostHoundRunsAs
CAN_ESCALATE_TO -> HostHoundCanEscalateTo
Each OpenGraph edge uses ID matching:
{
"kind": "HostHoundCanWrite",
"start": { "value": "Group:BUILTIN\\Users", "match_by": "id" },
"end": { "value": "File:C:\\HostHoundLab\\WritableService\\HostHoundLabSvc.exe", "match_by": "id" }
}Findings are exported as HostHoundFinding nodes by default. Use --no-findings with export_opengraph.py or --no-opengraph-findings with server.py to omit them.