guardlink 1.4.2 → 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/CHANGELOG.md +83 -9
  2. package/README.md +38 -1
  3. package/dist/agents/config.d.ts +7 -0
  4. package/dist/agents/config.d.ts.map +1 -1
  5. package/dist/agents/config.js.map +1 -1
  6. package/dist/agents/index.d.ts +1 -1
  7. package/dist/agents/index.d.ts.map +1 -1
  8. package/dist/agents/index.js +1 -1
  9. package/dist/agents/index.js.map +1 -1
  10. package/dist/agents/prompts.d.ts +14 -0
  11. package/dist/agents/prompts.d.ts.map +1 -1
  12. package/dist/agents/prompts.js +445 -2
  13. package/dist/agents/prompts.js.map +1 -1
  14. package/dist/analyze/format.d.ts +72 -0
  15. package/dist/analyze/format.d.ts.map +1 -0
  16. package/dist/analyze/format.js +176 -0
  17. package/dist/analyze/format.js.map +1 -0
  18. package/dist/analyze/index.d.ts +76 -0
  19. package/dist/analyze/index.d.ts.map +1 -1
  20. package/dist/analyze/index.js +165 -2
  21. package/dist/analyze/index.js.map +1 -1
  22. package/dist/analyze/prompts.d.ts +3 -2
  23. package/dist/analyze/prompts.d.ts.map +1 -1
  24. package/dist/analyze/prompts.js +16 -2
  25. package/dist/analyze/prompts.js.map +1 -1
  26. package/dist/analyzer/sarif.d.ts +3 -2
  27. package/dist/analyzer/sarif.d.ts.map +1 -1
  28. package/dist/analyzer/sarif.js +29 -3
  29. package/dist/analyzer/sarif.js.map +1 -1
  30. package/dist/cli/index.d.ts +2 -0
  31. package/dist/cli/index.d.ts.map +1 -1
  32. package/dist/cli/index.js +380 -28
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/dashboard/data.d.ts +11 -0
  35. package/dist/dashboard/data.d.ts.map +1 -1
  36. package/dist/dashboard/data.js +12 -0
  37. package/dist/dashboard/data.js.map +1 -1
  38. package/dist/dashboard/diagrams.d.ts +81 -12
  39. package/dist/dashboard/diagrams.d.ts.map +1 -1
  40. package/dist/dashboard/diagrams.js +750 -362
  41. package/dist/dashboard/diagrams.js.map +1 -1
  42. package/dist/dashboard/generate.d.ts +5 -2
  43. package/dist/dashboard/generate.d.ts.map +1 -1
  44. package/dist/dashboard/generate.js +2516 -244
  45. package/dist/dashboard/generate.js.map +1 -1
  46. package/dist/diff/engine.d.ts +2 -1
  47. package/dist/diff/engine.d.ts.map +1 -1
  48. package/dist/diff/engine.js +3 -2
  49. package/dist/diff/engine.js.map +1 -1
  50. package/dist/init/index.d.ts.map +1 -1
  51. package/dist/init/index.js +24 -5
  52. package/dist/init/index.js.map +1 -1
  53. package/dist/init/migrate.d.ts +39 -0
  54. package/dist/init/migrate.d.ts.map +1 -0
  55. package/dist/init/migrate.js +45 -0
  56. package/dist/init/migrate.js.map +1 -0
  57. package/dist/init/templates.d.ts +8 -0
  58. package/dist/init/templates.d.ts.map +1 -1
  59. package/dist/init/templates.js +71 -9
  60. package/dist/init/templates.js.map +1 -1
  61. package/dist/mcp/lookup.d.ts +1 -0
  62. package/dist/mcp/lookup.d.ts.map +1 -1
  63. package/dist/mcp/lookup.js +138 -10
  64. package/dist/mcp/lookup.js.map +1 -1
  65. package/dist/mcp/server.d.ts +2 -1
  66. package/dist/mcp/server.d.ts.map +1 -1
  67. package/dist/mcp/server.js +20 -8
  68. package/dist/mcp/server.js.map +1 -1
  69. package/dist/parser/clear.js +1 -1
  70. package/dist/parser/clear.js.map +1 -1
  71. package/dist/parser/feature-filter.d.ts +42 -0
  72. package/dist/parser/feature-filter.d.ts.map +1 -0
  73. package/dist/parser/feature-filter.js +109 -0
  74. package/dist/parser/feature-filter.js.map +1 -0
  75. package/dist/parser/format.d.ts +24 -0
  76. package/dist/parser/format.d.ts.map +1 -0
  77. package/dist/parser/format.js +29 -0
  78. package/dist/parser/format.js.map +1 -0
  79. package/dist/parser/index.d.ts +2 -0
  80. package/dist/parser/index.d.ts.map +1 -1
  81. package/dist/parser/index.js +1 -0
  82. package/dist/parser/index.js.map +1 -1
  83. package/dist/parser/parse-file.d.ts.map +1 -1
  84. package/dist/parser/parse-file.js +3 -1
  85. package/dist/parser/parse-file.js.map +1 -1
  86. package/dist/parser/parse-line.d.ts +3 -0
  87. package/dist/parser/parse-line.d.ts.map +1 -1
  88. package/dist/parser/parse-line.js +78 -22
  89. package/dist/parser/parse-line.js.map +1 -1
  90. package/dist/parser/parse-project.js +19 -0
  91. package/dist/parser/parse-project.js.map +1 -1
  92. package/dist/parser/validate.d.ts +3 -0
  93. package/dist/parser/validate.d.ts.map +1 -1
  94. package/dist/parser/validate.js +7 -0
  95. package/dist/parser/validate.js.map +1 -1
  96. package/dist/report/index.d.ts +1 -0
  97. package/dist/report/index.d.ts.map +1 -1
  98. package/dist/report/index.js +1 -0
  99. package/dist/report/index.js.map +1 -1
  100. package/dist/report/report.d.ts.map +1 -1
  101. package/dist/report/report.js +924 -24
  102. package/dist/report/report.js.map +1 -1
  103. package/dist/report/sequence.d.ts +11 -0
  104. package/dist/report/sequence.d.ts.map +1 -0
  105. package/dist/report/sequence.js +140 -0
  106. package/dist/report/sequence.js.map +1 -0
  107. package/dist/tui/commands.d.ts +1 -0
  108. package/dist/tui/commands.d.ts.map +1 -1
  109. package/dist/tui/commands.js +83 -4
  110. package/dist/tui/commands.js.map +1 -1
  111. package/dist/tui/index.d.ts.map +1 -1
  112. package/dist/tui/index.js +7 -2
  113. package/dist/tui/index.js.map +1 -1
  114. package/dist/types/index.d.ts +57 -3
  115. package/dist/types/index.d.ts.map +1 -1
  116. package/dist/workspace/merge.d.ts.map +1 -1
  117. package/dist/workspace/merge.js +6 -2
  118. package/dist/workspace/merge.js.map +1 -1
  119. package/package.json +1 -1
@@ -9,17 +9,10 @@
9
9
  * @flows #report -> Markdown via return -- "Report output"
10
10
  */
11
11
  import { generateMermaid } from './mermaid.js';
