Repository navigation
Expand file tree
/
Copy pathsections-contract.yaml
More file actions
1571 lines (1495 loc) · 85.5 KB
/
Copy pathsections-contract.yaml
File metadata and controls
1571 lines (1495 loc) · 85.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# sections-contract.yaml — declarative structure of threat-model.md
#
# This file is the SINGLE SOURCE OF TRUTH for the shape of the rendered
# Markdown report. The renderer (`scripts/renderers/compose_threat_model.py`) reads it
# and emits `threat-model.md` deterministically from:
#
# * `threat-model.yaml` → threats, mitigations, components,
# security_controls, meta (canonical)
# * `.triage-flags.json` → triage ranking (v2+)
# * `.fragments/<section-id>.json` → LLM-authored data fragments for
# sections that need prose (verdict,
# architecture-assessment themes,
# critical-attack-tree, …)
# * `.fragments/<section-id>.md` → LLM-authored raw-markdown fragments
# for the few sections that are pure
# prose (system-overview, assets,
# out-of-scope)
#
# The LLM NEVER writes `threat-model.md` directly. This eliminates the
# structural-drift failure mode observed when the composer invented a
# non-canonical Management Summary layout.
#
# Validation:
# * Every section listed in `document.order` must be rendered (or
# conditionally skipped per `condition`).
# * For sections with `fragment_type: data`, the fragment is validated
# against the JSON Schema in `schemas/fragments/<fragment>.schema.json`
# before rendering — a schema violation aborts the run.
# * For sections with `fragment_type: markdown`, the fragment is inlined
# verbatim after checking that it begins with the declared `heading`.
# * For sections with `fragment_type: hybrid`, the renderer combines a
# deterministic computed block with one schema-validated data fragment.
# * For sections with `fragment_type: computed`, the renderer derives
# the content from `threat-model.yaml` + triage output — no LLM
# fragment is consulted at all.
#
# Contract stability: structural changes (renaming a section, adding a
# required column) are breaking changes. Bump `contract_version` and
# record in CHANGELOG.
contract_version: 5 # v5 adds the unnumbered Systemic Weaknesses chapter
# ---------------------------------------------------------------------------
# Document-wide settings
# ---------------------------------------------------------------------------
document:
# Rendered as `# {{ title }}` at the top of the file.
title_template: "Security Assessment Report — {{ project.name }}"
# Sections are rendered in this order. Each id must appear in the
# `sections:` map below. `condition` is evaluated against a small
# context dict (`critical_count`, `high_count`, `check_requirements`,
# `verbose_report`, `has_out_of_scope`, …); missing context keys mean
# "condition false".
order:
- infobox
- changelog
- { id: quick_mode_notice, condition: "is_quick_depth" }
- toc
- management_summary
# The unnumbered `## Critical Findings by Root Cause` section sits
# between the Management Summary and §1. It links each Critical
# finding to the weakness records that explain it; the producer is
# renderers/pregenerate_fragments.py (`ms-critical-attack-tree.json`).
# The section is gated on `critical_count >= 2`.
- { id: critical_attack_tree, condition: "has_multi_critical" }
- system_overview
- { id: identified_actors, condition: "has_resolved_actors" }
- { id: trust_boundary_catalog, condition: "has_trust_boundaries" }
- architecture_diagrams
# §3 Attack Walkthroughs — conditional on `not skip_attack_walkthroughs`.
# `--quick` sets `skip_attack_walkthroughs=true` in `.skill-config.json`
# which causes the composer to drop §3 entirely (heading, body, TOC entry,
# quick-mode-notice bullet, §8 intro back-link). The composer's
# `is_skipped_section_id` / `_compute_toc_entries` paths read this flag
# via `eval_context["skip_attack_walkthroughs"]`. Standard / thorough
# depths leave it `false` and §3 renders normally.
- { id: attack_walkthroughs, condition: "not skip_attack_walkthroughs" }
- assets
- attack_surface
# §6 Security Architecture (2026-07-15 reorder) — the controls analysis now
# precedes the Weakness Register so the document reads controls → gaps →
# instances: §6 analyses which controls exist, §7 Weakness Register names the
# systemic gaps in them, and §8 Findings Register lists the concrete instances.
#
# §6 carries a `render_security_architecture` condition (true by default).
# Set to False at compose-time when current depth is `quick` AND no rich
# prior §6 from a standard/thorough run is preservable. Quick mode then
# skips §6 in both body and TOC; a subsequent standard run re-introduces
# the full per-domain analysis on the same repo.
#
# §6 is a deep-only section preserved on downgrade — see the authoritative
# `preserve_on_downgrade:` block at the end of this file (substrate: md-slice).
- { id: security_architecture, condition: "render_security_architecture" }
# When §6 is suppressed at quick depth, the document jumps from §5
# straight to §7 without explanation. The placeholder below renders
# a single italic line so the reader sees the numbering gap is
# deliberate. Active only when §6 is suppressed.
- { id: skipped_sections_placeholder, condition: "not render_security_architecture" }
# §7 Weakness Register — the standalone root-cause register. A weakness is the
# systemic control gap; findings (§8) are its concrete instances, and §6
# Security Architecture (above) analyses the controls it gaps.
- { id: systemic_weaknesses, condition: "has_weakness_register" }
- { id: requirements_compliance, condition: "check_requirements" }
- threat_register
- abuse_cases
- mitigation_register
- out_of_scope
- appendix_run_statistics # always — verbose flag only gates token/cost detail
- { id: composition_notes, condition: "compose_warned" } # M2.14 — only when prior compose warned
# M3.3 — `run_issues` is no longer rendered into threat-model.md. The
# observability data still lives in $OUTPUT_DIR/.run-issues.json
# (written by runtime/aggregate_run_issues.py) and the terminal completion
# summary still surfaces a `-- Run Issues --` block. Inlining the
# appendix into the persisted document was noisy in PR diffs and
# mixed run-meta with the threat content. To re-enable the appendix
# add `- { id: run_issues, condition: "run_warned" }` here and to
# the `full` document set below; the renderer remains in compose.
- appendix_vektor_taxonomy
# Inter-section separator. Rendered between every two consecutive
# sections. Infobox, changelog, toc share the same separator.
section_separator: "\n\n---\n\n"
# ---------------------------------------------------------------------------
# Document sets — named section orderings for partial renders.
#
# "full" is the canonical threat model (uses `order` above).
# "architecture" is the early-render Architecture Model produced at the end
# of Phase 8, before STRIDE data exists. The renderer selects this set when
# invoked with `--document architecture`; unknown sections are skipped
# non-fatally (strict=False) so the render succeeds with partial fragments.
# ---------------------------------------------------------------------------
document_sets:
full:
title_template: "Security Assessment Report - {{ project.name }}"
output_file: "threat-model.md"
order:
- infobox
- changelog
- { id: quick_mode_notice, condition: "is_quick_depth" }
- toc
- management_summary
# Critical Findings by Root Cause — see the main `order:` block.
- { id: critical_attack_tree, condition: "has_multi_critical" }
- system_overview
- { id: identified_actors, condition: "has_resolved_actors" }
- { id: trust_boundary_catalog, condition: "has_trust_boundaries" }
- architecture_diagrams
# §3 conditional on `not skip_attack_walkthroughs` — see main `order:` block.
- { id: attack_walkthroughs, condition: "not skip_attack_walkthroughs" }
- assets
- attack_surface
# §6 Security Architecture / §7 Weakness Register (2026-07-15 reorder) —
# controls precede gaps; see the main `order:` block above.
- { id: security_architecture, condition: "render_security_architecture" }
- { id: skipped_sections_placeholder, condition: "not render_security_architecture" }
- { id: systemic_weaknesses, condition: "has_weakness_register" }
- { id: requirements_compliance, condition: "check_requirements" }
- threat_register
- abuse_cases
- mitigation_register
- out_of_scope
- appendix_run_statistics
- { id: composition_notes, condition: "compose_warned" }
# M3.3 - run_issues removed; see comment on the same line in the
# main `order:` block above for re-enable instructions.
- appendix_vektor_taxonomy
architecture:
title_template: "Architecture Model — {{ project.name }}"
output_file: "analysis-model.md"
preamble: |
> **Architecture Model** — early snapshot produced after Phase 8 (Security Controls),
> before STRIDE threat enumeration. Sections requiring threat data are omitted.
> See `threat-model.md` for the complete assessment.
order:
- infobox
- toc
- system_overview
- { id: trust_boundary_catalog, condition: "has_trust_boundaries" }
- architecture_diagrams
# §3 conditional on `not skip_attack_walkthroughs` — see main `order:` block.
- { id: attack_walkthroughs, condition: "not skip_attack_walkthroughs" }
- assets
- attack_surface
# §6 Use Cases removed (2026-05); see comment in `order:` above.
- { id: security_architecture, condition: "render_security_architecture" }
- { id: requirements_compliance, condition: "check_requirements" }
# Metadata fields validated before rendering starts. Missing required
# fields abort the run; optional fields are rendered only when present.
metadata_fields:
required:
- project.name
- project.version
- project.repository
- project.license
- meta.generated
- meta.git.commit_sha
optional:
- project.description
- project.homepage
- project.author
- project.runtime
- project.tags
# ---------------------------------------------------------------------------
# Severity taxonomy — referenced by multiple sections for consistent emoji
# ---------------------------------------------------------------------------
severity_taxonomy:
critical: { emoji: "🔴", label: "Critical", color: "#dc2626" }
high: { emoji: "🟠", label: "High", color: "#e67700" }
medium: { emoji: "🟡", label: "Medium", color: "#ffd43b" }
low: { emoji: "🟢", label: "Low", color: "#2f9e44" }
info: { emoji: "⚪", label: "Info", color: "#868e96" }
# Aliases used by verdict/architecture-assessment fragments (traffic-light cue).
red: { emoji: "🔴", label: "", color: "#dc2626" }
yellow: { emoji: "🟡", label: "", color: "#f59e0b" }
green: { emoji: "🟢", label: "", color: "#2f9e44" }
effectiveness_taxonomy:
adequate: { emoji: "✅", label: "Adequate", rank: 0 }
partial: { emoji: "⚠️", label: "Partial", rank: 1 }
weak: { emoji: "🔶", label: "Weak", rank: 2 }
unsafe: { emoji: "⛔", label: "Unsafe", rank: 3 }
missing: { emoji: "❌", label: "Missing", rank: 4 }
# ---------------------------------------------------------------------------
# Per-section contracts
# ---------------------------------------------------------------------------
#
# Each section declares:
# heading: rendered heading line (supports `{{ }}` substitution)
# heading_numbered: whether the heading starts with "## N." (true/false)
# anchor: explicit anchor id (overrides heading-derived id)
# fragment_type: "computed" | "data" | "hybrid" | "markdown" | "template"
# computed — renderer derives content from yaml+triage; no fragment
# data — renderer reads <section>.json, validates schema, fills template
# hybrid — renderer combines deterministic content with a validated fragment
# markdown — renderer inlines <section>.md verbatim (prose-only sections)
# template — renderer emits a fixed template with simple substitutions
# fragment: relative path under `.fragments/` dir (only for data/markdown)
# schema: relative path under `schemas/fragments/` (only for data)
# template: relative path under `templates/fragments/` (only for data)
# required_subsections, forbidden_subsections, forbidden_patterns: enforcement
# tables: per-table column + row rules
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# Centralized vocabulary — shared across pregenerator, renderer, and qa_checks
# so a single edit here updates every consumer. Before this block existed,
# verdict icons drifted between `🔴 Missing / 🟠 Weak / 🟡 Partial / 🟢 Adequate`
# (pregenerator output) and `❌ Missing / ⚠️ Partial / 🔶 Weak / ✅ Adequate`
# (threat-analyst-prompt + ms-template). The canonical form below uses the
# reference-style 🔴/🟠/🟡/🟢 set because it provides four distinct hue steps
# (severity-graded) instead of three icons + one checkmark.
# ---------------------------------------------------------------------------
# Verdict semantics — the two red verdicts are NOT interchangeable, and the
# distinction decides the remediation the reader must take:
# 🟢 adequate — control present and sound (nothing to do)
# 🟡 partial — control present, meaningful gaps remain (close the gap)
# 🟠 weak — control present, has exploitable gaps (strengthen it)
# 🔴 unsafe — control present and relied upon, but defeated /
# trivially bypassable → FIX THE EXISTING CONTROL
# 🔴 missing — control was never built → ADD THE CONTROL
# — not_applicable — control category does not apply to this codebase
# "Unsafe" must be used (not "Missing") whenever the control exists in the
# code but does not hold — e.g. an MD5 password hash, a raw-SQL query layer,
# a hardcoded JWT signing key, a parser configured with unsafe options.
# Reserve "Missing" for controls that are genuinely absent (no CSP header,
# no CSRF middleware, no schema-validation layer).
verdict_icons:
adequate: "🟢 Adequate"
partial: "🟡 Partial"
weak: "🟠 Weak"
unsafe: "🔴 Unsafe"
missing: "🔴 Missing"
not_applicable: "—"
sections:
# -------------------------------------------------------------------------
# A. Front matter
# -------------------------------------------------------------------------
infobox:
heading: null # no heading — infobox is a blockquote table
fragment_type: template
template: "infobox.md.j2"
data_source: "threat-model.yaml#project" # renderer reads project dict
required_fields: [name, version, repository, license]
optional_fields: [description, homepage, author, runtime, tags]
changelog:
heading: "## Changelog"
heading_numbered: false
fragment_type: template
# `render_style` selects the template:
# * "table" (default) → one row per version, compact
# * "bullets" → legacy per-version H3 with delta bullets
# The choice is centralised here so the renderer does not need to know
# about the template layout. The composer reads `render_style` and picks
# the matching file; unknown values fall back to the legacy bullets.
render_style: table
template: "changelog-table.md.j2"
data_source: "threat-model.yaml#changelog"
conditional: "len(changelog) > 0"
# Quick-mode transparency banner. Rendered between Changelog and TOC
# when the assessment ran at `quick` depth. Lists what is materially
# reduced (no code examples in mitigations, max 2 threats per STRIDE
# category, max 3 components, no QA, no architect review, no per-finding
# sequence diagrams) so the reader knows the report is intentionally
# narrower than a standard/thorough run.
quick_mode_notice:
heading: "" # no heading — pure callout box
heading_numbered: false
fragment_type: computed # rendered inline by compose
conditional: "is_quick_depth"
toc:
heading: "## Table of Contents"
heading_numbered: false
fragment_type: computed # always derived from `document.order`
# The renderer emits one `1. Management Summary` line per non-skipped
# entry in `document.order`, with sub-bullets for declared subsections.
# -------------------------------------------------------------------------
# B. Management Summary — THE section that the LLM most often drifted on
# -------------------------------------------------------------------------
management_summary:
heading: "## Management Summary"
heading_numbered: false # ⚠ enforced — no "## 1. …"
anchor: "management-summary"
fragment_type: computed # renderer composes from sub-sections below
required_subsections:
- verdict
# mitigations renders "Remediation Priorities": root causes with the
# measures to do first (replaced Top Weaknesses + Top Mitigations).
- mitigations
# security_posture_at_a_glance renders the merged "Security Posture &
# Top Threats" section (heatmap + Top Threats table). The former
# top_findings / architecture_assessment / top_threats children are all
# folded into it.
- security_posture_at_a_glance
- operational_strengths
# Rendered order and blocks: specs/changes/report-anatomy/report-anatomy.md
# → Management Summary.
optional_subsections:
- { id: open_questions_for_team, condition: "has_open_questions" }
# architectural_anti_patterns and systemic_posture no longer render as
# Management Summary blocks (retired 2026-07-14). They stay listed so
# their sections remain reachable: the anti-patterns fragment feeds the
# §7 Weakness Register entries, and _render_security_principles still
# serves standalone callers.
- { id: architectural_anti_patterns, condition: "has_anti_patterns" }
- { id: systemic_posture, condition: "has_weakness_register" }
# ai_exposure_ms is generated from the model at every depth
# (pregenerate_fragments.gen_ai_exposure), so it is never carried over.
- { id: ai_exposure_ms, condition: "has_llm_surface" }
- { id: requirements_compliance_ms, condition: "check_requirements" }
forbidden_subsection_patterns: # regexes; match = QA-strip
- "^\\d+\\.\\d+\\s+" # numbered prefixes "1.1 …"
- "^\\d+\\.\\s+" # numbered prefixes "1. …"
- "^Risk Distribution$"
- "^STRIDE Coverage$"
- "^Executive Overview$"
- "^Immediate Actions( Required.*)?$"
- "^Top Critical Findings$"
- "^Key Strengths$"
- "^Follow-up Actions$"
- "^Overall Security Rating$"
- "^⚠ Worst Case Scenarios$"
# Deterministic questions selected from threat-model.yaml by the same module
# that supplies the completion summary. It follows Remediation Priorities and
# is absent when no team decision remains.
open_questions_for_team:
heading: "### Open Questions for the Team"
heading_numbered: false
fragment_type: computed
conditional: "has_open_questions"
# -- Weakness Register (pre-Findings, unnumbered) -----------------------
# Computed from `weaknesses[]`. Each W-NNN is an evidence-backed root-cause
# control gap; its F-NNN links are the concrete instances/practice sites, and
# each such finding carries a `Weakness:` back-link in §8 (2026-07-14 redesign).
systemic_weaknesses:
heading: "## 7. Weakness Register"
heading_numbered: true
anchor: "7-weakness-register"
fragment_type: computed
conditional: "has_weakness_register"
# -- Critical Findings by Root Cause (post-MS, unnumbered) ---------------
# The template also keeps the earlier anchors `critical-attack-tree` and
# `critical-attack-chain` for external deep-links.
critical_attack_tree:
heading: "## Critical Findings by Root Cause"
heading_numbered: false
anchor: "critical-findings-by-root-cause"
fragment_type: data
fragment: "ms-critical-attack-tree.json"
schema: "critical-attack-tree.schema.json"
template: "critical-attack-tree.md.j2"
# Skipped when there are 0 or 1 Critical findings — one finding has
# nothing to group. The condition
# grammar only supports bare names, so the >=2 check is computed
# in renderers/compose_threat_model.py and exposed as `has_multi_critical`.
conditional: "has_multi_critical"
# -- MS sub-sections ------------------------------------------------------
verdict:
heading: "### Verdict"
heading_numbered: false
fragment_type: data
fragment: "ms-verdict.json"
schema: "verdict.schema.json"
template: "verdict.md.j2"
required_elements:
- severity_emoji # must open with 🟢/🟡/🔴
- red_blockquote # must contain the red HTML blockquote
- finding_refs # ≥2 F-NNN or T-NNN refs in blockquote
# Architectural Anti-Patterns — optional MS callout naming the design-level
# defects the report articulates in §6 (e.g. "SPA without BFF",
# "JWT in localStorage"), so they are visible at executive level instead of
# buried in per-control prose. LLM-authored OPTIONAL fragment: the
# threat-renderer writes ms-anti-patterns.json only when ≥1 genuine
# anti-pattern is present; absent → the section renders nothing
# (gated by `has_anti_patterns`). Rendered via the special-case path in
# `_render_management_summary` (not the generic loop), so it never demands a
# fragment that older runs lack.
architectural_anti_patterns:
heading: "### Architectural Anti-Patterns"
heading_numbered: false
fragment_type: data
fragment: "ms-anti-patterns.json"
schema: "anti-patterns.schema.json"
template: "anti-patterns.md.j2"
conditional: "has_anti_patterns"
# AI / LLM Exposure — optional MS callout surfacing the architectural AI/LLM
# risks the report articulates via the OWASP LLM Top-10 lens (prompt injection,
# excessive agency, model supply chain, …), so the fact the system embeds an
# LLM/agent — and its headline risks — is visible at executive level instead of
# scattered across §6 control prose. Deterministic OPTIONAL fragment:
# pregenerate_fragments.gen_ai_exposure writes ms-ai-exposure.json from the
# findings' OWASP LLM/Agentic tags only when the model has an LLM/AI surface;
# absent → the section renders nothing (gated by `has_llm_surface`), so a repo
# with no AI usage pays zero cost. No agent authors it.
# Rendered via the special-case path in `_render_management_summary` (not the
# generic loop), so it never demands a fragment that non-LLM runs lack.
ai_exposure_ms:
heading: "### AI / LLM Exposure"
heading_numbered: false
fragment_type: data
fragment: "ms-ai-exposure.json"
schema: "ai-exposure.schema.json"
template: "ai-exposure.md.j2"
conditional: "has_llm_surface"
# Security Principles — the P4 systemic posture verdict (Layer-2). Per
# security principle, a deterministic VIOLATED / WEAK / ADEQUATE row fused
# from the weakness register + confirmed findings + implementation strategy
# (scripts/model/build_posture_verdict.py). Hoisted 2026-07-13 from §8 into the
# Management Summary so a systemically-violated principle (e.g. Access
# Control, Input Validation) is loud at executive level even absent a concrete
# instance; §8 carries a back-reference. COMPUTED — no LLM fragment; rendered
# via the special-case path in `_render_management_summary` (like the
# anti-patterns / AI-exposure callouts). Gated by `has_weakness_register`
# (weaknesses[] non-empty), so pre-register runs / clean repos render nothing.
systemic_posture:
heading: "### Security Principles"
heading_numbered: false
fragment_type: computed
conditional: "has_weakness_register"
# Security Posture at a Glance — architecture heatmap showing where the
# Critical findings concentrate and which attack paths connect them.
#
# Contract version 2 (2026-04): the section moved from arrowless
# heatmap + Top-Attack-Paths-table to an arrow-driven 4-column
# diagram + per-attack-class narrative bullets. The diagram is
# computed from threat-model.yaml + the three taxonomies
# (data/attack-class-taxonomy.yaml, business-impact-taxonomy.yaml,
# posture-actor-labels.yaml). The narrative bullets below the
# diagram come from an LLM-authored fragment validated against
# schemas/fragments/security-posture-attack-paths.schema.json.
#
# Conditional: skip the section entirely when there are no
# Critical/High findings (a green-status threat model does not need a
# hero diagram).
security_posture_at_a_glance:
# 2026-05 — merged section. Renders: the attack-route overview (Figure 2,
# forward-only) + an optional architecture diagram (Figure 1, from
# `.fragments/top-threats-architecture.md`) + the **Top Threats** table
# (one threat-grouped row per attack class, replacing the former
# Top Findings + Architecture Assessment). The standalone attack-path
# bullet list is no longer rendered.
heading: "### Security Posture & Top Threats"
heading_numbered: false
# `hybrid` = renderer composes a deterministic block (the diagram +
# the threat-actor intro paragraph) AND consumes one LLM fragment
# (`.fragments/security-posture-attack-paths.json`) for the seven
# numbered attack-class bullets that follow. Both halves are bound
# to schemas / templates listed below.
fragment_type: hybrid
fragment: ".fragments/security-posture-attack-paths.json"
schema: "security-posture-attack-paths.schema.json"
template: "security-posture-diagram.md.j2"
template_attack_paths: "security-posture-attack-paths.md.j2"
# Minimum findings before the diagram renders — below this threshold
# the hero diagram adds more noise than signal.
# 0 = always render (the fixture has only Medium findings, and the
# Management Summary contract requires this section unconditionally).
min_high_or_critical: 0
# Severity floor for findings shown in the tier severity counters.
# Low-severity findings are tracked in §8 but omitted from the
# heatmap (intro paragraph mentions this explicitly).
tier_severity_floor: medium
# Layout invariants — enforced by `validators/qa_checks.py
# check_security_posture_structure`. Each invariant ID is referenced
# in the QA repair-plan when violated, so adding/removing/renaming
# an entry here changes the QA gate semantics.
# Categories (must match the checks implemented in
# `validators/qa_checks.py check_security_posture_structure`):
# A = Figure 1 architecture stack (Data bottom / sink-band rules)
# D-SVG = Figure 2 PRIMARY form: a portable hand-built SVG image
# (`renderers/figure2_svg.py`) — file exists + glyph parity via `data-glyphs`;
# version 2 also validates scenario data, visible references and geometry
# T = Top Threats table below Figure 2 (glyph parity, links)
# D/E/C/F/G = Figure 2 FALLBACK form only (inline ELK Mermaid heatmap,
# used when the SVG builder yields nothing). 2026-07: Figure 2
# moved from inline ELK Mermaid to a portable SVG because common
# Markdown viewers (GitHub, VS Code preview) lack the ELK layout
# engine and collapse the 3-column heatmap. These Mermaid-markup
# invariants now guard the fallback path, not the shipped output.
# 2026-05: the legacy per-attack-class BULLET list (former N/B/L invariants)
# is no longer rendered — it was replaced by the Top Threats table (T-rules).
# The `**Threat actors.**` intro + actor legend above the table is
# deterministic renderer output (`_build_security_posture_actor_legend`) and
# is intentionally NOT gated here.
invariants:
# A — Figure 1 architecture diagram (optional, deterministic). Data is a
# bottom sink band, not a right-hand peer column.
- { id: A1, rule: "Figure 1, when present, uses `flowchart TB` so architecture tiers stack vertically" }
- { id: A2, rule: "Figure 1 subgraphs follow `ZONE_ACTORS → CLIENT → APP → DATA`; DATA is emitted last as the bottom tier" }
- { id: A3, rule: "Figure 1 has no solid red attack edge (`==>`) targeting a DATA node; attacks reach data only through propagation" }
- { id: A4, rule: "Figure 1 has no visible actor→DATA edge; DATA reachability is downstream from Client/Application only" }
- { id: A5, rule: "When Figure 1 has APP and DATA nodes, APP nodes connect to DATA by visible App→Data edges or `~~~` balancing edges so DATA stays centered below the app row" }
# D-SVG — Figure 2 PRIMARY form (portable SVG image). When the section
# references ``, only D-SVG + T apply;
# the Mermaid D/E/C/F/G rules below guard the fallback path only.
- { id: D-SVG, rule: "when Figure 2 is an SVG image, the referenced `<stem>.figure2.svg` file exists next to the md, and its `data-glyphs` attribute matches the Top Threats table row glyphs (emitted as `T2/G3`); version 2 also satisfies `schemas/figure2-diagram.schema.json` and retains visible route numbers, finding and weakness references, and text inside its cards" }
# D/E/C/F/G/T — Figure 2 FALLBACK heatmap (inline ELK Mermaid) + table.
- { id: D1, rule: "mermaid block declares ELK renderer via `%%{init: {\"flowchart\": {\"defaultRenderer\": \"elk\"}} }%%` directive" }
- { id: D2, rule: "mermaid block opens with `flowchart LR`" }
- { id: D3, rule: "exactly 3 top-level subgraphs in order: ACTORS, TIERS, IMPACT" }
- { id: D4, rule: "subgraph titles are intentionally empty (`[\" \"]`) — column headers are emitted as the FIRST node of each subgraph (HDR_A, HDR_T, HDR_I)" }
- { id: D5, rule: "every subgraph carries `direction TB`" }
- { id: D6, rule: "no nested sub-subgraphs (flat one-level subgraphs only)" }
- { id: E1, rule: "exactly 3 invisible alignment edges: HDR_A --- HDR_T, HDR_T --- HDR_I, plus per-component alignment as needed" }
- { id: E2, rule: "1–N attack arrows present (N = `glyph_sequence` length in attack-class-taxonomy.yaml), all `==>` solid red, with numbered glyph labels in declaration order without gaps" }
- { id: E3, rule: "1–6 consequence arrows present, all `-.->` dashed grey, terminating at IMPACT nodes" }
- { id: E4, rule: "linkStyle declarations exist for: alignment edges (transparent), attack arrows (#b71c1c), consequence arrows (#6b7280 dashed)" }
- { id: C1, rule: "every node label has ≤ 6 `<br/>` tags (≤ 7 visual lines)" }
- { id: C2, rule: "tier node labels include: bold tier name, root-causes line prefixed with ⚠, components line, severity-counts line (🔴 / 🟠 / 🟡 / ⚠ — Low explicitly excluded)" }
- { id: C3, rule: "HTML emphasis tags <b> and <i> ARE allowed in node labels (contract v2 reverses contract v1's C3)" }
- { id: F1, rule: "ACTORS column has 1–6 cards: 1 column header (HDR_A) + 1–5 actor cards" }
- { id: F2, rule: "TIERS column has 1–4 cards: 1 column header (HDR_T) + 1–3 tier cards (Client / Application / Data)" }
- { id: F3, rule: "IMPACT column has 1–5 cards: 1 column header (HDR_I) + 1–4 business-impact cards" }
- { id: G1, rule: "each attack glyph appears at most once on attack-arrow labels" }
- { id: G2, rule: "glyph order on attack arrows is ① ② … without gaps" }
# T — Top Threats table below the diagram (replaced the legacy bullet list).
- { id: T1, rule: "the Top Threats table header `| # | Threat Description | Findings (→ Component) | Risk & Impact | Fix |` is present below the diagram" }
- { id: T2, rule: "diagram ↔ table glyph parity: every heatmap attack-arrow glyph appears exactly once as a Top Threats table-row `#` glyph, and vice-versa (emitted as `T2/G3`)" }
- { id: T3, rule: "the Top Threats table links findings into §8 via `[F-NNN](#f-nnn)`" }
# 2026-05 — `top_findings` and `architecture_assessment` were MERGED into
# the single `top_threats` section below (one threat-grouped table that
# carries the per-finding ranking AND the cross-cutting architectural
# weakness, fully linked). Their renderers (`_render_top_findings`,
# `_render_architecture_assessment`) remain in compose for reference but are
# no longer wired into the Management Summary.
# Top Threats — the merged threat-grouped table (replaced Top Findings +
# Architecture Assessment). 2026-05: FOLDED into `security_posture_at_a_glance`
# so the attack-route overview (Figure 2) and the table render under one
# "### Security Posture & Top Threats" heading. The renderer
# (`_compute_top_threats_rows` + `templates/fragments/top-threats.md.j2`,
# invoked from `_render_security_posture_at_a_glance`) joins the attack-paths
# fragment (class taxonomy → glyph, generic description, curated finding
# membership, impacts) with the deterministic threat/mitigation/component
# data. Every cell except the one-line class description is derived.
# Remediation Priorities — what the team fixes first. One row per mitigation,
# grouped under the top root causes; the root-cause cell spans its group's
# rows (raw HTML for `rowspan`) and carries the weakness's structural fix as
# the lasting fix. Every Critical-fixing mitigation is listed; one without a
# root cause lands in a trailing "No root cause linked" group. Selection and
# caps live in compose `_render_mitigations`; overflow is counted in the
# footer and listed in §10 Mitigation Register.
mitigations:
heading: "### Remediation Priorities"
heading_numbered: false
fragment_type: computed # derived from threat-model.yaml weaknesses[] + mitigations[]
table:
columns:
- { key: root_cause, header: "Root cause → lasting fix", render: weakness_id, required: true }
- { key: do_first, header: "Do first", render: mitigation_id, required: true }
- { key: resolves, header: "Resolves", render: finding_list, required: true }
- { key: effort, header: "Effort", render: effort_badge, required: true, enum: [Low, Medium, High] }
operational_strengths:
heading: "### Operational Strengths"
heading_numbered: false
fragment_type: computed # derived from yaml security_controls[]
filter: "effectiveness in [adequate, partial, weak] AND show_in_strengths_by_default"
sort: [effectiveness_rank:asc, cluster_definition_order:asc]
# M3.10 — categorical cluster layout. Fine-grained controls are
# grouped via data/strength-clusters.yaml into broad strength
# clusters (Hardened HTTP Stack, Authentication & Session Management,
# …). Each row aggregates its constituent controls' implementations.
# The legacy 5-column schema (Architectural Control / Implementation /
# Effectiveness / Gap / Mitigates) is retired — the new schema below
# is the contract.
table:
columns:
- { key: label, header: "Strength", render: bold_text, required: true }
- { key: what_in_place, header: "What's in Place", render: plain_text, required: true }
- { key: effectiveness, header: "Effectiveness", render: effectiveness_badge, required: true }
- { key: gap, header: "Gap", render: plain_text, required: false }
- { key: mitigates, header: "Mitigates", render: finding_list, required: false }
rows:
min: 1
max: 8
truncation_footnote: "_+{overflow_count} additional control(s) catalogued in [Section 6](#6-security-architecture)._"
intro_conditional:
condition: "verdict_severity in [yellow, red]"
template: "Despite the {{ intentionally_vulnerable_or_deficient }} design, the project implements several security-relevant controls. None fully mitigate a Critical finding, but each narrows part of the attack surface. This table is a filtered view of [Section 6](#6-security-architecture) — rows with effectiveness ≥ Weak."
footer_template: "**Bottom line:** {{ bottom_line }}"
# -------------------------------------------------------------------------
# C. Numbered body sections — prose-heavy, fragment-as-markdown
# -------------------------------------------------------------------------
system_overview:
heading: "## 1. System Overview"
heading_numbered: true
fragment_type: markdown
fragment: "system-overview.md"
# No schema — pure prose. Renderer validates that first line matches heading.
# Canonical actor inventory with individual access, authority, finding links,
# and represented scenario groups; legacy models retain their vector table.
# This computed section needs no runtime sidecar or authored fragment.
identified_actors:
heading: "### Identified Actors"
heading_numbered: false # unnumbered ### subsection of §1 System Overview (like "### Scope")
fragment_type: computed
# No fragment file or required subsections.
trust_boundary_catalog:
heading: "### Trust Boundaries"
heading_numbered: false
fragment_type: computed
# Compact crossing / required condition / findings-or-question catalogue.
# Detailed provenance remains in canonical YAML; control sections do not
# duplicate inferred dependencies from CWE adjacency.
# Canonical human catalogue folded under §1. The deterministic renderer
# caps it at 20 rows and points overflow to YAML/query output. A boundary a
# finding references is always rendered, so every `#tb-N` link on a finding
# card resolves; the cap bounds the unreferenced rows only.
architecture_diagrams:
heading: "## 2. Architecture Diagrams"
heading_numbered: true
fragment_type: markdown
fragment: "architecture-diagrams.md"
# Reference layout has exactly these three subsections, in this order.
# "Security Architecture Assessment" was removed — its content is now in §6.
# "Technology Architecture" was removed — §2.2 names every runtime,
# framework and version the scan found.
# "Data Flow Matrix" and "Trust Boundaries" are NOT canonical §2 sub-sections.
required_subsections:
- { level: 3, title: "2.1 System Context" }
- { level: 3, title: "2.2 Container Architecture" }
- { level: 3, title: "2.3 Components" }
# These §2.x sub-section titles are forbidden — their content lives in §6.
forbidden_subsection_patterns:
- "^2\\.4 Technology Architecture"
- "^2\\.5 Security Architecture Assessment"
- "^2\\.\\d+ Data Flow Matrix"
- "^2\\.\\d+ Trust Boundaries"
required_patterns:
# At least one diagram: a Mermaid block, or a §2 detail figure (deployment
# in §2.2 when several deployment units run) that replaces that
# subsection's Mermaid diagram. §2.2 with one deployment unit and §2.3
# show a generator-owned `<!-- detail-table -->` table instead; §2.1
# always keeps its diagram.
- '```mermaid|!\[Figure \d+ - [^\]]+\]\((?:[^)\s]+\.figure\d+\.svg|figure\d+\.svg|data:image/svg\+xml;base64,[A-Za-z0-9+/=]+)\)'
# ---------------------------------------------------------------------
# Diagram compactness — Single-Source-of-Truth (post-2026-05).
# ---------------------------------------------------------------------
# The §2 Mermaid diagrams in earlier runs grew wide and unreadable: 5
# subgraphs side-by-side, 10+ nodes, 80–120-char labels carrying tech
# name + version + defect-list. That extra detail belongs in the §2
# tables and in §6/§8 — not in the diagram label. The rules below pin a compact
# 4-tier layout (orientation: top-down) and are consumed by:
#
# * scripts/renderers/pregenerate_fragments.py — generates scaffolds that
# obey the limits by construction.
# * scripts/renderers/compose_threat_model.py — when `skip_phase11_enrichment`
# is true, the Phase-11 LLM is forbidden from re-authoring the
# listed sub-sections (Pre-Gen output is canonical).
# * scripts/validators/qa_checks.py — `check_diagram_compactness` parses every
# Mermaid block in §2, counts subgraphs / nodes / label-lines,
# verifies the required classDef block is present, and runs the
# threat-traceability cross-check. Violations raise a repair-plan
# action that the Re-Render Loop consumes.
#
# The contract is the SoT — never duplicate a limit in code. If a limit
# needs to change, edit it here.
diagram_compactness:
# Fix (1): §2.2 Container Architecture was previously unvalidated,
# leading to minimal 3-box diagrams with no port labels and no auth
# middleware named as a distinct unit. These rules ensure the diagram
# carries protocol+port edge labels and names the auth middleware box
# when one exists (detected via the presence of express-jwt / similar
# tokens in the recon summary).
"2.2 Container Architecture":
layout_keyword: "flowchart TB"
max_subgraphs: 4
max_nodes_total: 8
max_label_lines: 3
max_label_chars_per_line: 60
# Every edge in §2.2 must carry a label that names the protocol.
# A bare `-->` without a label is forbidden — it hides whether
# traffic is HTTP, HTTPS, an ORM call, a file I/O path, etc.
require_edge_labels: true
# §2.2 is generator-owned like §2.3. A renderer previously
# appended per-container threat rows and exceeded this eight-node cap
# on the 2026-07-13 Juice Shop standard run. Protocol labels and
# middleware detail must be derived by the generator, never added by
# the Phase-11 LLM.
skip_phase11_enrichment: true
"2.3 Components":
# Mandatory mermaid declaration line — `graph LR` is forbidden
# because horizontal layout pushes the diagram beyond viewport
# width on every viewer we tested.
layout_keyword: "flowchart TD"
max_subgraphs: 4
max_nodes_total: 8
max_label_lines: 3
max_label_chars_per_line: 60
# The four canonical tier-subgraphs. This block is the SoT for the
# titles — the pre-generator reads them from here; qa_checks rejects
# deviation on the ids.
#
# CLIENT is titled "Untrusted Zone - …" for the same reason EXT is:
# a browser SPA executes on the user's device, on the attacker's side
# of every server-side control, so it is NOT a trust zone of its own.
# Figure 1 draws the client device INSIDE the untrusted Internet zone
# (`renderers/figure1_dfd.py`; the `renderers/figure1_svg.py::_zone_rank` fallback collapses
# actors + client the same way), so §2.3 must not contradict that. Four zone-looking columns that marked
# nothing invited the reader to place the boundary between EXT and
# CLIENT, where trust does not change — the parallel titles say the
# untrusted zone spans both, and the boundary-crossing edges below
# mark where it actually does change (CLIENT/EXT → APP).
required_subgraphs:
- { id: "EXT", title: "Untrusted Zone - Internet" }
- { id: "CLIENT", title: "Untrusted Zone - Browser Client" }
- { id: "APP", title: "Application Tier" }
- { id: "DATA", title: "Data Tier" }
# Audit actor convention — verbatim color values. The classDef block
# MUST appear at the bottom of the mermaid block (after edges).
required_classdefs:
legit: "fill:#e8f1ea,stroke:#2e7d32,color:#1b5e20,stroke-width:1.5px"
threat: "fill:#f3dada,stroke:#b71c1c,color:#7f0000,stroke-width:2px"
external: "fill:#f2f2f2,stroke:#424242,color:#212121,stroke-width:1.5px"
risk: "fill:#fef2f2,stroke:#991b1b,color:#111,stroke-width:2.5px"
# Edge convention — inverted from the heatmap (which uses solid =
# attack, dashed = consequence). In §2.3 the question is "who
# talks legitimately to whom?", so legit data flow is the visual
# default (solid green) and intrusion vectors are the deviation
# (dashed red).
#
# `boundary_crossing` is the third convention: an edge that leaves the
# untrusted zone and enters the application/data tier, i.e. the place
# where trust actually changes. `==>` is the arrow the §2 legend
# already defines as "crosses an untrusted trust boundary
# (security-critical)" — this reuses it instead of inventing a fourth
# glyph, and it replaces the legit/attack arrow on that one edge so
# the crossing is visible AS a crossing without spending a subgraph
# (all four subgraph slots above are taken).
#
# Drawn ONLY from resolved `trust_boundaries[]` in threat-model.yaml
# (never inferred): the edge label gains a `trust boundary · tb-N`
# line naming the boundary ids, and a caption under the diagram
# resolves them into §1. A model with no resolved boundary keeps the
# legit/attack arrows unchanged. Note which edges qualify: EXT → CLIENT
# does NOT (both sides are untrusted), CLIENT → APP does. Only the exit
# from the untrusted zone is marked — the legend reads `==>` as
# "crosses an UNTRUSTED trust boundary", so an internal APP → DATA
# enforcement point stays a plain legit edge and is carried by §1/§2.2.
edge_convention:
legit: { arrow: "-->", linkstyle: "stroke:#2e7d32,stroke-width:1.5px" }
attack: { arrow: "-.->", linkstyle: "stroke:#b71c1c,stroke-width:2.5px,stroke-dasharray:6 4" }
# Amber, not the attack red: a crossing edge replaces the legit OR the
# attack arrow, so it must not paint a legitimate request as an
# intrusion (nor an intrusion as legitimate). Shape carries "this is
# the crossing", the node class and the edge label still carry who is
# sending it. Amber is this document's "caution / High" hue.
boundary_crossing: { arrow: "==>", linkstyle: "stroke:#ef6c00,stroke-width:3px" }
# When a tier holds ≥2 components, render the first as the main
# node and append the rest as `<br/>+ C-NN <name>` bullets in the
# same node label. Keeps the diagram at ≤4 subgraphs even for
# microservice-style decompositions.
sub_component_aggregation: "bullet_list_in_parent_label"
# Bidirectional traceability — every T-NNN cited in a node label
# or edge label MUST resolve to a §8 Threat Register entry; every
# Critical/High threat from §8 MUST appear EITHER in this diagram
# (node or edge label) OR in the §2.3 component table. This
# closes the "diagram simplification dropped a Critical finding"
# gap.
require_threat_traceability: true
# The Phase-11 enrichment LLM has historically rewritten this
# block into 5-subgraph / 10-node forms. Lock the Pre-Gen output
# as canonical for §2.3.
skip_phase11_enrichment: true
attack_walkthroughs:
heading: "## 3. Attack Walkthroughs"
heading_numbered: true
fragment_type: markdown
fragment: "attack-walkthroughs.md"
# §3 is a flat list of per-Critical walkthroughs: `### 3.1`, `### 3.2`, …
# one numbered sub-section per Critical finding, each with a
# `sequenceDiagram` showing `alt Current state` / `else After mitigation`.
#
# The retired §3.1 "Attack Chain Overview" cross-finding view (graph LR
# kill-chains) has been removed — the cross-finding/strategic picture now
# lives solely in the `## Critical Findings by Root Cause` section above §1, so the
# plugin no longer narrates attack paths in two competing places.
#
# No specific sub-section is mandated by title here; the per-Critical
# coverage is enforced by `per_critical_subsection` + check_walkthrough_coverage.
required_subsections: []
# required_patterns applies only when §3 actually carries per-Critical
# walkthroughs. renderers/walkthrough_renderer.py emits one `sequenceDiagram` block
# per effective Critical finding (none for effective Highs), so the
# diagram is present iff walkthroughs are NOT skipped AND ≥1 Critical exists.
# A `--quick` run (skip notice, no diagrams) OR a clean report with zero
# Criticals (intro-only stub) must NOT trip the missing-pattern check. The
# contract grammar has no `and`/numeric ops, so this is gated on the
# precomputed `has_authored_walkthroughs` flag (renderers/compose_threat_model.py
# eval_context). per_critical coverage is enforced separately by
# qa_checks.check_walkthrough_coverage, which independently no-ops on zero
# Criticals.
required_patterns:
- "sequenceDiagram" # per-Critical walkthrough
required_patterns_condition: "has_authored_walkthroughs"
# §3 is CAPPED at the top-N highest-priority effective Criticals — see
# walkthrough_renderer.DEFAULT_MAX_WALKTHROUGHS (currently 8, ceiling 10).
# Selection is risk- and chain-ranked, reserves Access Control and
# evidence-classified LLM Abuse when present, then takes one finding per
# primary threat category before repeating a category. This stops §3 from
# exploding into near-identical walkthroughs while overflow Criticals keep
# their full §8 row. check_walkthrough_coverage enforces the same CAPPED
# selection by reusing walkthrough_renderer.select_walkthrough_picks.
per_critical_subsection: true # §3.2+ = one per walked-through Critical
per_critical_subsection_condition: "has_authored_walkthroughs"
# ---------------------------------------------------------------------
# Walkthrough quality — guard against trivial-stub renderings (added
# 2026-05 after a Stage-2 re-render produced 3 stub chains of the form
# `attacker --> threat --> "Privileged access"`, defeating the point of
# §3). Consumed by validators/qa_checks.py:
#
# * check_walkthrough_coverage — one §3.x sub-section per Critical
# threat in `threat-model.yaml`. When N Criticals exist, exactly
# N H3 blocks must appear in §3 (excluding §3.1). Repair plan
# enumerates each missing T-NNN by id + title.
#
# * check_walkthrough_depth — per §3.x walkthrough body:
# - sequenceDiagram block MUST carry `alt Current state` and
# `else After mitigation` (or the legacy `else After M-NNN`)
# when `require_alt_else_block` is true.
# - body line count (between the `### 3.x` heading and the next
# H3 / H2) must be ≥ `min_body_lines`. Stub renderings score
# < 10 lines; a real walkthrough is 25+.
# ---------------------------------------------------------------------
walkthrough_depth:
# Per §3.x walkthrough.
#
# 2026-05 iteration 3: shrunk back to ~5 lines after reader feedback
# that the 70-100 line walkthroughs drowned the actual attack story
# in boilerplate (Detection Signals / Cross-references / Prereqs were
# mostly padding). Walkthroughs are now short and concise: a brief
# intro, the attack steps, and a focused sequence diagram. Detection
# Signals / Business Impact / Cross-references are dropped (§8 row
# already carries the same data).
min_body_lines: 5
require_alt_else_block: false
# Per-§3.x section structure — every walkthrough MUST carry these
# labelled-form blocks in this order. Consumed by
# `validators/qa_checks.py:check_walkthrough_structure`.
#
# 2026-05 iteration 3: reduced from 8 → 3. Attacker Profile (folded
# into the intro sentence), Prerequisites (mostly boilerplate),
# Business Impact / Detection Signals (already covered in §8 row),
# and Cross-references (the §8 Threat Register row holds them) are
# all dropped.
required_labelled_sections:
- "Attack Steps"
- "Sequence Diagram"
- "Defense in Depth"
# The labelled-form sections must be **bold-headed** (`**Title**`),
# not `####` H4 headings.
labelled_section_format: bold
# Renderer must replace every `<!-- WALKTHROUGH_FILL: ... -->` placeholder
# before composing. A surviving placeholder is a contract violation.
forbidden_placeholders:
- "WALKTHROUGH_FILL"
per_critical_subsection_condition: "has_authored_walkthroughs"
# (The §3.1 Attack Chain compactness block was removed with the §3.1
# Attack Chain Overview. The cross-finding view is the `## Critical
# Attack Tree`; §3 is now per-Critical walkthroughs only.)
assets:
heading: "## 4. Assets"
heading_numbered: true
fragment_type: markdown
fragment: "assets.md"
required_patterns:
- "\\| Asset \\|" # must contain an Asset table
attack_surface:
heading: "## 5. Attack Surface"
heading_numbered: true
fragment_type: markdown
fragment: "attack-surface.md"
required_subsections:
- { level: 3, title_pattern: "^5\\.1 Unauthenticated Entry Points" }
- { level: 3, title_pattern: "^5\\.2 Authenticated Entry Points" }
# §6 Use Cases removed (2026-05). The numbering gap (§5 → §6) is
# intentional. Re-introduction would require: (1) re-adding this section
# block, (2) restoring `use_cases` to all `order:` lists above,
# (3) reinstating `gen_use_cases` in scripts/renderers/pregenerate_fragments.py,
# (4) restoring `use_cases` in scripts/renderers/compose_threat_model.py
# FRAGMENT_PATHS / dispatcher / has_use_cases condition.
skipped_sections_placeholder:
heading: "" # no heading; the body is a single italic line.
heading_numbered: false
fragment_type: computed
# Renderer: _render_skipped_sections_placeholder.
# Active only when §6 is suppressed (quick depth without a rich prior
# §6 to preserve). Emits one italic explanation line so the §5 -> §8
# numbering jump is visible to the reader as intentional.
security_architecture:
heading: "## 6. Security Architecture"
heading_numbered: true
fragment_type: markdown
fragment: "security-architecture.md"
# M6 (2026-05) — The v1 15-section layout (7.1 Overview, 7.2 Key
# Architectural Risks, 7.3 Identity & Access Management, …) was retired
# when schema_v2 became the single source of truth for §6. Both
# `renderers/compose_threat_model.py:_section_required_subs` and
# `validators/qa_checks.py:_resolve_section_overlay` already prefer
# `schema_v2.required_subsections` over a section-level
# `required_subsections` when both exist (see the explicit overlay at
# renderers/compose_threat_model.py:1427-1431 and validators/qa_checks.py:287-289). Removing
# the top-level v1 list ensures there is exactly one authoritative
# subsection set — drift between the two was the 2026-05 failure mode
# that allowed legacy fragments to claim contract pass while the
# rendered §6 used v2 anchors.
# §6 clarity guards — enforced by dedicated runtime checks in
# scripts/validators/qa_checks.py rather than as YAML patterns, because the
# generic contract pattern matcher does not currently scan the
# rendered §6 body. The relevant checks (added 2026-05) are:
#
# * `section7_narrative_placeholders` (M2) — fails when any
# `<!-- NARRATIVE_PLACEHOLDER ... -->` token survives in §6. The
# Stage-3 `gate` enforces it only when enrich_arch_fragments is on;
# without enrichment the scaffold is the delivered §6.
# * `section7_h4_positive_intro` (M7) — fails when an H4 control
# block lacks a ≥25-word positive intro paragraph before