Threat Model¶
System¶
Offline inputs (exported findings or synthetic data) go through validation into an in-memory attack graph. The engines work on that graph, and results are served over the CLI (stdout JSON) or a localhost FastAPI.
Assets¶
- Ingested findings. They map an organisation's weaknesses, so they are sensitive and useful to an attacker.
- Remediation output. It lists which fixes matter most, so it also works as a roadmap for an attacker.
- Integrity of the recommendations. Wrong advice could lead defenders to deprioritise a real risk.
Trust boundaries¶
| Boundary | Crossing | Controls |
|---|---|---|
| Scanner export to connector | untrusted file content | pydantic validation, drop-and-log invalid records; defusedxml parsing (a hard dependency since 0.2.0) |
| User to API | HTTP on localhost | bind 127.0.0.1; Host allow-list (LINCHPIN_ALLOWED_HOSTS) against DNS rebinding; JSON bodies only (no simple-request CSRF); size and work limits; no auth (token auth is needed before any non-local use) |
| Config file | YAML | yaml.safe_load and pydantic validation |
STRIDE (abridged)¶
| Threat | Example | Mitigation / status |
|---|---|---|
| Spoofing | anyone on the host, or a browser page via DNS rebinding, calls the API | localhost bind; Host header allow-list (400 otherwise); auth is still TODO |
| Tampering | forged findings hide a path, e.g. fake "allowed: false" reachability | provenance (source, observed_at) kept on each finding; signed or hashed input bundles are TODO |
| Repudiation | nobody can tell who ingested which data | findings keep their source; audit log is TODO |
| Information disclosure | state file or API output leaks the weakness map | state stays local and is gitignored; API is local-only |
| Denial of service | huge body, quadratic graph build, path explosion | 413 above LINCHPIN_MAX_BODY_MB (25, also for streamed bodies), above LINCHPIN_MAX_FINDINGS (200k held) and when a build would exceed LINCHPIN_MAX_EDGES (1M, checked before the work); k <= 1000, budget <= 50, what-if lists <= 1000 (422) |
| Elevation of privilege | XML entity expansion in the parser | defusedxml (a hard dependency) rejects internal and external entity declarations |
Misuse considerations¶
The tool could be pointed at stolen scan data to plan an attack. Its output (ranked choke points) is the same thing a defender needs, and it contains nothing that helps with exploitation: no exploit mapping, no payloads, no target interaction. That is the main reason the project is defensively scoped.
Model-correctness risks¶
The cost model is heuristic. prerequisite_match is fixed, credential reuse ignores network reachability, and reachability is modelled per segment. Every recommendation lists its evidence path ids so an analyst can verify it. Treat the output as decision support, not ground truth.
v0.2 additions¶
| Asset / entry point | Threat | Mitigation |
|---|---|---|
| XML exports (OpenVAS, Nessus, nmap) | XXE, billion laughs | defusedxml by default; entity declarations refused without it |
| Dataset downloader | tampered sample files | commit-pinned URLs plus SHA-256 verification; HTTPS only; reports only, never binaries |
Web UI (/ui) |
CDN compromise, XSS from finding strings | Cytoscape pinned with SRI; all finding text HTML-escaped before insertion; Swagger UI / ReDoc (unpinned CDN scripts) are off unless LINCHPIN_API_DOCS=1 |
/demo/load scenario path |
reading arbitrary server files, NTLM leaks via UNC paths | only a relative name under LINCHPIN_SCENARIO_DIR (or the configured LINCHPIN_SCENARIO); absolute, drive, UNC, device and .. paths are refused by their text before any filesystem call; every file the scenario lists must stay inside LINCHPIN_DATA_DIR (403). v1.0.0 accepted any server path here: upgrade |
| Neo4j mirror | credential leakage; cross-origin access to Neo4j's HTTP API | password only via NEO4J_PASSWORD (compose refuses to start without it), never in config; ports bound to localhost; compose restricts Neo4j's CORS origin to its own Browser |
Findings with a malformed detail |
crash the graph build (500 until restart) | per-kind detail validation at ingest (models.detail_problem); invalid records are rejected or dropped and logged, and the builder skips them |
| Cypher export | scanned banners containing $graph break the script |
single-pass parameter substitution in to_cypher, escaped map keys |