{# 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. #} {% macro stride_table(matrix) %}
{% for letter in ["S", "T", "R", "I", "D", "E"] %} {% 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
{{ tm_dash.coverage_pct }}% Coverage — {{ tm_dash.coverage_reviewed }} / {{ tm_dash.coverage_in_scope }} in-scope systems
{{ tm_dash.open_threats_past_due }} Open threats past due (>{{ past_due_sla_days }}d)
{{ tm_dash.mitigated_untested }} Threats mitigated but untested
{{ tm_dash.median_threats_per_model if tm_dash.median_threats_per_model is not none else "—" }} Median threats per model

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.

Portfolio total (every modeled project, summed)
{{ stride_table(tm_dash.stride_aggregate) }}
{% if tm_dash.stride_by_project %}

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

{% for entry in tm_dash.stride_by_project %}
{{ entry.project_display_name }} ({{ entry.threat_count }} threat{{ "s" if entry.threat_count != 1 else "" }})
{{ stride_table(entry.matrix) }}
{% endfor %}
{% endif %}

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 }})

Accepted threats

Every threat marked Accepted, reconciled with who actually signed off — secfoo accept create is how "not recorded" turns into a real name.

{% if tm_dash.accepted_threats %}
{% for item in tm_dash.accepted_threats %} {% endfor %}
ThreatProjectAccepted by
{{ item.threat }} {{ item.project }} {% if item.accepted_by %}{{ item.accepted_by }} {% else %}Not recorded{% endif %}
{% else %}

No accepted threats yet.

{% endif %}

Models needing rework

{% if tm_dash.needs_rework %}
{% for p in tm_dash.needs_rework %} {% endfor %}
ProjectStateLast modeled
{{ p.project_display_name }} {% if p.state == "never_reviewed" %}Never modeled {% elif p.state == "boundary_changed" %}Trust boundary changed {% else %}Stale ({{ p.days_since_review }}d){% endif %} {{ (p.last_reviewed_at | fmt_time) if p.last_reviewed_at else "Never" }}
{% else %}
Nothing needs rework — every in-scope project has a current model.
{% endif %}

Recurring threat patterns

{% if tm_dash.recurring_patterns %}
{% for archetype, count in tm_dash.recurring_patterns %}
{{ archetype }}
{{ count }}
{% endfor %}
{% else %}
No threats yet to aggregate.
{% endif %}

Model depth mix

{{ tm_dash.model_depth_mix.full }}Full
{{ tm_dash.model_depth_mix.feature_level }}Feature-level
{{ tm_dash.model_depth_mix.lightweight }}Lightweight

Threats found post-build

{{ tm_dash.post_build_count_recent }} in the last 90 days ({{ tm_dash.post_build_count_total }} all-time) {% if tm_dash.post_build_recent %} {% else %}

None recorded yet — see secfoo miss.

{% endif %}

Falsified assumptions

{{ tm_dash.falsified_assumptions | length }} {% if tm_dash.falsified_assumptions %} {% else %}

No falsified assumptions recorded yet.

{% endif %}