12
+ import { generateSequenceDiagram } from './sequence.js';
12
13
  export function generateReport(model) {
13
14
  const lines = [];
14
- // ── Header ──
15
- lines.push(`# Threat Model Report — ${model.project}`);
16
- lines.push('');
17
- lines.push(`> Generated: ${model.generated_at} `);
18
- lines.push(`> Files scanned: ${model.source_files} | Annotations: ${model.annotations_parsed}`);
19
- lines.push('');
20
- // ── Executive Summary ──
21
- lines.push('## Executive Summary');
22
- lines.push('');
15
+ // ── Pre-compute shared data ──
23
16
  const mitigatedPairs = new Set();
24
17
  const acceptedPairs = new Set();
25
18
  for (const m of model.mitigations)
@@ -31,6 +24,91 @@ export function generateReport(model) {
31
24
  return !mitigatedPairs.has(key) && !acceptedPairs.has(key);
32
25
  });
33
26
  const severityCounts = countBySeverity(unmitigated);
27
+ const hasAI = detectAI(model);
28
+ // ── Header ──
29
+ lines.push(`# Threat Model Report — ${model.project}`);
30
+ lines.push('');
31
+ lines.push(`> Generated: ${model.generated_at} `);
32
+ lines.push(`> Files scanned: ${model.source_files} | Annotations: ${model.annotations_parsed}`);
33
+ if (model.metadata?.guardlink_version) {
34
+ lines.push(`> GuardLink version: ${model.metadata.guardlink_version}`);
35
+ }
36
+ if (model.metadata?.commit_sha) {
37
+ lines.push(`> Commit: ${model.metadata.commit_sha}${model.metadata.branch ? ` (${model.metadata.branch})` : ''}`);
38
+ }
39
+ lines.push('');
40
+ // ══════════════════════════════════════════════════════════════════════
41
+ // SECTION 1: Application Overview
42
+ // ══════════════════════════════════════════════════════════════════════
43
+ lines.push('## Application Overview');
44
+ lines.push('');
45
+ emitApplicationOverview(model, unmitigated, severityCounts, hasAI, lines);
46
+ // ══════════════════════════════════════════════════════════════════════
47
+ // SECTION 2: Scope
48
+ // ══════════════════════════════════════════════════════════════════════
49
+ lines.push('## Scope of This Threat Model');
50
+ lines.push('');
51
+ emitScope(model, lines);
52
+ // ══════════════════════════════════════════════════════════════════════
53
+ // SECTION 3: Architecture
54
+ // ══════════════════════════════════════════════════════════════════════
55
+ lines.push('## Architecture');
56
+ lines.push('');
57
+ emitArchitecture(model, lines);
58
+ // ══════════════════════════════════════════════════════════════════════
59
+ // SECTION 4: Key Flows & Sequence
60
+ // ══════════════════════════════════════════════════════════════════════
61
+ if (model.flows.length > 0) {
62
+ lines.push('## Key Flows & Sequence');
63
+ lines.push('');
64
+ emitKeyFlows(model, lines);
65
+ }
66
+ // ══════════════════════════════════════════════════════════════════════
67
+ // SECTION 5: Data Inventory
68
+ // ══════════════════════════════════════════════════════════════════════
69
+ if (model.data_handling.length > 0 || model.assets.length > 0) {
70
+ lines.push('## Data Inventory');
71
+ lines.push('');
72
+ emitDataInventory(model, hasAI, lines);
73
+ }
74
+ // ══════════════════════════════════════════════════════════════════════
75
+ // SECTION 6: Roles & Access
76
+ // ══════════════════════════════════════════════════════════════════════
77
+ lines.push('## Roles & Access');
78
+ lines.push('');
79
+ emitRolesAccess(model, lines);
80
+ // ══════════════════════════════════════════════════════════════════════
81
+ // SECTION 7: Dependencies
82
+ // ══════════════════════════════════════════════════════════════════════
83
+ lines.push('## Dependencies');
84
+ lines.push('');
85
+ emitDependencies(model, lines);
86
+ // ══════════════════════════════════════════════════════════════════════
87
+ // SECTION 8: Secrets, Keys & Credential Management
88
+ // ══════════════════════════════════════════════════════════════════════
89
+ lines.push('## Secrets, Keys & Credential Management');
90
+ lines.push('');
91
+ emitSecretsManagement(model, lines);
92
+ // ══════════════════════════════════════════════════════════════════════
93
+ // SECTION 9: Logging, Monitoring & Audit
94
+ // ══════════════════════════════════════════════════════════════════════
95
+ lines.push('## Logging, Monitoring & Audit');
96
+ lines.push('');
97
+ emitLoggingAudit(model, lines);
98
+ // ══════════════════════════════════════════════════════════════════════
99
+ // SECTION 10: AI/ML System Details (conditional)
100
+ // ══════════════════════════════════════════════════════════════════════
101
+ if (hasAI) {
102
+ lines.push('## AI/ML System Details');
103
+ lines.push('');
104
+ emitAIDetails(model, lines);
105
+ }
106
+ // ══════════════════════════════════════════════════════════════════════
107
+ // EXISTING SECTIONS: Executive Summary + Findings
108
+ // ══════════════════════════════════════════════════════════════════════
109
+ // ── Executive Summary ──
110
+ lines.push('## Executive Summary');
111
+ lines.push('');
34
112
  lines.push(`| Metric | Count |`);
35
113
  lines.push(`|--------|-------|`);
36
114
  lines.push(`| Assets | ${model.assets.length} |`);
