# `Choreo.ThreatModel.Analysis`
[🔗](https://github.com/code-shoily/choreo/blob/v0.14.1/lib/choreo/threat_model/analysis.ex#L1)

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 |
|----------|---------|----------|
| **S**poofing | External entities, processes | Can someone impersonate this? |
| **T**ampering | Processes, data stores, flows | Can data be modified? |
| **R**epudiation | External entities, processes | Can actions be denied? |
| **I**nformation Disclosure | Processes, data stores, flows | Can data leak? |
| **D**enial of Service | Processes, data stores, flows | Can this be overwhelmed? |
| **E**levation of Privilege | Processes, data stores | Can an attacker gain access? |

## Further reading

  * [STRIDE Model (Microsoft)](https://learn.microsoft.com/en-us/azure/security/develop/threat-modeling-tool-threats)
  * [STRIDE-per-Element](https://shostack.org/resources/stride-per-element)
  * [OWASP Threat Modeling](https://owasp.org/www-community/Threat_Modeling)

# `attack_paths`

```elixir
@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`

```elixir
@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`

```elixir
@spec boundary_matrix(Choreo.ThreatModel.t()) :: %{
  optional({String.t() | nil, String.t() | nil}) =&gt; 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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`.

# `high_risk_processes`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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

  * `:rules` — list of modules implementing `Choreo.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 `: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`

```elixir
@spec threat_summary(Choreo.ThreatModel.t()) :: %{
  by_category: %{required(atom()) =&gt; %{required(atom()) =&gt; non_neg_integer()}},
  by_severity: %{required(atom()) =&gt; 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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
