Threat Model Concepts
August 26, 2026 · View on GitHub
A threat model is a structured representation of your system that captures what components exist, how they communicate, what data they handle, and what security controls protect them. This page explains the building blocks.
Methodology
Dethereal's default methodology is STRIDE-per-element — the plugin systematically evaluates spoofing, tampering, repudiation, information disclosure, denial of service, and elevation of privilege for each component, boundary crossing, and data flow. This maps directly to the data flow diagram (DFD) modeling approach and integrates with MITRE ATT&CK for concrete technique identification.
The underlying data model is methodology-agnostic. STRIDE is the built-in interpretive overlay; the platform's module system can support additional methodologies (PASTA, attack trees) as analysis modules.
Model Scope
Before modeling the system you declare scope — the framing that tells analysis what this model is for and what to hold constant. Scope is set at scope-definition time (e.g. via /dethereal:create), stored on the model itself, and round-trips through .dethereal/scope.json on sync:
| Field | Meaning |
|---|---|
| Depth | How deep the model reasons: architecture, design, or implementation. |
| Modeling intent | Why the model exists: initial, security_review, compliance, or incident_response. |
| Compliance drivers | Regulatory/standards obligations in scope (e.g. PCI-DSS, HIPAA) — these drive which compliance prompts appear during enrichment. |
| Exclusions | What you deliberately put out of scope, so analysis doesn't fault their absence. |
| Trust assumptions | What the model treats as trusted (e.g. "cloud control plane is trusted"). |
Scope is seed context for analysis, not a score — it shapes which findings matter rather than computing risk directly. Crown jewels are also declared at scope-definition time (see Crown Jewels).
Components
Components are the things in your system. Every component has a type that determines its behavior in security analysis:
| Type | What It Represents | Examples |
|---|---|---|
| PROCESS | Running software that processes data | API servers, web applications, workers, microservices |
| STORE | Anything that persists data | Databases, caches, file storage, message queues |
| EXTERNAL_ENTITY | People or systems outside your control | End users, third-party APIs, OAuth providers |
Why types matter: The analysis engine treats each type differently. A STORE holding credentials has different threat implications than a PROCESS routing traffic. STORE components must be classified for data sensitivity analysis to work — this is why the classification quality gate requires 100% STORE classification.
Trust Boundaries
Trust boundaries define security zones in your architecture. They represent where the rules change — different network segments, different authentication requirements, different access controls.
Why Hierarchy Matters
A flat model loses security context:
defaultBoundary/
├── User (EXTERNAL_ENTITY)
├── Web Server (PROCESS)
├── API Server (PROCESS)
└── Database (STORE)
A hierarchical model captures trust transitions:
defaultBoundary/
├── Internet Zone/
│ └── User (EXTERNAL_ENTITY)
├── DMZ/
│ └── Web Server (PROCESS)
└── Internal Network/
├── Application Tier/
│ └── API Server (PROCESS)
└── Data Tier/
└── Database (STORE)
The analysis engine identifies threats at boundary crossings — data flows that move between trust zones. A flat model has no boundary crossings, so the analysis has nothing to work with.
Boundary Enforcement Attributes
Each boundary can have enforcement attributes that describe how it controls traffic:
| Attribute | Values | What It Means |
|---|---|---|
implicit_deny_enabled | true / false | Boundary blocks traffic by default |
allow_any_inbound | true / false | Boundary allows unrestricted inbound |
egress_filtering | deny_all / allow_list / allow_all / unknown | Outbound traffic policy |
These attributes feed into the attack surface analysis — a boundary with implicit_deny_enabled: true and egress_filtering: deny_all provides stronger isolation than one with allow_any_inbound: true.
Trust Zones, Planes, Domains, and Conduits
Beyond enforcement posture, each boundary can carry zoning — your declared segmentation intent. Zoning is stored on the boundary in structure.json and round-trips to the platform:
| Field | What It Captures |
|---|---|
zone | The trust zone — the exposure tier the boundary sits on: who can reach it. One of UNTRUSTED, PUBLIC, EXPOSED, INTERNAL, RESTRICTED, or VENDOR. |
planes | The boundary's operational role: WORKLOAD, MANAGEMENT, or both. |
domains | Free-text business-function tags (e.g. payments, identity). |
conduits | Approved channels — directional peer boundaries this one is meant to talk to, each with an optional justification. |
A trust zone inherits down. A boundary with no zone of its own takes the zone of its nearest declaring ancestor; if nothing up the chain declares one, it falls back to the default INTERNAL (and is flagged). Set a zone explicitly wherever the trust level genuinely changes — especially when a child is stricter than its parent (a RESTRICTED store inside an INTERNAL tier), since inheritance would otherwise understate it.
Structural containers abstain — zone the leaves, not the wrapper. A boundary that mainly nests other boundaries (a VPC, a Kubernetes cluster, a cloud account) usually spans several tiers at once, so no single zone is correct for it. Leave it unset and zone the leaf boundaries inside; the review shows the container's span as a display roll-up rather than forcing one zone onto the whole subtree.
Domains and planes are tags, not structure. Use them to describe what a boundary is for and how it operates, separate from who can reach it. Model identity, compute-node, location, and business grouping as domains/planes tags rather than as zone-bearing boundaries.
Conduits are approved crossings. A conduit records that a boundary is supposed to communicate with a peer, in a chosen direction, with a reason. It is declared design intent — the platform records it but does not verify or enforce it. Whether the real flows match your declared conduits is the job of security analysis, which reads your zoning as its baseline.
Zoning is authored two ways that write the same fields: the guided workflow ratifies it step by step (see Guided Workflow), and the platform GUI edits it directly. For the complete how-to — inheritance rules, the Zoning overview, worked examples — see the platform guide Boundary Trust Zones.
Data Flows
Data flows are directed connections between components. They represent who talks to whom, using what protocol.
API Server → PostgreSQL: SQL queries (TLS 1.3)
End Users → Web Server: HTTPS requests
Cross-Boundary Flows
When a data flow crosses a trust boundary (source and target are in different boundaries), it becomes high-priority for security analysis. Cross-boundary flows are where authentication, encryption, and access controls are most critical.
Flow Attributes
Data flows can carry security attributes:
| Attribute | Purpose |
|---|---|
required_credentials | Which credentials are needed for this flow (drives lateral movement analysis) |
auth_failure_mode | What happens when authentication fails: deny, fallback, fail_open, unknown |
encryption_in_transit | TLS version, mTLS, none |
The auth_failure_mode is particularly important — a flow that appears authenticated but fails open under error conditions provides no security guarantee.
Data Items
Data items classify what data flows carry. They're attached to data flows and describe the sensitivity of the information.
Sensitivity Levels
| Level | Examples |
|---|---|
| Restricted (Tier 1) | Regulated PII, cardholder data, credentials, health records |
| Confidential (Tier 2) | Internal business data, session tokens, API keys |
| Internal (Tier 3) | Internal operational data, metrics |
| Public (Tier 4) | Public content, documentation |
Regulatory Labels
Data items can carry regulatory flags — free-text compliance labels, kept separate from the sensitivity level. The recommended canonical set (matched exactly and case-sensitively by the platform's dataInRegulatoryScope query, so spelling and case matter):
| Label | Framework | Sensitivity floor |
|---|---|---|
PCI cardholder | PCI-DSS | Restricted |
PHI | HIPAA | Restricted |
GDPR personal | GDPR | Confidential (special-category data is Restricted) |
PII | General | Confidential |
SOX financial | SOX | Confidential |
CCPA personal | CCPA | Confidential |
A data item may carry several flags; its sensitivity is the highest applicable floor. This set is the single source of truth in the architecture docs — it's extensible, but emit the canonical casing so scope queries match. Your compliance drivers (set during scope definition) determine which regulatory prompts appear during enrichment.
Classes and Modules
What Classes Are
Classes are predefined types from the platform's module system. When you classify a component as "Database" or "Web Application," you're assigning it a class that comes with:
- An attribute schema (which security properties are relevant)
- Default attribute values
- Guidance for enrichment
How Classification Works
Classification happens in two passes:
-
Deterministic (Pass 1): The plugin queries the platform for available classes and matches components by name and type. "PostgreSQL" matches "Database" with high confidence — no AI needed.
-
LLM-assisted (Pass 2): For ambiguous components, the AI proposes classes based on boundary context and peer components.
You always confirm classifications before they're written. The plugin never auto-classifies without showing you what it's doing.
For more on the classification process, see the classify command or the platform's Component Configuration Guide.
Security Attributes
The 6 Key Component Attributes
A floor of six security properties that enrichment captures on every in-scope component, whatever its class template happens to cover. Five are component attributes; encryption in transit is recorded on the data flow. The attribute key column is what you must write — the scorer reads these literal names:
| # | Concept | Attribute key | Written on | Value the scorer accepts |
|---|---|---|---|---|
| 1 | Authentication | authentication_type | component | String — e.g. oauth2, mtls, sso. none does not count. basic/digest count only when encryption_in_transit is a non-deprecated string |
| 2 | Encryption in transit | encryption_in_transit | data flow (and component where meaningful) | String — e.g. TLS 1.3. Rejected: none, sslv3, ssl v3, tls 1.0, tls1.0 |
| 3 | Encryption at rest | encryption_at_rest | component | String — e.g. AES-256. Rejected: none, des, 3des, triple-des, rc4 |
| 4 | Logging | (no scored key) | — | Captured by the class template; nothing reads a floor-level key for this one |
| 5 | Access control | implicit_deny_enabled | component (boundaries too, but only the component value is scored) | Boolean true |
| 6 | Log telemetry | monitoring_tools | component | Non-empty string[] after discarding none/n/a. Use [], never ["None"]. Components with no monitoring tools are detection blind spots |
Write these key names literally. control_coverage_rate (10 points of the quality score) and the attack-surface report's encryption and authentication coverage read these exact keys and no others — a semantically equivalent name that a class template happens to use (transit_encryption_enforced, tls_enabled, …) does not satisfy the floor. Types are strict too: a boolean true for encryption_at_rest scores as absent, because that key wants the concrete algorithm string.
Note that encryption_in_transit is scored per cross-boundary data flow, not per component — it belongs on the flow's attribute file under attributes/dataFlows/. The other five are component attributes, stored in individual files under attributes/components/.
These attributes are a floor, not the whole enrichment job: the primary pass resolves every field declared by each element's assigned class template. See Discovery and Enrichment.
Additional Attributes
asset_criticality— high/medium/low, the business impact of compromisestores_credentials— true for STORE components that hold credential materialcredential_scope— which credential identifiers are stored (drives lateral movement analysis)
Crown Jewels
Crown jewels are your most valuable assets — the data or capabilities an attacker would target. You name them during scope definition:
Crown jewels: ["Cardholder data", "User PII", "API authentication keys"]
During classification, these free-text names are fuzzy-matched to actual components and tagged with crownJewel: true on the component in structure.json (the first-class Component.crownJewel field, synced to the platform). Crown-jewel marks are tracked per component; marking a data item, boundary, or flow stays local. Besides this AI classify path, crown jewels can also be toggled per component directly in the GUI (component settings → General tab → crown button) — both write the same Component.crownJewel field. Crown jewels receive priority treatment:
- Enrichment tier 1 — enriched first, with the most thorough prompts
- Control gap analysis — crown jewels without controls are flagged as highest-priority gaps
- Attack surface — appear in the top tier of the surface analysis
Quality Scoring
The quality score (0-100) measures how completely your model captures the system. It is not a security rating — a model with 95/100 quality could describe a system with critical vulnerabilities. The score reflects modeling completeness, not security posture.
The score is computed from 7 weighted factors: component classification (25), attribute completion (20), boundary hierarchy (15), data flow coverage (15), data classification (10), control coverage (10), and credential coverage (5).
Score labels: Starting (0-39), In Progress (40-69), Good (70-89), Comprehensive (90-100). Analysis readiness requires 70+.
The 3 Quality Gates
Three progressive gates enforce increasing strictness:
- Gate 1 (Creation, advisory) — flags structural issues without blocking: missing classifications, unnamed flows, single-component boundaries
- Gate 2 (Sync, blocking) — must pass before
/dethereal:sync push: manifest completeness, structure validity, reference integrity - Gate 3 (Analysis, blocking) — must pass for meaningful analysis: 100% classification, >= 80% attribute completion, data items classified for sensitive flows, >= 1 cross-boundary flow
For the full factor breakdown, gate criteria, and example output, see Review and Analysis.
The Split-File Directory Format
Threat models are stored as a directory of JSON files:
threat-models/my-system/
├── manifest.json # Model metadata
├── structure.json # Boundary and component hierarchy
├── dataflows.json # Data flow connections
├── data-items.json # Data classifications
├── README.md # Auto-generated summary
├── .dethereal/ # Workflow metadata
│ ├── state.json # Current workflow state
│ ├── scope.json # Scope definition
│ ├── quality.json # Quality score cache
│ ├── discovery.json # Discovery provenance (gitignore)
│ ├── sync.json # Sync metadata (gitignore)
│ ├── control-audit.log # Append-only control-decision ledger (commit)
│ ├── class-cache/ # Cached class templates and guides
│ │ └── {class-id}.json
│ └── template-fields/ # Per-element template field manifests
│ └── {element-id}.json
├── attributes/ # Per-element security attributes
│ ├── boundaries/
│ │ └── {id}.json
│ ├── components/
│ │ └── {id}.json
│ ├── dataFlows/
│ │ └── {id}.json
│ └── dataItems/
│ └── {id}.json
└── controls/ # Per-Control library files (one per referenced Control)
└── {id}.json
manifest.json
Model metadata: name, description, and which platform modules it uses.
structure.json
The hierarchy of trust boundaries and components, with visual coordinates for diagram rendering. This is where the boundary tree lives.
dataflows.json
An array of directed connections between components. Each flow has a source, target, protocol, and description.
data-items.json
An array of data classification items attached to data flows. Captures what sensitive data is flowing through the system.
attributes/
Per-element attribute files containing security properties. Each element type has its own subdirectory. Attribute files are created during classification (as stubs) and populated during enrichment.
controls/
Per-Control files for the control library. Each Control referenced by the model (via controls[] on a component, boundary, or flow in structure.json / dataflows.json) gets a file here. See Controls and the Control Library below.
ID Handling
When you create a model locally, components get temporary reference IDs (UUIDs). These link elements together (e.g., a data flow's source/target IDs reference components in structure.json).
After pushing to the platform, the server assigns permanent IDs that are written back to your local files. The original temporary IDs become obsolete.
Metadata Directories
Two metadata directories track plugin state:
.dethernety/ (Project Root)
Plugin-level metadata shared across models:
models.json— registry of all local models with names, paths, and timestampsdiscovery-cache.json— cached discovery results for multi-model projects (gitignore)decomposition-plan.json— multi-model decomposition plan (when modeling complex systems)
.dethereal/ (Per Model)
Per-model workflow metadata inside each model directory:
state.json— current workflow state and completed statesscope.json— scope definition (crown jewels, compliance drivers, etc.)quality.json— cached quality score (deleted on backward transitions)discovery.json— discovery provenance (gitignore — may contain infrastructure details)sync.json— sync metadata (gitignore — per-user state)control-audit.log— append-only ledger of shared-ownership control decisionsclass-cache/— class templates and configuration guides cached at classification time. This is what makes enrichment work offline, and it is the basis the attribute-completion factor is measured against. Delete it and the quality score silently falls back to counting attribute files instead of resolved fields;generate_attribute_stubsrebuilds it.template-fields/— one manifest per element, recording which fields its current class declared. When an element is reclassified, this is what lets the plugin drop the old class's unanswered fields while preserving values you already enriched.
Controls and the Control Library
Controls are reusable security controls (e.g., "Database Encryption Package", "SOC Monitoring") that one or more model elements reference. They live in the platform's library and are mirrored locally as controls/<id>.json — one file per Control referenced by your model. References from elements live in controls[] arrays on components, boundaries, and flows.
Why Controls are separate from attributes
The 6 key security attributes (authentication, encryption in transit, etc.) describe each element's intrinsic posture. Controls describe how that posture is achieved — and the same Control can apply to many elements across many Models. Editing the control's configuration in one Model affects every other Model that uses it, which is why Controls have their own file per Control and a safety check on push (see shared-ownership prompts).
ControlClass and per-instance attributes
A Control is an instance of one or more ControlClasses (e.g., "Encryption at Rest", "Access Control"). Each (Control, ControlClass) pair has its own attribute payload describing how that instance is configured (algorithm, key rotation, audit retention, etc.).
{
"id": "ctrl-encryption-package",
"name": "Database Encryption Package",
"lifecycle": "brownfield",
"classes": [
{
"classId": "class-encryption-at-rest",
"attributes": { "algorithm": "AES-256", "keyRotationDays": 30 },
"platformAttributes": { "algorithm": "AES-128", "keyRotationDays": 90 }
}
]
}
The attributes block is what the operator edits. The platformAttributes block is the raw server-side payload as of the last pull. The pair is how the plugin detects local edits and shared-ownership conflicts.
Lifecycle states
| State | Meaning |
|---|---|
greenfield | Created locally; no platform counterpart yet. Push assigns a UUID and rebinds. |
brownfield | Pulled from the platform. Edits queue as pendingEdit blocks for the next push. |
partially-pushed | Mid-flight state during a push that touched multiple class entries. |
tombstoned | Will not be re-pulled (deleted on platform, or operator-retired locally). |
The Two-Write Rule
Every edit to attributes must (a) bump localEditedAt and (b) populate pendingEdit. The plugin enforces this via the set-local-edited MCP action — never edit controls/<id>.json by hand. Direct edits bypass the safety check and drop your changes silently on the next reconciliation.
For the full control workflow, see Discovery and Enrichment Part 4. For the push-time safety mechanics, see Sync and Version Control.
Next: Discovery and Enrichment — infrastructure scanning, security attributes, MITRE integration