Choreo.ThreatModel.Analysis (Choreo v0.14.0)

Copy Markdown View Source

STRIDE threat analysis for Choreo.ThreatModel.

Automatically generates threats based on element types, data-flow topology, and trust-boundary crossings.

STRIDE categories

CategoryTargetsQuestion
SpoofingExternal entities, processesCan someone impersonate this?
TamperingProcesses, data stores, flowsCan data be modified?
RepudiationExternal entities, processesCan actions be denied?
Information DisclosureProcesses, data stores, flowsCan data leak?
Denial of ServiceProcesses, data stores, flowsCan this be overwhelmed?
Elevation of PrivilegeProcesses, data storesCan an attacker gain access?

Further reading

Summary

Functions

Returns all paths from external entities to data stores.

Computes the downstream blast radius if element_id is compromised.

Summarises data flows between trust boundaries.

Returns control gaps inferred from trust boundaries, data sensitivity, and privileges.

Returns all data flows that cross a trust boundary.

Identifies external entry points entering trusted zones.

Returns sensitive-data exfiltration paths from data stores to external entities.

Identifies egress exit points leaving trusted zones towards untrusted or external targets.

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.

Highlights attack paths in the model by setting :highlighted_nodes and :highlighted_edges.

Produces a prioritized security-review finding list.

Calculates residual risk after declared controls have mitigated threats.

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 threats targeting a specific element or flow.

Generates a GitHub Flavored Markdown threat table.

Returns unencrypted data flows that cross a trust boundary.

Returns only unmitigated threats.

Validates a threat model and returns a list of issues.

Functions

attack_paths(model, opts \\ [])

@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_paths to 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
true

This analysis answers the question: "What are the attack vectors from outside to data at rest?"

blast_radius(model, element_id)

@spec blast_radius(Choreo.ThreatModel.t(), Yog.node_id()) :: %{
  element: Yog.node_id(),
  reachable_nodes: [Yog.node_id()],
  affected_stores: [Yog.node_id()],
  affected_boundaries: [String.t()],
  max_sensitivity: atom() | nil,
  risk_level: atom()
}

Computes the downstream blast radius if element_id is compromised.

Returns a map detailing reachable nodes, affected data stores, affected boundaries, maximum data sensitivity exposed, and qualitative risk rating.

Examples

iex> model = Choreo.ThreatModel.new()
...>   |> Choreo.ThreatModel.add_process(:api)
...>   |> Choreo.ThreatModel.add_data_store(:db, sensitivity: :restricted)
...>   |> Choreo.ThreatModel.data_flow(:api, :db)
iex> radius = Choreo.ThreatModel.Analysis.blast_radius(model, :api)
iex> radius.max_sensitivity
:restricted
iex> radius.risk_level
:critical
iex> radius.affected_stores
[:db]

boundary_matrix(model)

@spec boundary_matrix(Choreo.ThreatModel.t()) :: %{
  optional({String.t() | nil, String.t() | nil}) => map()
}

Summarises data flows between trust boundaries.

Returns a map keyed by {from_boundary, to_boundary} with flow counts, encrypted/authenticated counts, unencrypted counts, max observed sensitivity, and the concrete flows in each boundary pair.

control_gaps(model)

@spec control_gaps(Choreo.ThreatModel.t()) :: [map()]

Returns control gaps inferred from trust boundaries, data sensitivity, and privileges.

This acts as a lightweight security-review checklist for missing controls on elements and data flows. Each gap includes :target, :missing, :severity, and a human-readable :reason.

cross_boundary_flows(model)

@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)
true

This analysis answers the question: "Which data flows cross a trust boundary?"

entry_points(model)

@spec entry_points(Choreo.ThreatModel.t()) :: [map()]

Identifies external entry points entering trusted zones.

Returns a list of entry point maps with :source, :target, :from_boundary, :to_boundary, :authenticated, :encrypted, :label, and :controls.

Examples

iex> model = Choreo.ThreatModel.new()
...>   |> 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, label: "Login")
iex> [entry] = Choreo.ThreatModel.Analysis.entry_points(model)
iex> entry.source
:user
iex> entry.target
:api

exfiltration_paths(model, opts \\ [])

@spec exfiltration_paths(Choreo.ThreatModel.t(), keyword()) :: [[Yog.node_id()]]

Returns sensitive-data exfiltration paths from data stores to external entities.

Options

  • :sensitivity - sensitivity level or list of levels to consider (default: [:confidential, :restricted])
  • :max_paths - maximum paths to return

exit_points(model)

@spec exit_points(Choreo.ThreatModel.t()) :: [map()]

Identifies egress exit points leaving trusted zones towards untrusted or external targets.

Returns a list of exit point maps with :source, :target, :from_boundary, :to_boundary, :authenticated, :encrypted, :label, and :controls.

Examples

iex> model = Choreo.ThreatModel.new()
...>   |> Choreo.ThreatModel.add_trust_boundary("internet", level: 0)
...>   |> Choreo.ThreatModel.add_trust_boundary("app", level: 2)
...>   |> Choreo.ThreatModel.add_external_entity(:webhook, boundary: "internet")
...>   |> Choreo.ThreatModel.add_process(:api, boundary: "app")
...>   |> Choreo.ThreatModel.data_flow(:api, :webhook, label: "Notify")
iex> [exit_point] = Choreo.ThreatModel.Analysis.exit_points(model)
iex> exit_point.source
:api
iex> exit_point.target
:webhook

