{# Defensive defaults block — placed BEFORE the render-mode gate per the E.1/E.2/E.3 + C.1/C.2/D pattern. Mirrors all prior Phase 1b/1c migrations. Coerces non-mapping / non-iterable upstream values so the template never crashes on data drift. #} {% set summary = summary | default({}) %} {% if summary is not mapping %}{% set summary = {} %}{% endif %} {% set portfolio_kpi = portfolio_kpi | default({}) %} {% if portfolio_kpi is not mapping %}{% set portfolio_kpi = {} %}{% endif %} {% set cve_update_summary = cve_update_summary | default({}) %} {% if cve_update_summary is not mapping %}{% set cve_update_summary = {} %}{% endif %} {% set cve_updates = cve_updates | default({}) %} {% if cve_updates is not mapping %}{% set cve_updates = {} %}{% endif %} {% set portfolio_progression = portfolio_progression | default([]) %} {% if portfolio_progression is mapping or portfolio_progression is string or portfolio_progression is not iterable %}{% set portfolio_progression = [] %}{% endif %} {% set projects = projects | default([]) %} {% if projects is mapping or projects is string or projects is not iterable %}{% set projects = [] %}{% endif %} {% import "_console_macros.html" as fs with context %} {% if render_mode != 'fragment' %} {% if domain %}{% endif %} Security Progress Report - Finite State Report {# ECharts library — replaces the pre-migration Chart.js stack (umd build + datalabels plugin). #} {% include "_design_system.html" %} {% include "_console_shell.html" %} {% include "_echarts_ready.html" %} {% endif %} {# Recipe-local styles — render in BOTH standalone and fragment modes so fragment_extractor (_STYLE_RE) collects them, scopes under .fs-section-security-progress, and re-emits at the top of the fragment. All hex literals migrated to design tokens. #} {% if render_mode != 'fragment' %} {% endif %} {# Build topbar meta items. Period + Mode + Records emerge only when the upstream data carries them — gated guards keep the topbar clean for flat_snapshot mode and missing metadata. #} {% set sp_meta = [{"label": "Domain", "value": domain or "—"}] %} {% set _ = sp_meta.append({"label": "Scope", "value": scope_label | default('All Projects')}) %} {% if summary.get('period_start') and summary.get('period_end') %} {% set _ = sp_meta.append({"label": "Period", "value": summary.get('period_start') ~ " → " ~ summary.get('period_end')}) %} {% endif %} {% if summary.get('mode') %} {% set _sp_mode_raw = summary.get('mode') %} {% set _sp_mode_label = 'Version Progression' if _sp_mode_raw == 'version_progression' else ('Flat Snapshot' if _sp_mode_raw == 'flat_snapshot' else _sp_mode_raw) %} {% set _ = sp_meta.append({"label": "Mode", "value": _sp_mode_label}) %} {% endif %} {% if metadata is mapping and metadata.get('transformed_count') is not none %} {% set _ = sp_meta.append({"label": "Records", "value": metadata.get('transformed_count') | string}) %} {% endif %} {% set _ = sp_meta.append({"label": "Generated", "value": generated_at or "—"}) %} {% if render_mode != 'fragment' %} {{ fs.topbar( crumbs=["Finite State", "Security Progress"], meta=sp_meta, controls=[], ) }} {% endif %}

Security Progress Report

{{ scope_label | default('All Projects') }} — Security Posture Over Time

{# State-aware flat_snapshot banner — prose preserved verbatim from pre-migration lines 124-131. Migrated to .sp-flat-notice (yellow chip via color-mix on the medium-severity text token). #} {% if summary.get('mode') == 'flat_snapshot' %}
Version progression data is not available. This report requires multiple firmware versions per project to show progress over time. The data below reflects a point-in-time snapshot rather than version-to-version trends.
{% endif %} {# 4 KPI cells via fs.kpi_cell — only in version_progression mode (portfolio_kpi is empty in flat_snapshot mode). #} {% if portfolio_kpi %} {% set sp_total_resolved = portfolio_kpi.get('total_resolved', 0) | int %} {% set sp_total_new = portfolio_kpi.get('total_new', 0) | int %} {% set sp_net_change = portfolio_kpi.get('net_change', 0) | int %} {% set sp_projects_improved = portfolio_kpi.get('projects_improved', 0) | int %} {% set sp_total_projects = portfolio_kpi.get('total_projects', 0) | int %}
{{ fs.kpi_cell( "Total Resolved", sp_total_resolved, dot=("low" if sp_total_resolved > 0 else None), ) }} {{ fs.kpi_cell( "Total New", sp_total_new, dot=("high" if sp_total_new > 0 else None), ) }} {{ fs.kpi_cell( "Net Change", (("+" ~ (sp_net_change | string)) if sp_net_change > 0 else (sp_net_change | string)), ) }} {{ fs.kpi_cell( "Projects Improved", (sp_projects_improved | string) ~ " / " ~ (sp_total_projects | string), dot=("low" if sp_projects_improved > 0 else None), ) }}
{% endif %} {# Open CVE Trend by Severity — stacked-severity line chart. The container is gated on portfolio_progression having at least 2 points so a single-point series doesn't render an empty frame. #} {% if portfolio_progression and (portfolio_progression | length) > 1 %}
{{ fs.panel_head("Open CVE Trend", meta="total open CVEs over time, broken down by severity") }} {% if render_mode == 'fragment' and not fragment_scripts_enabled %}
Chart unavailable in fragment view
{% else %}
{% endif %}
{% endif %} {# Per-project progress summary table. #} {% if projects and (projects | length) > 0 %}
{{ fs.panel_head("Project Progress", meta="per-project change between baseline and current versions") }}

Per-project vulnerability change between baseline and current versions. Sorted by most improved.

{% set ns = namespace(hidden=0) %} {# Pre-filter to mappings with a defined net_change BEFORE sorting — `| sort(attribute='net_change')` would crash on non-mapping rows or rows missing the key. #} {% set _sp_safe_projects = [] %} {% for _p in projects %}{% if _p is mapping and _p.get('net_change') is not none %}{% set _ = _sp_safe_projects.append(_p) %}{% endif %}{% endfor %} {% for p in _sp_safe_projects | sort(attribute='net_change') %} {% set p_net = p.get('net_change', 0) | int %} {% set p_baseline = p.get('baseline_open', 0) | int %} {% set p_current = p.get('current_open', 0) | int %} {% set p_resolved = p.get('total_resolved', 0) | int %} {% set p_new = p.get('total_new', 0) | int %} {% if p_net != 0 or p_baseline > 0 or p_current > 0 or p_resolved > 0 or p_new > 0 %} {% else %} {% set ns.hidden = ns.hidden + 1 %} {% endif %} {% endfor %} {% if ns.hidden > 0 %} {% endif %}
Project Baseline Open Current Open Resolved New Net Change
{{ p.get('project_name', '—') }} {{ p_baseline }} {{ p_current }} {{ p_resolved }} {{ p_new }} {% if p_net > 0 %}+{% endif %}{{ p_net }}
{{ ns.hidden }} project{{ 's' if ns.hidden != 1 else '' }} with no findings or changes not shown
{% endif %} {# CVE Updates breakdown table — gated on `cve_update_summary` being a mapping with a positive `total` (defensive `is mapping` guard mirrors C.2/D pattern). Per-row description prose added per the 1c.1 IN-band spec. #} {% if cve_update_summary is mapping and cve_update_summary.get('total', 0) > 0 %}
{{ fs.panel_head("CVE Updates Summary", meta="CVE database changes during the reporting window") }}

CVE database changes detected during the reporting window that affect open findings.

{% if cve_update_summary.get('added', 0) > 0 %} {% endif %} {% if cve_update_summary.get('retracted', 0) > 0 %} {% endif %} {% if cve_update_summary.get('severity_escalated', 0) > 0 %} {% endif %} {% if cve_update_summary.get('severity_downgraded', 0) > 0 %} {% endif %} {% if cve_update_summary.get('exploit_gained', 0) > 0 %} {% endif %} {% if cve_update_summary.get('exploit_subsided', 0) > 0 %} {% endif %} {% if cve_update_summary.get('other_updates', 0) > 0 %} {% endif %}
Category Description Count
Newly Added CVEs Newly added to NVD during this period. {{ cve_update_summary.get('added', 0) }}
Retracted CVEs Rejected or withdrawn by NVD during this period. {{ cve_update_summary.get('retracted', 0) }}
Severity Escalated CVSS score increased — re-prioritize remediation. {{ cve_update_summary.get('severity_escalated', 0) }}
Severity Downgraded CVSS score decreased — may relax priority. {{ cve_update_summary.get('severity_downgraded', 0) }}
Exploit Gained New exploit signal (KEV listing, weaponization, threat-actor attribution). {{ cve_update_summary.get('exploit_gained', 0) }}
Exploit Subsided Exploit signal retracted — pressure reduced. {{ cve_update_summary.get('exploit_subsided', 0) }}
Other Updates Other NVD metadata changes (CWE, description, references, etc.). {{ cve_update_summary.get('other_updates', 0) }}
Total {{ cve_update_summary.get('total', 0) }}
{% endif %} {# Per-project collapsible detail — gated on summary.mode == 'version_progression' (not shown in flat_snapshot mode). #} {% if projects and (projects | length) > 0 and summary.get('mode') == 'version_progression' %}
{{ fs.panel_head("Version-by-Version Detail", meta="expand a project to see per-version progression") }}

Expand a project to see how vulnerability counts changed across firmware versions.

{# Pre-filter to mappings with defined net_change BEFORE sorting (same defensive ordering as the project summary table above). #} {% set _sp_detail_safe_projects = [] %} {% for _p in projects %}{% if _p is mapping and _p.get('net_change') is not none %}{% set _ = _sp_detail_safe_projects.append(_p) %}{% endif %}{% endfor %} {% for p in _sp_detail_safe_projects | sort(attribute='net_change') %} {% set p_net = p.get('net_change', 0) | int %} {% set p_baseline = p.get('baseline_open', 0) | int %} {% set p_current = p.get('current_open', 0) | int %} {% set p_resolved = p.get('total_resolved', 0) | int %} {% set p_new = p.get('total_new', 0) | int %} {% set p_progression = p.get('progression', []) %} {% if p_progression is mapping or p_progression is string or p_progression is not iterable %}{% set p_progression = [] %}{% endif %} {% if (p_progression | length) > 0 and (p_net != 0 or p_baseline > 0 or p_current > 0 or p_resolved > 0 or p_new > 0) %}
{{ p.get('project_name', '—') }} ({{ p_progression | length }} version{{ 's' if (p_progression | length) != 1 else '' }}, net {% if p_net > 0 %}+{% endif %}{{ p_net }} ) {% for v in p_progression %} {% if v is mapping %} {% endif %} {% endfor %}
Version Date Open Resolved New Critical High Medium Low
{{ v.get('version', '--') }} {{ v.get('date_display') or v.get('date') or '--' }} {{ v.get('open_count', 0) }} {{ v.get('resolved', 0) }} {{ v.get('new', 0) }} {{ v.get('critical', 0) }} {{ v.get('high', 0) }} {{ v.get('medium', 0) }} {{ v.get('low', 0) }}
{% endif %} {% endfor %}
{% endif %}
{% if render_mode != 'fragment' %} {{ fs.status_bar(items=[ "Recipe: Security Progress", "Net Change: " ~ ((portfolio_kpi.get('net_change', summary.get('net_change', 0))) | string), "Projects: " ~ ((portfolio_kpi.get('total_projects', summary.get('total_projects', 0))) | string), generated_at | default(''), ], version='fs-report ' ~ (fs_report_version | default('dev'))) }} {% endif %} {% if render_mode != 'fragment' or fragment_scripts_enabled %} {% include '_action_buttons.html' %} {% endif %}