{# Extended Threat Modeling dashboard: coverage, a portfolio-total STRIDE x asset-class matrix plus one per project, threat disposition (tested vs. untested mitigations), accepted threats reconciled with recorded human owners, models needing rework, recurring threat archetypes, model-depth mix, post-build misses, and falsified assumptions. See web/threat_modeling_dashboard.py for what's real vs. approximated. #} {% from "_macros.html" import info_tip %} {% macro stride_table(matrix) %}
{% for letter, name in [("S", "Spoofing"), ("T", "Tampering"), ("R", "Repudiation"), ("I", "Information disclosure"), ("D", "Denial of service"), ("E", "Elevation of privilege")] %} {% endfor %} {% for row in matrix %} {% for letter in ["S", "T", "R", "I", "D", "E"] %} {% set n = row.cells[letter] %} {% endfor %} {% endfor %}
Asset class{{ letter }}
{{ row.asset_class }} {{ n if n else "—" }}
{% endmacro %} Program dashboard
{% call info_tip("Coverage") %}

Share of in-scope systems that have at least one successful threat model.

In scope means every project that has an assessment. To raise it, model the projects listed under Models needing rework.

{% endcall %} {{ tm_dash.coverage_pct }}% Coverage — {{ tm_dash.coverage_reviewed }} / {{ tm_dash.coverage_in_scope }} in-scope systems
{% call info_tip("Open threats past due") %}

Threats a model left as a Gap (no mitigation) where that model is more than {{ past_due_sla_days }} days old.

Threats have no individual due dates, so the model's age stands in for one. Close the gap, or record a risk acceptance with secfoo accept create.

{% endcall %} {{ tm_dash.open_threats_past_due }} Open threats past due (>{{ past_due_sla_days }}d)
{% call info_tip("Mitigated but untested") %}

Threats marked Mitigated with no test reference in the threat register: the control is claimed, not proven.

Add a test, pen-test or scan reference in the next model to move them to Mitigated + tested.

{% endcall %} {{ tm_dash.mitigated_untested }} Threats mitigated but untested
{% call info_tip("Median threats per model") %}

The middle value of how many threats each project's latest model found.

A very low number usually means a shallow model rather than a safe system. Compare with Model depth mix.

{% endcall %} {{ tm_dash.median_threats_per_model if tm_dash.median_threats_per_model is not none else "—" }} Median threats per model
{% call info_tip("STRIDE × asset-class coverage") %}

Counts the threats in each project's latest model by STRIDE category (columns) and by the kind of element they target (rows).

Red cells (none found) are the ones to read: that combination was never examined, which is not the same as safe. Choose a project to see which system a gap belongs to.

{% endcall %}

STRIDE × asset-class coverage

Empty cells are the signal — a blank column means that category was never exercised, not that it's safe from it.

S SpoofingT TamperingR Repudiation I Information disclosureD Denial of serviceE Elevation of privilege
None found 1–2 threats 3 or more
Portfolio total (every modeled project, summed)
{{ stride_table(tm_dash.stride_aggregate) }}
{% if selected_stride_project %}

By project — a blank cell here is a real gap on that system; the total above can't say which project it came from.

{% if tm_dash.stride_by_project | length > 1 %}
{% endif %}
{{ selected_stride_project.project_display_name }} ({{ selected_stride_project.threat_count }} threat{{ "s" if selected_stride_project.threat_count != 1 else "" }})
{{ stride_table(selected_stride_project.matrix) }}
{% endif %}
{% call info_tip("Threat disposition") %}

What happened to every threat in each project's latest model:

Mitigated + tested (control proven), Mitigated, untested (claimed only), Accepted (risk taken on), Transferred (to a vendor or insurer), Open (a gap), and Past due (a gap in a model older than {{ past_due_sla_days }} days).

{% endcall %}

Threat disposition

{% if tm_dash.disposition_total %}
{% for key in ["mitigated_tested", "mitigated_untested", "accepted", "transferred", "open", "past_due"] %} {% set count = tm_dash.disposition[key] %} {% if count %}
{% endif %} {% endfor %}
{% else %}
{% endif %}
Mitigated + tested ({{ tm_dash.disposition.mitigated_tested }}) Mitigated, untested ({{ tm_dash.disposition.mitigated_untested }}) Accepted ({{ tm_dash.disposition.accepted }}) Transferred ({{ tm_dash.disposition.transferred }}) Open ({{ tm_dash.disposition.open }}) Past due ({{ tm_dash.disposition.past_due }})
{% call info_tip("Accepted threats") %}

Every threat a model marked Accepted, matched with the person who signed it off.

A threat with no owner shown has not been signed off by anyone yet. Record the owner with secfoo accept create.

This card shows the newest ones. The full list is on its own page.

{% endcall %}

Accepted threats

The newest threats marked Accepted, reconciled with who signed off. secfoo accept create is how "not recorded" turns into a real name.

{% if tm_dash.accepted_preview %}
{% for item in tm_dash.accepted_preview %} {% endfor %}
ThreatProjectAccepted by
{{ item.threat }} {{ item.project }} {% if item.accepted_by %}{{ item.accepted_by }} {% else %}Not recorded{% endif %}
{% if tm_dash.accepted_threats | length > tm_dash.accepted_preview | length %} View all {{ tm_dash.accepted_threats | length }} → {% endif %} {% else %}

No accepted threats yet.

{% endif %}

Models needing rework {% call info_tip("Models needing rework", inline=True) %}

In-scope projects whose threat model is missing, stale (latest successful model is over a year old), or whose trust boundaries changed between the last two models.

Sorted by open findings, as a stand-in for how critical the system is.

{% endcall %} {% if tm_dash.needs_rework %}{{ tm_dash.needs_rework | length }}{% endif %}

{% if tm_dash.needs_rework %} {% else %}
Nothing needs rework — every in-scope project has a current model.
{% endif %}

Recurring threat patterns {% call info_tip("Recurring threat patterns", inline=True) %}

The most common kinds of threat across every successful threat model, including older ones.

A pattern that keeps coming back is best fixed once, at platform level (a shared control or library), rather than project by project.

{% endcall %} {% if tm_dash.recurring_patterns %}{{ tm_dash.recurring_patterns | length }}{% endif %}

{% if tm_dash.recurring_patterns %} {% else %}
No threats yet to aggregate.
{% endif %}
{% call info_tip("Model depth mix") %}

How thorough each project's latest threat model was, as the model declares about itself.

Full covers the whole system. Feature-level covers one feature or new data flow. Lightweight is a fast checklist pass.

Lightweight models miss more. Critical systems should have a Full model; a mostly-lightweight portfolio means coverage looks better than it is.

{% endcall %}

Model depth mix

{% set full_n = tm_dash.model_depth_mix.full %} {% set feature_n = tm_dash.model_depth_mix.feature_level %} {% set light_n = tm_dash.model_depth_mix.lightweight %} {% set depth_total = full_n + feature_n + light_n %}

How deep each project's latest model went{% if depth_total %} ({{ depth_total }} model{{ "s" if depth_total != 1 else "" }}){% endif %}.

{% if depth_total %} {% endif %} {% if light_n > full_n %}

More lightweight than full models: re-run critical systems at --depth standard.

{% endif %}
{% call info_tip("Threats found post-build") %}

Threats discovered after release (an incident, a pen test, a later scan) that an earlier model should have caught.

Recorded by hand with secfoo miss, since a model can't know what it missed. A rising count means models are too shallow. This card shows the newest ones; the full list is on its own page.

{% endcall %}

Threats found post-build

{{ tm_dash.post_build_count_recent }} Last 90 days
{{ tm_dash.post_build_count_total }} All-time
{% if tm_dash.post_build_recent %} {% if tm_dash.post_build_count_total > tm_dash.post_build_recent | length %} View all {{ tm_dash.post_build_count_total }} → {% endif %} {% else %}

None recorded yet — see secfoo miss.

{% endif %}
{% call info_tip("Falsified assumptions") %}

Assumptions a model relied on (for example, "only internal traffic reaches this service") that it then checked against the code and found false.

Each one marks a place where the design and the real system disagree. This card shows the newest ones; the full list is on its own page.

{% endcall %}

Falsified assumptions

{{ tm_dash.falsified_assumptions | length }} checked against the code and found false.

{% if tm_dash.falsified_preview %} {% if tm_dash.falsified_assumptions | length > tm_dash.falsified_preview | length %} View all {{ tm_dash.falsified_assumptions | length }} → {% endif %} {% else %}

No falsified assumptions recorded yet.

{% endif %}