exposed_data_stores(model)

@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?"

heatmap(model, opts \\ [])

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.

high_risk_processes(model)

@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?"

highlight_attack_paths(model, opts \\ [])

@spec highlight_attack_paths(Choreo.ThreatModel.t(), keyword()) ::
  Choreo.ThreatModel.t()

Highlights attack paths in the model by setting :highlighted_nodes and :highlighted_edges.

Examples

iex> model = Choreo.ThreatModel.new()
...>   |> Choreo.ThreatModel.add_external_entity(:attacker)
...>   |> Choreo.ThreatModel.add_process(:gateway)
...>   |> Choreo.ThreatModel.add_data_store(:vault)
...>   |> Choreo.ThreatModel.data_flow(:attacker, :gateway)
...>   |> Choreo.ThreatModel.data_flow(:gateway, :vault)
iex> highlighted = Choreo.ThreatModel.Analysis.highlight_attack_paths(model)
iex> :vault in highlighted.highlighted_nodes
true
iex> {:gateway, :vault} in highlighted.highlighted_edges
true

prioritized_findings(model, opts \\ [])

@spec prioritized_findings(Choreo.ThreatModel.t(), keyword()) :: [map()]

Produces a prioritized security-review finding list.

Findings compose validation issues, exposed stores, unencrypted boundary flows, high-risk processes, exfiltration paths, and control gaps into a concise list sorted by severity. Use :max_findings to cap report length.

residual_risk_score(model, opts \\ [])

@spec residual_risk_score(Choreo.ThreatModel.t(), keyword()) :: %{
  score: number(),
  rating: atom()
}

Calculates residual risk after declared controls have mitigated threats.

This is equivalent to risk_score(model, only_unmitigated: true) and accepts the same options as risk_score/2, including custom severity :weights.

risk_score(model, opts \\ [])

@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:none
  • 1..10:low
  • 11..30:medium
  • 31..70:high
  • >70:critical

Options

  • :weights — keyword list of custom severity weights (e.g., [low: 2, medium: 4, ...])
  • :only_unmitigated, :category, :severity, and :rules — forwarded to stride_threats/2

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]
true

This analysis answers the question: "What is the overall security risk rating of the architecture?"

stride_threats(model, opts \\ [])

@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(),
    mitigated?: boolean(),
    controls: [atom()],
    owasp: 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

ID ordering

Threat IDs (T1, T2, ...) are assigned in element-iteration order followed by flow-iteration order. Custom rule threats that already have an :id field 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))
true

This analysis answers the question: "What threats exist in my architecture?"

threat_summary(model)

@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)
true

This analysis answers the question: "How are threats distributed by category and severity?"

threats_for(model, target, opts \\ [])

@spec threats_for(
  Choreo.ThreatModel.t(),
  Yog.node_id() | {Yog.node_id(), Yog.node_id()},
  keyword()
) ::
  [map()]

Returns threats targeting a specific element or flow.

Options

  • :include_flows — boolean, whether to include flows connected to target (default: true)

Examples

iex> model = Choreo.ThreatModel.new()
...>   |> Choreo.ThreatModel.add_process(:api)
...>   |> Choreo.ThreatModel.add_data_store(:db)
...>   |> Choreo.ThreatModel.data_flow(:api, :db)
iex> api_threats = Choreo.ThreatModel.Analysis.threats_for(model, :api, include_flows: false)
iex> Enum.all?(api_threats, &(&1.target == :api))
true

to_markdown(model, opts \\ [])

@spec to_markdown(Choreo.ThreatModel.t(), keyword()) :: String.t()

Generates a GitHub Flavored Markdown threat table.

Options

  • :summary — boolean, whether to include a summary header with risk score and counts (default: true)
  • All other options are passed to stride_threats/2 (:only_unmitigated, :category, :severity, etc.)

Examples

iex> model = Choreo.ThreatModel.new()
...>   |> Choreo.ThreatModel.add_trust_boundary("app")
...>   |> Choreo.ThreatModel.add_process(:api, boundary: "app")
iex> md = Choreo.ThreatModel.Analysis.to_markdown(model)
iex> String.contains?(md, "| ID | Category | Target |")
true

unencrypted_boundary_flows(model)

@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?"

unmitigated_threats(model, opts \\ [])

@spec unmitigated_threats(Choreo.ThreatModel.t(), keyword()) :: [map()]

Returns only unmitigated threats.

Examples

iex> model = Choreo.ThreatModel.new()
...>   |> Choreo.ThreatModel.add_process(:api)
iex> threats = Choreo.ThreatModel.Analysis.unmitigated_threats(model)
iex> Enum.all?(threats, &(!&1.mitigated?))
true

validate(model, opts \\ [])

@spec validate(Choreo.ThreatModel.t(), keyword()) :: [{: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
  • direct data flows between external entities and data stores
  • sensitive data stores located in low-trust boundaries
  • trust boundaries without a :level (when require_levels: true)

Options

  • :require_levels — boolean, whether to warn if trust boundaries lack a :level (default: false)

Examples

iex> model = Choreo.ThreatModel.new()
iex> model = model
...>   |> Choreo.ThreatModel.add_trust_boundary("app", level: 2)
...>   |> 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)
true

This analysis answers the question: "Is the threat model structurally sound?"