@@ -39,6 +117,8 @@ export function generateReport(model) {
39
117
  lines.push(`| Active mitigations | ${model.mitigations.length} |`);
40
118
  lines.push(`| Accepted risks | ${model.acceptances.length} |`);
41
119
  lines.push(`| **Unmitigated exposures** | **${unmitigated.length}** |`);
120
+ if ((model.confirmed || []).length > 0)
121
+ lines.push(`| **🔴 Confirmed exploitable** | **${model.confirmed.length}** |`);
42
122
  if (severityCounts.critical > 0)
43
123
  lines.push(`| ↳ Critical (P0) | ${severityCounts.critical} |`);
44
124
  if (severityCounts.high > 0)
@@ -64,7 +144,7 @@ export function generateReport(model) {
64
144
  lines.push('');
65
145
  // ── Unmitigated Exposures ──
66
146
  if (unmitigated.length > 0) {
67
- lines.push('## Unmitigated Exposures');
147
+ lines.push('## Unmitigated Exposures');
68
148
  lines.push('');
69
149
  lines.push('These exposures have no matching `@mitigates` or `@accepts` and require attention.');
70
150
  lines.push('');
@@ -78,9 +158,24 @@ export function generateReport(model) {
78
158
  }
79
159
  lines.push('');
80
160
  }
161
+ // ── Confirmed Exploitable ──
162
+ if ((model.confirmed || []).length > 0) {
163
+ lines.push('## 🔴 Confirmed Exploitable');
164
+ lines.push('');
165
+ lines.push('These threats have been verified through testing — **not false positives**. Immediate remediation required.');
166
+ lines.push('');
167
+ lines.push('| Severity | Asset | Threat | Evidence | Location |');
168
+ lines.push('|----------|-------|--------|----------|----------|');
169
+ for (const c of model.confirmed) {
170
+ const sev = severityBadge(c.severity);
171
+ const desc = c.description ? truncate(c.description, 60) : '—';
172
+ lines.push(`| ${sev} | ${c.asset} | ${c.threat} | ${desc} | ${c.location.file}:${c.location.line} |`);
173
+ }
174
+ lines.push('');
175
+ }
81
176
  // ── Accepted Risks ──
82
177
  if (model.acceptances.length > 0) {
83
- lines.push('## Accepted Risks');
178
+ lines.push('## Accepted Risks');
84
179
  lines.push('');
85
180
  lines.push('| Asset | Threat | Rationale | Location |');
86
181
  lines.push('|-------|--------|-----------|----------|');
@@ -92,7 +187,7 @@ export function generateReport(model) {
92
187
  }
93
188
  // ── Active Mitigations ──
94
189
  if (model.mitigations.length > 0) {
95
- lines.push('## 🛡 Active Mitigations');
190
+ lines.push('## Active Mitigations');
96
191
  lines.push('');
97
192
  lines.push('| Asset | Threat | Control | Description | Location |');
98
193
  lines.push('|-------|--------|---------|-------------|----------|');
@@ -105,7 +200,7 @@ export function generateReport(model) {
105
200
  }
106
201
  // ── Trust Boundaries ──
107
202
  if (model.boundaries.length > 0) {
108
- lines.push('## 🔒 Trust Boundaries');
203
+ lines.push('## Trust Boundaries');
109
204
  lines.push('');
110
205
  lines.push('| Side A | Side B | Boundary ID | Description | Location |');
111
206
  lines.push('|--------|--------|-------------|-------------|----------|');
@@ -118,7 +213,7 @@ export function generateReport(model) {
118
213
  }
119
214
  // ── Data Flows ──
120
215
  if (model.flows.length > 0) {
121
- lines.push('## 📊 Data Flows');
216
+ lines.push('## Data Flows');
122
217
  lines.push('');
123
218
  lines.push('| Source | Target | Mechanism | Description |');
124
219
  lines.push('|--------|--------|-----------|-------------|');
@@ -131,7 +226,7 @@ export function generateReport(model) {
131
226
  }
132
227
  // ── Data Handling ──
133
228
  if (model.data_handling.length > 0) {
134
- lines.push('## 📋 Data Classification');
229
+ lines.push('## Data Classification');
135
230
  lines.push('');
136
231
  lines.push('| Asset | Classification | Description |');
137
232
  lines.push('|-------|---------------|-------------|');
@@ -143,7 +238,7 @@ export function generateReport(model) {
143
238
  }
144
239
  // ── Risk Transfers ──
145
240
  if (model.transfers.length > 0) {
146
- lines.push('## 🔀 Risk Transfers');
241
+ lines.push('## Risk Transfers');
147
242
  lines.push('');
148
243
  lines.push('| Source | Threat | Target | Description | Location |');
149
244
  lines.push('|--------|--------|--------|-------------|----------|');
@@ -155,7 +250,7 @@ export function generateReport(model) {
155
250
  }
156
251
  // ── Validations ──
157
252
  if (model.validations.length > 0) {
158
- lines.push('## Validations');
253
+ lines.push('## Validations');
159
254
  lines.push('');
160
255
  lines.push('| Control | Asset | Description | Location |');
161
256
  lines.push('|---------|-------|-------------|----------|');
@@ -167,7 +262,7 @@ export function generateReport(model) {
167
262
  }
168
263
  // ── Ownership ──
169
264
  if (model.ownership.length > 0) {
170
- lines.push('## 👤 Ownership');
265
+ lines.push('## Ownership');
171
266
  lines.push('');
172
267
  for (const o of model.ownership) {
173
268
  const desc = o.description ? ` — ${o.description}` : '';
@@ -177,7 +272,7 @@ export function generateReport(model) {
177
272
  }
178
273
  // ── Audit Items ──
179
274
  if (model.audits.length > 0) {
180
- lines.push('## 🔍 Audit Items');
275
+ lines.push('## Audit Items');
181
276
  lines.push('');
182
277
  for (const a of model.audits) {
183
278
  const desc = a.description || 'Needs review';
@@ -187,7 +282,7 @@ export function generateReport(model) {
187
282
  }
188
283
  // ── Assumptions ──
189
284
  if (model.assumptions.length > 0) {
190
- lines.push('## Assumptions');
285
+ lines.push('## Assumptions');
191
286
  lines.push('');
192
287
  lines.push('These are unverified assumptions that should be periodically reviewed.');
193
288
  lines.push('');
@@ -199,7 +294,7 @@ export function generateReport(model) {
199
294
  }
200
295
  // ── Shielded Regions ──
201
296
  if (model.shields.length > 0) {
202
- lines.push('## 🛡️ Shielded Regions');
297
+ lines.push('## Shielded Regions');
203
298
  lines.push('');
204
299
  lines.push('Code regions where annotations are intentionally suppressed via `@shield`.');
205
300
  lines.push('');
@@ -209,9 +304,31 @@ export function generateReport(model) {
209
304
  }
210
305
  lines.push('');
211
306
  }
307
+ // ── Features ──
308
+ if (model.features.length > 0) {
309
+ const uniqueFeatures = new Map();
310
+ for (const f of model.features) {
311
+ const key = f.feature.toLowerCase();
312
+ if (!uniqueFeatures.has(key)) {
313
+ uniqueFeatures.set(key, { name: f.feature, files: new Set(), description: f.description });
314
+ }
315
+ uniqueFeatures.get(key).files.add(f.location.file);
316
+ }
317
+ lines.push('## Feature Tags');
318
+ lines.push('');
319
+ lines.push('Annotations are tagged with the following features via `@feature`.');
320
+ lines.push('');
321
+ lines.push('| Feature | Files | Description |');
322
+ lines.push('|---------|-------|-------------|');
323
+ for (const [, entry] of [...uniqueFeatures.entries()].sort((a, b) => a[1].name.localeCompare(b[1].name))) {
324
+ const desc = entry.description ? truncate(entry.description, 60) : '—';
325
+ lines.push(`| ${entry.name} | ${entry.files.size} | ${desc} |`);
326
+ }
327
+ lines.push('');
328
+ }
212
329
  // ── Developer Comments ──
213
330
  if (model.comments.length > 0) {
214
- lines.push('## 💬 Developer Comments');
331
+ lines.push('## Developer Comments');
215
332
  lines.push('');
216
333
  lines.push('Security-relevant notes left by developers via `@comment`.');
217
334
  lines.push('');
@@ -223,15 +340,721 @@ export function generateReport(model) {
223
340
  }
224
341
  // ── Footer ──
225
342
  lines.push('---');
226
- lines.push(`*Generated by [GuardLink](https://guardlink.bugb.io) — Security annotations for code.*`);
343
+ lines.push(`*Generated from security annotations on ${model.generated_at}.*`);
227
344
  return lines.join('\n');
228
345
  }
229
- // ─── Helpers ─────────────────────────────────────────────────────────
346
+ // ═══════════════════════════════════════════════════════════════════════
347
+ // New Section Emitters
348
+ // ═══════════════════════════════════════════════════════════════════════
349
+ function emitApplicationOverview(model, unmitigated, severityCounts, hasAI, lines) {
350
+ // If user provided a project description via .guardlink/prompt.md, use it
351
+ if (model.prompt) {
352
+ lines.push(model.prompt);
353
+ lines.push('');
354
+ }
355
+ else {
356
+ // Fallback: derive overview from annotations
357
+ const topLevelGroups = new Map();
358
+ for (const a of model.assets) {
359
+ const group = a.path[0] || 'Unknown';
360
+ if (!topLevelGroups.has(group))
361
+ topLevelGroups.set(group, []);
362
+ topLevelGroups.get(group).push(a.path.slice(1).join('.') || a.path[0]);
363
+ }
364
+ lines.push(`**${model.project}** is composed of **${model.assets.length} assets** across **${model.source_files} source files** ` +
365
+ `with **${model.annotations_parsed} security annotations**.`);
366
+ lines.push('');
367
+ if (topLevelGroups.size > 0) {
368
+ lines.push('**Component groups:**');
369
+ lines.push('');
370
+ for (const [group, members] of topLevelGroups) {
371
+ lines.push(`- **${group}**: ${members.join(', ')}`);
372
+ }
373
+ lines.push('');
374
+ }
375
+ }
376
+ // Risk posture summary — always shown
377
+ const totalExposures = model.exposures.length;
378
+ const mitigatedCount = model.mitigations.length;
379
+ const acceptedCount = model.acceptances.length;
380
+ const coveragePct = totalExposures > 0
381
+ ? Math.round(((mitigatedCount + acceptedCount) / totalExposures) * 100)
382
+ : 100;
383
+ lines.push('**Risk posture at a glance:**');
384
+ lines.push('');
385
+ lines.push(`| Indicator | Value |`);
386
+ lines.push(`|-----------|-------|`);
387
+ lines.push(`| Exposure coverage | ${coveragePct}% addressed (${mitigatedCount} mitigated, ${acceptedCount} accepted) |`);
388
+ lines.push(`| Unmitigated exposures | ${unmitigated.length} (${severityCounts.critical} critical, ${severityCounts.high} high, ${severityCounts.medium} medium, ${severityCounts.low} low) |`);
389
+ lines.push(`| Trust boundaries | ${model.boundaries.length} |`);
390
+ lines.push(`| Data flows tracked | ${model.flows.length} |`);
391
+ if (hasAI)
392
+ lines.push(`| AI/ML components | Yes |`);
393
+ lines.push('');
394
+ }
395
+ function emitScope(model, lines) {
396
+ // Scope intro — summarize what's modeled based on annotations
397
+ const annotatedCount = model.annotated_files.length;
398
+ const totalFiles = model.source_files;
399
+ const assetCount = model.assets.length;
400
+ const threatCount = model.threats.length;
401
+ lines.push(`This threat model covers **${assetCount} assets** and **${threatCount} threat categories** ` +
402
+ `derived from **${model.annotations_parsed} annotations** across **${annotatedCount}** of **${totalFiles}** source files.`);
403
+ lines.push('');
404
+ // What's in scope: annotated files / assets / threat categories
405
+ const threatCategories = [...new Set(model.threats.map(t => t.canonical_name || t.name))];
406
+ const assetNames = model.assets.map(a => a.id ? `\`${a.id}\`` : `\`${a.path.join('.')}\``);
407
+ lines.push('### Assets in Scope');
408
+ lines.push('');
409
+ if (assetNames.length > 0) {
410
+ for (const name of assetNames) {
411
+ const asset = model.assets.find(a => (a.id ? `\`${a.id}\`` : `\`${a.path.join('.')}\``) === name);
412
+ const desc = asset?.description ? ` — ${truncate(asset.description, 80)}` : '';
413
+ lines.push(`- ${name}${desc}`);
414
+ }
415
+ }
416
+ else {
417
+ lines.push('_No explicit assets defined. Consider adding `@asset` definitions._');
418
+ }
419
+ lines.push('');
420
+ lines.push('### Threat Categories Addressed');
421
+ lines.push('');
422
+ if (threatCategories.length > 0) {
423
+ const severityMap = new Map();
424
+ for (const t of model.threats) {
425
+ severityMap.set(t.canonical_name || t.name, t.severity || 'unset');
426
+ }
427
+ for (const cat of threatCategories) {
428
+ const sev = severityMap.get(cat) || 'unset';
429
+ lines.push(`- **${cat}** (${sev})`);
430
+ }
431
+ }
432
+ else {
433
+ lines.push('_No explicit threats defined._');
434
+ }
435
+ lines.push('');
436
+ // Coverage gaps
437
+ lines.push('### Coverage');
438
+ lines.push('');
439
+ const covAnnotated = model.annotated_files.length;
440
+ const covTotal = model.source_files;
441
+ const unannotatedCount = model.unannotated_files.length;
442
+ lines.push(`- **${covAnnotated}** of **${covTotal}** files have security annotations (${covTotal > 0 ? Math.round((covAnnotated / covTotal) * 100) : 0}%)`);
443
+ if (unannotatedCount > 0) {
444
+ lines.push(`- **${unannotatedCount}** files have no annotations`);
445
+ }
446
+ if (model.coverage.unannotated_critical.length > 0) {
447
+ lines.push(`- **${model.coverage.unannotated_critical.length}** unannotated security-critical symbols detected:`);
448
+ for (const sym of model.coverage.unannotated_critical.slice(0, 10)) {
449
+ lines.push(` - \`${sym.name}\` (${sym.kind}) at ${sym.file}:${sym.line}`);
450
+ }
451
+ if (model.coverage.unannotated_critical.length > 10) {
452
+ lines.push(` - ... and ${model.coverage.unannotated_critical.length - 10} more`);
453
+ }
454
+ }
455
+ lines.push('');
456
+ }
457
+ function emitArchitecture(model, lines) {
458
+ // ── Components ──
459
+ lines.push('### Components');
460
+ lines.push('');
461
+ if (model.assets.length > 0) {
462
+ lines.push('| Component | ID | Description | Defined At |');
463
+ lines.push('|-----------|-----|-------------|------------|');
464
+ for (const a of model.assets) {
465
+ const name = a.path.join('.');
466
+ const id = a.id || '—';
467
+ const desc = a.description ? truncate(a.description, 50) : '—';
468
+ lines.push(`| ${name} | ${id} | ${desc} | ${a.location.file}:${a.location.line} |`);
469
+ }
470
+ }
471
+ else {
472
+ lines.push('_No components defined via `@asset`._');
473
+ }
474
+ lines.push('');
475
+ // ── Entrypoints ──
476
+ lines.push('### Entrypoints');
477
+ lines.push('');
478
+ // Build asset name set matching both "#id" and "id" forms
479
+ const assetNames = new Set();
480
+ for (const a of model.assets) {
481
+ const id = a.id || a.path.join('.');
482
+ assetNames.add(id);
483
+ assetNames.add(`#${id}`);
484
+ assetNames.add(a.path.join('.'));
485
+ assetNames.add(`#${a.path.join('.')}`);
486
+ }
487
+ const flowTargets = new Set(model.flows.map(f => f.target));
488
+ const flowSources = new Set(model.flows.map(f => f.source));
489
+ // External sources: flow sources that are NOT defined assets
490
+ const externalSources = new Set();
491
+ for (const src of flowSources) {
492
+ if (!assetNames.has(src))
493
+ externalSources.add(src);
494
+ }
495
+ // Entrypoints: assets that receive flows from external sources
496
+ const entrypoints = new Set();
497
+ for (const f of model.flows) {
498
+ if (externalSources.has(f.source) && assetNames.has(f.target)) {
499
+ entrypoints.add(f.target);
500
+ }
501
+ }
502
+ // Also: assets that appear in exposures but not as flow targets from internal sources
503
+ if (entrypoints.size === 0) {
504
+ // Fallback: assets with exposures are likely entrypoints
505
+ for (const e of model.exposures) {
506
+ if (assetNames.has(e.asset))
507
+ entrypoints.add(e.asset);
508
+ }
509
+ }
510
+ if (entrypoints.size > 0) {
511
+ lines.push('Assets receiving external input:');
512
+ lines.push('');
513
+ for (const ep of entrypoints) {
514
+ const incomingFlows = model.flows.filter(f => f.target === ep && externalSources.has(f.source));
515
+ const mechanisms = incomingFlows.map(f => `${f.source} via ${f.mechanism || 'unspecified'}`).join(', ');
516
+ lines.push(`- **${ep}**${mechanisms ? `: ${mechanisms}` : ''}`);
517
+ }
518
+ }
519
+ else {
520
+ lines.push('_No explicit entrypoints identified. Add `@flows` from external sources to assets._');
521
+ }
522
+ lines.push('');
523
+ // ── Callers (external entities) ──
524
+ if (externalSources.size > 0) {
525
+ lines.push('### External Callers');
526
+ lines.push('');
527
+ for (const src of externalSources) {
528
+ const targets = model.flows.filter(f => f.source === src).map(f => f.target);
529
+ lines.push(`- **${src}** → ${[...new Set(targets)].join(', ')}`);
530
+ }
531
+ lines.push('');
532
+ }
533
+ // ── Architecture Diagram ──
534
+ lines.push('### Architecture Diagram');
535
+ lines.push('');
536
+ lines.push('```mermaid');
537
+ lines.push(generateMermaid(model));
538
+ lines.push('```');
539
+ lines.push('');
540
+ // ── Trust Boundaries / Network Zones ──
541
+ if (model.boundaries.length > 0) {
542
+ lines.push('### Network Zones & Trust Boundaries');
543
+ lines.push('');
544
+ for (const b of model.boundaries) {
545
+ const desc = b.description || b.id || 'Unnamed boundary';
546
+ lines.push(`- **${desc}**: ${shortName(b.asset_a)} ↔ ${shortName(b.asset_b)}`);
547
+ }
548
+ lines.push('');
549
+ }
550
+ // ── Multi-tenancy ──
551
+ lines.push('### Multi-tenancy');
552
+ lines.push('');
553
+ const tenantAnnotations = [
554
+ ...model.comments.filter(c => /tenant|multi.?tenant|isolat/i.test(c.description || '')),
555
+ ...model.assumptions.filter(a => /tenant|multi.?tenant|isolat/i.test(a.description || '')),
556
+ ];
557
+ if (tenantAnnotations.length > 0) {
558
+ for (const a of tenantAnnotations) {
559
+ lines.push(`- ${a.description} (${a.location.file}:${a.location.line})`);
560
+ }
561
+ }
562
+ else {
563
+ lines.push('_No multi-tenancy annotations found. If this is a multi-tenant application, consider adding `@comment` or `@boundary` annotations describing tenant isolation._');
564
+ }
565
+ lines.push('');
566
+ // ── Compliance ──
567
+ lines.push('### Compliance');
568
+ lines.push('');
569
+ const complianceAnnotations = [
570
+ ...model.comments.filter(c => /complian|gdpr|hipaa|soc|pci|iso|fedramp|ccpa/i.test(c.description || '')),
571
+ ...model.assumptions.filter(a => /complian|gdpr|hipaa|soc|pci|iso|fedramp|ccpa/i.test(a.description || '')),
572
+ ];
573
+ const hasPII = model.data_handling.some(h => h.classification === 'pii');
574
+ const hasPHI = model.data_handling.some(h => h.classification === 'phi');
575
+ const hasFinancial = model.data_handling.some(h => h.classification === 'financial');
576
+ if (complianceAnnotations.length > 0) {
577
+ for (const a of complianceAnnotations) {
578
+ lines.push(`- ${a.description} (${a.location.file}:${a.location.line})`);
579
+ }
580
+ }
581
+ if (hasPII)
582
+ lines.push('- Handles **PII** — consider GDPR, CCPA compliance requirements');
583
+ if (hasPHI)
584
+ lines.push('- Handles **PHI** — consider HIPAA compliance requirements');
585
+ if (hasFinancial)
586
+ lines.push('- Handles **Financial data** — consider PCI-DSS compliance requirements');
587
+ if (complianceAnnotations.length === 0 && !hasPII && !hasPHI && !hasFinancial) {
588
+ lines.push('_No compliance-related annotations found._');
589
+ }
590
+ lines.push('');
591
+ }
592
+ function emitKeyFlows(model, lines) {
593
+ // Group flows into chains (sequences of connected flows)
594
+ const chains = buildFlowChains(model.flows);
595
+ // Emit sequence diagram
596
+ lines.push('### Sequence Diagram');
597
+ lines.push('');
598
+ lines.push('```mermaid');
599
+ lines.push(generateSequenceDiagram(model));
600
+ lines.push('```');
601
+ lines.push('');
602
+ // Emit step-by-step for each chain
603
+ lines.push('### Flow Details');
604
+ lines.push('');
605
+ let chainIdx = 0;
606
+ for (const chain of chains) {
607
+ chainIdx++;
608
+ lines.push(`**Flow ${chainIdx}:** ${chain[0].source} → ${chain[chain.length - 1].target}`);
609
+ lines.push('');
610
+ let step = 0;
611
+ for (const f of chain) {
612
+ step++;
613
+ const mech = f.mechanism ? ` via **${f.mechanism}**` : '';
614
+ const desc = f.description ? ` — ${f.description}` : '';
615
+ lines.push(`${step}. **${f.source}** → **${f.target}**${mech}${desc}`);
616
+ }
617
+ lines.push('');
618
+ }
619
+ }
620
+ function emitDataInventory(model, hasAI, lines) {
621
+ // ── Data Types ──
622
+ if (model.data_handling.length > 0) {
623
+ lines.push('### Data Types');
624
+ lines.push('');
625
+ const byClassification = new Map();
626
+ for (const h of model.data_handling) {
627
+ if (!byClassification.has(h.classification))
628
+ byClassification.set(h.classification, []);
629
+ byClassification.get(h.classification).push(`${h.asset}${h.description ? ` (${truncate(h.description, 40)})` : ''}`);
630
+ }
631
+ for (const [cls, items] of byClassification) {
632
+ lines.push(`**${classificationBadge(cls)}:**`);
633
+ for (const item of items) {
634
+ lines.push(`- ${item}`);
635
+ }
636
+ lines.push('');
637
+ }
638
+ }
639
+ // ── Top Data Assets ──
640
+ lines.push('### Top Data Assets');
641
+ lines.push('');
642
+ // Assets that handle the most data flows
643
+ const assetFlowCount = new Map();
644
+ for (const f of model.flows) {
645
+ assetFlowCount.set(f.target, (assetFlowCount.get(f.target) || 0) + 1);
646
+ assetFlowCount.set(f.source, (assetFlowCount.get(f.source) || 0) + 1);
647
+ }
648
+ const topDataAssets = [...assetFlowCount.entries()]
649
+ .sort((a, b) => b[1] - a[1])
650
+ .slice(0, 10);
651
+ if (topDataAssets.length > 0) {
652
+ lines.push('Assets by data flow volume:');
653
+ lines.push('');
654
+ lines.push('| Asset | Data Flows | Classifications |');
655
+ lines.push('|-------|-----------|-----------------|');
656
+ for (const [asset, count] of topDataAssets) {
657
+ const classes = model.data_handling
658
+ .filter(h => h.asset === asset)
659
+ .map(h => h.classification)
660
+ .join(', ') || '—';
661
+ lines.push(`| ${asset} | ${count} | ${classes} |`);
662
+ }
663
+ lines.push('');
664
+ }
665
+ else {
666
+ lines.push('_No data flow volume data available._');
667
+ lines.push('');
668
+ }
669
+ // ── AI-Specific Data Questions ──
670
+ if (hasAI) {
671
+ lines.push('### AI-Specific Data Considerations');
672
+ lines.push('');
673
+ const aiFlows = model.flows.filter(f => isAIRelated(f.source) || isAIRelated(f.target));
674
+ const aiHandling = model.data_handling.filter(h => isAIRelated(h.asset));
675
+ const aiComments = model.comments.filter(c => /prompt|model|train|inference|embed|token|llm|ai|ml/i.test(c.description || ''));
676
+ if (aiFlows.length > 0) {
677
+ lines.push('**Data flowing to/from AI components:**');
678
+ lines.push('');
679
+ for (const f of aiFlows) {
680
+ lines.push(`- ${f.source} → ${f.target} via ${f.mechanism || 'unspecified'}${f.description ? ` — ${f.description}` : ''}`);
681
+ }
682
+ lines.push('');
683
+ }
684
+ if (aiHandling.length > 0) {
685
+ lines.push('**Data classifications on AI components:**');
686
+ lines.push('');
687
+ for (const h of aiHandling) {
688
+ lines.push(`- ${h.asset}: ${classificationBadge(h.classification)}${h.description ? ` — ${h.description}` : ''}`);
689
+ }
690
+ lines.push('');
691
+ }
692
+ if (aiComments.length > 0) {
693
+ lines.push('**AI-related notes:**');
694
+ lines.push('');
695
+ for (const c of aiComments) {
696
+ lines.push(`- ${c.description} (${c.location.file}:${c.location.line})`);
697
+ }
698
+ lines.push('');
699
+ }
700
+ // Checklist
701
+ lines.push('**AI data checklist:**');
702
+ lines.push('');
703
+ lines.push('- [ ] Are prompts logged? If so, is PII scrubbed?');
704
+ lines.push('- [ ] Is user data used for training/fine-tuning?');
705
+ lines.push('- [ ] What is the data retention policy for AI inputs/outputs?');
706
+ lines.push('- [ ] Are embeddings stored? Can they be reversed to recover source data?');
707
+ lines.push('');
708
+ }
709
+ }
710
+ function emitRolesAccess(model, lines) {
711
+ // ── Owners / Internal Actors ──
712
+ if (model.ownership.length > 0) {
713
+ lines.push('### Ownership & Internal Actors');
714
+ lines.push('');
715
+ const byOwner = new Map();
716
+ for (const o of model.ownership) {
717
+ if (!byOwner.has(o.owner))
718
+ byOwner.set(o.owner, []);
719
+ byOwner.get(o.owner).push(o.asset);
720
+ }
721
+ for (const [owner, assets] of byOwner) {
722
+ lines.push(`- **${owner}**: ${assets.join(', ')}`);
723
+ }
724
+ lines.push('');
725
+ }
726
+ // ── Actors from flows ──
727
+ const actors = new Set();
728
+ const actorPattern = /user|admin|client|browser|operator|attacker|customer|tenant|role/i;
729
+ for (const f of model.flows) {
730
+ if (actorPattern.test(f.source))
731
+ actors.add(f.source);
732
+ if (actorPattern.test(f.target))
733
+ actors.add(f.target);
734
+ }
735
+ // Also check assets for actor-like patterns
736
+ for (const a of model.assets) {
737
+ const name = a.path.join('.');
738
+ if (actorPattern.test(name))
739
+ actors.add(a.id || name);
740
+ }
741
+ if (actors.size > 0) {
742
+ lines.push('### Customer / External Roles');
743
+ lines.push('');
744
+ for (const actor of actors) {
745
+ const flows = model.flows.filter(f => f.source === actor || f.target === actor);
746
+ const interacts = [...new Set(flows.map(f => f.source === actor ? f.target : f.source))];
747
+ lines.push(`- **${actor}** interacts with: ${interacts.join(', ') || '—'}`);
748
+ }
749
+ lines.push('');
750
+ }
751
+ // ── Cross-Tenant Gut Check ──
752
+ lines.push('### Cross-Tenant Gut Check');
753
+ lines.push('');
754
+ const boundaryCount = model.boundaries.length;
755
+ const hasTenantBoundaries = model.boundaries.some(b => /tenant|isolat/i.test(b.description || '') || /tenant|isolat/i.test(b.id || ''));
756
+ if (hasTenantBoundaries) {
757
+ lines.push('Tenant isolation boundaries are defined:');
758
+ lines.push('');
759
+ for (const b of model.boundaries.filter(b => /tenant|isolat/i.test(b.description || '') || /tenant|isolat/i.test(b.id || ''))) {
760
+ lines.push(`- ${b.description || b.id} (${b.asset_a} ↔ ${b.asset_b})`);
761
+ }
762
+ }
763
+ else if (boundaryCount > 0) {
764
+ lines.push(`${boundaryCount} trust boundaries defined, but none explicitly mention tenant isolation. If this is multi-tenant, verify that cross-tenant data access is prevented at each boundary.`);
765
+ }
766
+ else {
767
+ lines.push('_No trust boundaries defined. If multi-tenant, add `@boundary` annotations to document tenant isolation._');
768
+ }
769
+ lines.push('');
770
+ }
771
+ function emitDependencies(model, lines) {
772
+ // Build asset ID set that matches both "#id" and "id" forms used in flows
773
+ const assetIds = new Set();
774
+ for (const a of model.assets) {
775
+ const id = a.id || a.path.join('.');
776
+ assetIds.add(id);
777
+ assetIds.add(`#${id}`);
778
+ // Also add the dotted path form
779
+ const path = a.path.join('.');
780
+ assetIds.add(path);
781
+ assetIds.add(`#${path}`);
782
+ }
783
+ // ── Internal services: assets that are flow targets from other assets ──
784
+ lines.push('### Internal Services');
785
+ lines.push('');
786
+ const internalDeps = new Set();
787
+ for (const f of model.flows) {
788
+ if (assetIds.has(f.source) && assetIds.has(f.target) && f.source !== f.target) {
789
+ internalDeps.add(`${f.source} → ${f.target}`);
790
+ }
791
+ }
792
+ if (internalDeps.size > 0) {
793
+ for (const dep of internalDeps) {
794
+ lines.push(`- ${dep}`);
795
+ }
796
+ }
797
+ else {
798
+ lines.push('_No internal service dependencies detected from flows._');
799
+ }
800
+ lines.push('');
801
+ // ── External / Cloud / AI Vendors ──
802
+ lines.push('### External & Cloud Dependencies');
803
+ lines.push('');
804
+ const externalNodes = new Set();
805
+ for (const f of model.flows) {
806
+ if (!assetIds.has(f.source))
807
+ externalNodes.add(f.source);
808
+ if (!assetIds.has(f.target))
809
+ externalNodes.add(f.target);
810
+ }
811
+ // Also from transfers
812
+ for (const t of model.transfers) {
813
+ if (!assetIds.has(t.target))
814
+ externalNodes.add(t.target);
815
+ if (!assetIds.has(t.source))
816
+ externalNodes.add(t.source);
817
+ }
818
+ // Also external_refs
819
+ if (model.external_refs) {
820
+ for (const ref of model.external_refs) {
821
+ if (ref.inferred_repo)
822
+ externalNodes.add(ref.inferred_repo);
823
+ }
824
+ }
825
+ if (externalNodes.size > 0) {
826
+ const aiVendors = [];
827
+ const cloudVendors = [];
828
+ const otherVendors = [];
829
+ for (const node of externalNodes) {
830
+ if (isAIRelated(node)) {
831
+ aiVendors.push(node);
832
+ }
833
+ else if (/aws|gcp|azure|cloud|s3|lambda|cdn|redis|postgres|mysql|mongo|kafka|rabbit|elastic/i.test(node)) {
834
+ cloudVendors.push(node);
835
+ }
836
+ else {
837
+ otherVendors.push(node);
838
+ }
839
+ }
840
+ if (aiVendors.length > 0) {
841
+ lines.push('**AI/ML Vendors:**');
842
+ for (const v of aiVendors)
843
+ lines.push(`- ${v}`);
844
+ lines.push('');
845
+ }
846
+ if (cloudVendors.length > 0) {
847
+ lines.push('**Cloud/Infrastructure:**');
848
+ for (const v of cloudVendors)
849
+ lines.push(`- ${v}`);
850
+ lines.push('');
851
+ }
852
+ if (otherVendors.length > 0) {
853
+ lines.push('**Other External:**');
854
+ for (const v of otherVendors)
855
+ lines.push(`- ${v}`);
856
+ lines.push('');
857
+ }
858
+ }
859
+ else {
860
+ lines.push('_No external dependencies detected from flows or transfers._');
861
+ lines.push('');
862
+ }
863
+ // Risk transfers to external parties
864
+ if (model.transfers.length > 0) {
865
+ lines.push('### Risk Transfers to Dependencies');
866
+ lines.push('');
867
+ for (const t of model.transfers) {
868
+ lines.push(`- **${t.threat}** transferred from ${t.source} → ${t.target}${t.description ? ` — ${t.description}` : ''}`);
869
+ }
870
+ lines.push('');
871
+ }
872
+ }
873
+ function emitSecretsManagement(model, lines) {
874
+ // ── Secret Inventory ──
875
+ const secretHandling = model.data_handling.filter(h => h.classification === 'secrets');
876
+ const keyExposures = model.exposures.filter(e => /key|secret|cred|token|password|api.?key/i.test(e.threat) ||
877
+ /key|secret|cred|token|password|api.?key/i.test(e.description || ''));
878
+ const keyMitigations = model.mitigations.filter(m => /key|secret|cred|token|password|api.?key|redact|encrypt/i.test(m.control || '') ||
879
+ /key|secret|cred|token|password|api.?key/i.test(m.description || ''));
880
+ const keyComments = model.comments.filter(c => /key|secret|cred|token|password|api.?key|rotat|vault|kms/i.test(c.description || ''));
881
+ lines.push('### Secret Inventory');
882
+ lines.push('');
883
+ if (secretHandling.length > 0) {
884
+ lines.push('| Asset | Description | Location |');
885
+ lines.push('|-------|-------------|----------|');
886
+ for (const h of secretHandling) {
887
+ lines.push(`| ${h.asset} | ${h.description || '—'} | ${h.location.file}:${h.location.line} |`);
888
+ }
889
+ lines.push('');
890
+ }
891
+ else {
892
+ lines.push('_No assets classified as `secrets` via `@handles`. Consider adding `@handles secrets on <asset>` annotations._');
893
+ lines.push('');
894
+ }
895
+ // ── Leak Impact ──
896
+ lines.push('### Leak Impact Analysis');
897
+ lines.push('');
898
+ if (keyExposures.length > 0) {
899
+ lines.push('Key/credential-related exposures:');
900
+ lines.push('');
901
+ for (const e of keyExposures) {
902
+ lines.push(`- ${severityBadge(e.severity)} **${e.asset}** exposed to **${e.threat}**${e.description ? ` — ${e.description}` : ''} (${e.location.file}:${e.location.line})`);
903
+ }
904
+ lines.push('');
905
+ }
906
+ if (keyMitigations.length > 0) {
907
+ lines.push('Active credential protections:');
908
+ lines.push('');
909
+ for (const m of keyMitigations) {
910
+ lines.push(`- **${m.control || 'control'}** on ${m.asset}${m.description ? ` — ${m.description}` : ''}`);
911
+ }
912
+ lines.push('');
913
+ }
914
+ if (keyExposures.length === 0 && keyMitigations.length === 0) {
915
+ lines.push('_No credential-related exposures or mitigations found._');
916
+ lines.push('');
917
+ }
918
+ // ── Rotation Strategy ──
919
+ lines.push('### Rotation Strategy');
920
+ lines.push('');
921
+ if (keyComments.length > 0) {
922
+ for (const c of keyComments) {
923
+ lines.push(`- ${c.description} (${c.location.file}:${c.location.line})`);
924
+ }
925
+ }
926
+ else {
927
+ lines.push('_No rotation strategy documented. Consider adding `@comment` annotations describing key rotation policies._');
928
+ }
929
+ lines.push('');
930
+ }
931
+ function emitLoggingAudit(model, lines) {
932
+ // ── What's Logged ──
933
+ const loggingComments = model.comments.filter(c => /log|audit|trace|monitor|alert|metric|observ/i.test(c.description || ''));
934
+ lines.push('### Logging & Observability');
935
+ lines.push('');
936
+ if (loggingComments.length > 0) {
937
+ for (const c of loggingComments) {
938
+ lines.push(`- ${c.description} (${c.location.file}:${c.location.line})`);
939
+ }
940
+ }
941
+ else {
942
+ lines.push('_No logging-related annotations found. Consider documenting what security events are logged._');
943
+ }
944
+ lines.push('');
945
+ // ── Incident Reconstruction ──
946
+ lines.push('### Incident Reconstruction');
947
+ lines.push('');
948
+ if (model.audits.length > 0) {
949
+ lines.push(`**${model.audits.length} audit items** flagged for review:`);
950
+ lines.push('');
951
+ for (const a of model.audits.slice(0, 10)) {
952
+ lines.push(`- **${a.asset}**: ${a.description || 'Needs review'} (${a.location.file}:${a.location.line})`);
953
+ }
954
+ if (model.audits.length > 10) {
955
+ lines.push(`- ... and ${model.audits.length - 10} more (see Audit Items section)`);
956
+ }
957
+ }
958
+ else {
959
+ lines.push('_No `@audit` items. Consider flagging security-critical code paths for review._');
960
+ }
961
+ lines.push('');
962
+ // ── Alerting ──
963
+ lines.push('### Alerting');
964
+ lines.push('');
965
+ const alertComments = model.comments.filter(c => /alert|page|notify|incident|on.?call/i.test(c.description || ''));
966
+ if (alertComments.length > 0) {
967
+ for (const c of alertComments) {
968
+ lines.push(`- ${c.description} (${c.location.file}:${c.location.line})`);
969
+ }
970
+ }
971
+ else {
972
+ lines.push('_No alerting annotations found. Consider documenting alerting strategies via `@comment`._');
973
+ }
974
+ lines.push('');
975
+ }
976
+ function emitAIDetails(model, lines) {
977
+ // ── Model Inventory ──
978
+ const aiAssets = model.assets.filter(a => isAIRelated(a.id || a.path.join('.')));
979
+ const aiExposures = model.exposures.filter(e => isAIRelated(e.asset) || /prompt.?inject|model|adversarial/i.test(e.threat));
980
+ const aiMitigations = model.mitigations.filter(m => isAIRelated(m.asset) || /prompt.?inject|model|adversarial/i.test(m.threat));
981
+ const aiComments = model.comments.filter(c => /prompt|model|llm|ai|ml|inference|embed|token|train|fine.?tun|rag|vector/i.test(c.description || ''));
982
+ lines.push('### Model Inventory');
983
+ lines.push('');
984
+ if (aiAssets.length > 0) {
985
+ lines.push('| Component | ID | Description |');
986
+ lines.push('|-----------|-----|-------------|');
987
+ for (const a of aiAssets) {
988
+ lines.push(`| ${a.path.join('.')} | ${a.id || '—'} | ${a.description || '—'} |`);
989
+ }
990
+ }
991
+ else {
992
+ lines.push('_AI usage detected in annotations but no AI-specific assets defined._');
993
+ }
994
+ lines.push('');
995
+ // ── Safety Guardrails ──
996
+ lines.push('### Safety Guardrails');
997
+ lines.push('');
998
+ if (aiMitigations.length > 0) {
999
+ for (const m of aiMitigations) {
1000
+ lines.push(`- **${m.control || 'control'}** on ${m.asset} against ${m.threat}${m.description ? ` — ${m.description}` : ''}`);
1001
+ }
1002
+ }
1003
+ else {
1004
+ lines.push('_No AI-specific mitigations found._');
1005
+ }
1006
+ lines.push('');
1007
+ // ── Prompt Injection Handling ──
1008
+ lines.push('### Prompt Injection Handling');
1009
+ lines.push('');
1010
+ const promptInjectionExposures = aiExposures.filter(e => /prompt.?inject/i.test(e.threat));
1011
+ const promptInjectionMitigations = aiMitigations.filter(m => /prompt.?inject/i.test(m.threat));
1012
+ if (promptInjectionExposures.length > 0 || promptInjectionMitigations.length > 0) {
1013
+ if (promptInjectionExposures.length > 0) {
1014
+ lines.push('**Exposures:**');
1015
+ for (const e of promptInjectionExposures) {
1016
+ lines.push(`- ${severityBadge(e.severity)} ${e.asset}${e.description ? ` — ${e.description}` : ''} (${e.location.file}:${e.location.line})`);
1017
+ }
1018
+ lines.push('');
1019
+ }
1020
+ if (promptInjectionMitigations.length > 0) {
1021
+ lines.push('**Mitigations:**');
1022
+ for (const m of promptInjectionMitigations) {
1023
+ lines.push(`- ${m.control || 'control'} on ${m.asset}${m.description ? ` — ${m.description}` : ''}`);
1024
+ }
1025
+ lines.push('');
1026
+ }
1027
+ }
1028
+ else {
1029
+ lines.push('_No prompt injection exposures or mitigations documented._');
1030
+ lines.push('');
1031
+ }
1032
+ // ── Data Retention ──
1033
+ lines.push('### Data Retention & AI Notes');
1034
+ lines.push('');
1035
+ if (aiComments.length > 0) {
1036
+ for (const c of aiComments) {
1037
+ lines.push(`- ${c.description} (${c.location.file}:${c.location.line})`);
1038
+ }
1039
+ }
1040
+ else {
1041
+ lines.push('_No AI data retention notes found. Consider documenting prompt logging, training data handling, and model output storage._');
1042
+ }
1043
+ lines.push('');
1044
+ }
1045
+ // ═══════════════════════════════════════════════════════════════════════
1046
+ // Helpers
1047
+ // ═══════════════════════════════════════════════════════════════════════
230
1048
  function truncate(s, max) {
231
1049
  if (s.length <= max)
232
1050
  return s;
233
1051
  return s.slice(0, max - 1) + '…';
234
1052
  }
1053
+ function shortName(s) {
1054
+ if (s.startsWith('#'))
1055
+ return s.slice(1);
1056
+ return s.split('.').pop() || s;
1057
+ }
235
1058
  const SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
236
1059
  function severityBadge(sev) {
237
1060
  switch (sev) {
@@ -269,4 +1092,81 @@ function countBySeverity(exposures) {
269
1092
  }
270
1093
  return counts;
271
1094
  }
1095
+ /** Detect if the project uses AI/ML based on annotations */
1096
+ function detectAI(model) {
1097
+ // Strict patterns — avoid false positives from "model" (data model), "token" (auth token), etc.
1098
+ const aiAssetPattern = /\bllm\b|(?:^|\W)ai(?:\W|$)|\bml\b|\binference\b|\bembed(?:ding)?\b|\bopenai\b|\banthropic\b|\bgpt\b|\bclaude\b|\bgemini\b|\brag\b|\bvector.?(?:db|store)\b|\bneural\b/i;
1099
+ const aiFlowPattern = /\bllm\b|\bopenai\b|\banthropic\b|\bgpt\b|\bclaude\b|\bgemini\b|\binference\b|\bembed(?:ding)?\b|\brag\b|\bvector.?(?:db|store)\b|\bchat.?completion\b/i;
1100
+ for (const a of model.assets) {
1101
+ if (aiAssetPattern.test(a.id || '') || aiAssetPattern.test(a.path.join('.')))
1102
+ return true;
1103
+ if (aiAssetPattern.test(a.description || ''))
1104
+ return true;
1105
+ }
1106
+ for (const t of model.threats) {
1107
+ if (/prompt.?inject/i.test(t.name) || /prompt.?inject/i.test(t.canonical_name))
1108
+ return true;
1109
+ }
1110
+ for (const f of model.flows) {
1111
+ if (aiFlowPattern.test(f.source) || aiFlowPattern.test(f.target))
1112
+ return true;
1113
+ if (aiFlowPattern.test(f.mechanism || ''))
1114
+ return true;
1115
+ }
1116
+ return false;
1117
+ }
1118
+ /** Check if a name is AI-related (strict) */
1119
+ function isAIRelated(name) {
1120
+ return /\bllm\b|\bopenai\b|\banthropic\b|\bgpt\b|\bclaude\b|\bgemini\b|\binference\b|\bembed(?:ding)?\b|\brag\b|\bvector.?(?:db|store)\b|\bneural\b|\bchat.?completion\b/i.test(name);
1121
+ }
1122
+ /** Build connected flow chains from individual flow edges */
1123
+ function buildFlowChains(flows) {
1124
+ if (flows.length === 0)
1125
+ return [];
1126
+ // Build adjacency: source -> list of flows
1127
+ const adj = new Map();
1128
+ for (const f of flows) {
1129
+ if (!adj.has(f.source))
1130
+ adj.set(f.source, []);
1131
+ adj.get(f.source).push(f);
1132
+ }
1133
+ // Find chain starting points: sources that are not targets of other flows
1134
+ const allTargets = new Set(flows.map(f => f.target));
1135
+ const startNodes = new Set();
1136
+ for (const f of flows) {
1137
+ if (!allTargets.has(f.source))
1138
+ startNodes.add(f.source);
1139
+ }
1140
+ // If no clear start points, use all sources
1141
+ if (startNodes.size === 0) {
1142
+ for (const f of flows)
1143
+ startNodes.add(f.source);
1144
+ }
1145
+ const visited = new Set();
1146
+ const chains = [];
1147
+ for (const start of startNodes) {
1148
+ if (visited.has(start))
1149
+ continue;
1150
+ const chain = [];
1151
+ let current = start;
1152
+ const chainVisited = new Set();
1153
+ while (adj.has(current) && !chainVisited.has(current)) {
1154
+ chainVisited.add(current);
1155
+ visited.add(current);
1156
+ const nextFlows = adj.get(current);
1157
+ const next = nextFlows[0]; // Take first path
1158
+ chain.push(next);
1159
+ current = next.target;
1160
+ }
1161
+ if (chain.length > 0)
1162
+ chains.push(chain);
1163
+ }
1164
+ // Add any isolated flows not in chains
1165
+ const chainedFlows = new Set(chains.flat().map(f => `${f.source}::${f.target}`));
1166
+ const isolated = flows.filter(f => !chainedFlows.has(`${f.source}::${f.target}`));
1167
+ for (const f of isolated) {
1168
+ chains.push([f]);
1169
+ }
1170
+ return chains;
1171
+ }
272
1172
  //# sourceMappingURL=report.js.map