STRIDE threat analysis for Choreo.ThreatModel.
Automatically generates threats based on element types, data-flow topology, and trust-boundary crossings.
STRIDE categories
| Category | Targets | Question |
|---|---|---|
| Spoofing | External entities, processes | Can someone impersonate this? |
| Tampering | Processes, data stores, flows | Can data be modified? |
| Repudiation | External entities, processes | Can actions be denied? |
| Information Disclosure | Processes, data stores, flows | Can data leak? |
| Denial of Service | Processes, data stores, flows | Can this be overwhelmed? |
| Elevation of Privilege | Processes, data stores | Can an attacker gain access? |
Further reading
Summary
Functions
Returns all paths from external entities to data stores.
Returns all data flows that cross a trust boundary.
Returns data stores that are reachable from an external entity (directly or indirectly).
Generates a heatmap of the threat model based on threat density.
Returns processes that sit in low-trust boundaries but access high-sensitivity data stores.
Calculates a total risk score and qualitative risk rating for the threat model.
Generates STRIDE threats for every element and data flow in the model.
Summarises threats by STRIDE category and severity.
Returns unencrypted data flows that cross a trust boundary.
Validates a threat model and returns a list of issues.
Functions
@spec attack_paths( Choreo.ThreatModel.t(), keyword() ) :: [[Yog.node_id()]]
Returns all paths from external entities to data stores.
Each result is a list of node IDs representing a path from the internet to data at rest. These are the attack vectors that an adversary would follow.
Complexity warning
This function enumerates every simple path from each external entity to each data store. On dense graphs the number of paths grows exponentially. Use
:max_pathsto cap output for large models.
Options
:max_paths— maximum number of paths to return (default: unlimited)
Examples
iex> model = Choreo.ThreatModel.new()
iex> model = model
...> |> Choreo.ThreatModel.add_external_entity(:user)
...> |> Choreo.ThreatModel.add_process(:api)
...> |> Choreo.ThreatModel.add_data_store(:db)
...> |> Choreo.ThreatModel.data_flow(:user, :api)
...> |> Choreo.ThreatModel.data_flow(:api, :db)
iex> paths = Choreo.ThreatModel.Analysis.attack_paths(model)
iex> [:user, :api, :db] in paths
trueThis analysis answers the question: "What are the attack vectors from outside to data at rest?"
@spec cross_boundary_flows(Choreo.ThreatModel.t()) :: [ {Yog.node_id(), Yog.node_id(), String.t() | nil, String.t() | nil} ]
Returns all data flows that cross a trust boundary.
Each result is {from, to, from_boundary, to_boundary}.
Examples
iex> model = Choreo.ThreatModel.new()
iex> model = model
...> |> Choreo.ThreatModel.add_trust_boundary("internet", level: 0)
...> |> Choreo.ThreatModel.add_trust_boundary("app", level: 2)
...> |> Choreo.ThreatModel.add_external_entity(:user, boundary: "internet")
...> |> Choreo.ThreatModel.add_process(:api, boundary: "app")
...> |> Choreo.ThreatModel.data_flow(:user, :api)
iex> flows = Choreo.ThreatModel.Analysis.cross_boundary_flows(model)
iex> length(flows)
1
iex> Enum.any?(flows, fn {from, to, _, _} -> from == :user and to == :api end)
trueThis analysis answers the question: "Which data flows cross a trust boundary?"
@spec exposed_data_stores(Choreo.ThreatModel.t()) :: [Yog.node_id()]
Returns data stores that are reachable from an external entity (directly or indirectly).
These are high-value targets because they contain data at rest and are exposed to untrusted input.
Examples
iex> model = Choreo.ThreatModel.new()
iex> model = model
...> |> Choreo.ThreatModel.add_external_entity(:user)
...> |> Choreo.ThreatModel.add_process(:api)
...> |> Choreo.ThreatModel.add_data_store(:db)
...> |> Choreo.ThreatModel.data_flow(:user, :api)
...> |> Choreo.ThreatModel.data_flow(:api, :db)
iex> Choreo.ThreatModel.Analysis.exposed_data_stores(model)
[:db]This analysis answers the question: "Which data stores are reachable from external entities?"
@spec heatmap( Choreo.ThreatModel.t(), keyword() ) :: Choreo.ThreatModel.t()
Generates a heatmap of the threat model based on threat density.
Nodes with more threats will be colored with "hotter" colors from the selected palette.
Note on flow threats
Flow-level threats (targeting
{from, to}tuples) are not counted toward node heat since they apply to edges, not nodes. A node with zero element-level threats but many high-severity flow threats may appear cold in the heatmap.
Options
:palette— Color palette (:heat,:cool,:spectral)- All other options are passed to
stride_threats/2.
@spec high_risk_processes(Choreo.ThreatModel.t()) :: [Yog.node_id()]
Returns processes that sit in low-trust boundaries but access high-sensitivity data stores.
These are risky because compromised process code can leak or tamper with sensitive data.
Examples
iex> model = Choreo.ThreatModel.new()
iex> model = model
...> |> Choreo.ThreatModel.add_process(:api)
...> |> Choreo.ThreatModel.add_data_store(:db, sensitivity: :confidential)
...> |> Choreo.ThreatModel.data_flow(:api, :db)
iex> Choreo.ThreatModel.Analysis.high_risk_processes(model)
[:api]This analysis answers the question: "Which processes access sensitive data from low-trust zones?"
@spec risk_score( Choreo.ThreatModel.t(), keyword() ) :: %{score: number(), rating: atom()}
Calculates a total risk score and qualitative risk rating for the threat model.
Each threat's severity is mapped to a numeric score. The total score is the sum of all individual threat scores.
Severity Weights (Default)
The default weights ratchet up sharply (1 → 3 → 6 → 10). This maps roughly to qualitative severity ramps but is not directly equivalent to CVSS scoring.
:low— 1:medium— 3:high— 6:critical— 10
Qualitative Ratings (Default)
0—:none1..10—:low11..30—:medium31..70—:high>70—:critical
Options
:weights— keyword list of custom severity weights (e.g.,[low: 2, medium: 4, ...])
Examples
iex> model = Choreo.ThreatModel.new()
...> |> Choreo.ThreatModel.add_trust_boundary("app")
...> |> Choreo.ThreatModel.add_process(:api, boundary: "app")
iex> %{score: score, rating: rating} = Choreo.ThreatModel.Analysis.risk_score(model)
iex> is_number(score)
true
iex> rating in [:none, :low, :medium, :high, :critical]
trueThis analysis answers the question: "What is the overall security risk rating of the architecture?"
@spec stride_threats( Choreo.ThreatModel.t(), keyword() ) :: [ %{ id: String.t(), category: atom(), target: Yog.node_id() | {Yog.node_id(), Yog.node_id()}, description: String.t(), severity: :low | :medium | :high | :critical, mitigation: String.t() } ]
Generates STRIDE threats for every element and data flow in the model.
Returns a list of threat structs:
%{
id: String.t(),
category: :spoofing | :tampering | :repudiation | :information_disclosure | :denial_of_service | :elevation_of_privilege,
target: Yog.node_id(),
description: String.t(),
severity: :low | :medium | :high | :critical,
mitigation: String.t()
}Options
:rules— list of modules implementingChoreo.ThreatModel.Analysis.Rule
ID ordering
Threat IDs (
T1,T2, ...) are assigned in element-iteration order followed by flow-iteration order. Custom rule threats that already have an:idfield keep their original ID; only auto-generated threats are numbered.
Examples
iex> model = Choreo.ThreatModel.new()
iex> model = model
...> |> Choreo.ThreatModel.add_trust_boundary("internet", level: 0)
...> |> Choreo.ThreatModel.add_trust_boundary("app", level: 2)
...> |> Choreo.ThreatModel.add_external_entity(:user, boundary: "internet")
...> |> Choreo.ThreatModel.add_process(:api, boundary: "app")
...> |> Choreo.ThreatModel.data_flow(:user, :api)
iex> threats = Choreo.ThreatModel.Analysis.stride_threats(model)
iex> Enum.any?(threats, & &1.category == :spoofing)
true
iex> Enum.any?(threats, & &1.target == :user)
true
iex> Enum.any?(threats, & match?({:user, :api}, &1.target))
trueThis analysis answers the question: "What threats exist in my architecture?"
@spec threat_summary(Choreo.ThreatModel.t()) :: %{ by_category: %{required(atom()) => %{required(atom()) => non_neg_integer()}}, by_severity: %{required(atom()) => non_neg_integer()}, total: non_neg_integer() }
Summarises threats by STRIDE category and severity.
Returns a map of %{category => %{severity => count}} plus totals.
Useful for dashboards and executive reporting.
Examples
iex> model = Choreo.ThreatModel.new()
iex> model = model
...> |> Choreo.ThreatModel.add_trust_boundary("app")
...> |> Choreo.ThreatModel.add_process(:api, boundary: "app")
iex> summary = Choreo.ThreatModel.Analysis.threat_summary(model)
iex> summary.total > 0
true
iex> is_map(summary.by_category)
true
iex> is_map(summary.by_severity)
trueThis analysis answers the question: "How are threats distributed by category and severity?"
@spec unencrypted_boundary_flows(Choreo.ThreatModel.t()) :: [ {Yog.node_id(), Yog.node_id()} ]
Returns unencrypted data flows that cross a trust boundary.
These are prime targets for interception and tampering.
Examples
iex> model = Choreo.ThreatModel.new()
iex> model = model
...> |> Choreo.ThreatModel.add_trust_boundary("internet", level: 0)
...> |> Choreo.ThreatModel.add_trust_boundary("app", level: 2)
...> |> Choreo.ThreatModel.add_external_entity(:user, boundary: "internet")
...> |> Choreo.ThreatModel.add_process(:api, boundary: "app")
...> |> Choreo.ThreatModel.data_flow(:user, :api)
iex> Choreo.ThreatModel.Analysis.unencrypted_boundary_flows(model)
[{:user, :api}]This analysis answers the question: "Which cross-boundary flows are unencrypted?"
@spec validate(Choreo.ThreatModel.t()) :: [{:error | :warning, String.t()}]
Validates a threat model and returns a list of issues.
Checks for:
- elements not assigned to a trust boundary
- unencrypted cross-boundary flows
- processes without privilege level
- data stores without sensitivity classification
Does not currently check:
- external entities with incoming flows from internal processes
- data stores in low-trust boundaries with high sensitivity
- trust boundaries without a
:level
Examples
iex> model = Choreo.ThreatModel.new()
iex> model = model
...> |> Choreo.ThreatModel.add_trust_boundary("app")
...> |> Choreo.ThreatModel.add_process(:api, boundary: "app", privilege: :user)
...> |> Choreo.ThreatModel.add_data_store(:db, boundary: "app", sensitivity: :internal)
...> |> Choreo.ThreatModel.data_flow(:api, :db, encrypted: true)
iex> Choreo.ThreatModel.Analysis.validate(model)
[]
iex> model = Choreo.ThreatModel.new()
iex> model = model
...> |> Choreo.ThreatModel.add_process(:api)
iex> issues = Choreo.ThreatModel.Analysis.validate(model)
iex> Enum.any?(issues, fn {_sev, msg} -> String.contains?(msg, "trust boundary") end)
trueThis analysis answers the question: "Is the threat model structurally sound?"