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.
- package/CHANGELOG.md +83 -9
- package/README.md +38 -1
- package/dist/agents/config.d.ts +7 -0
- package/dist/agents/config.d.ts.map +1 -1
- package/dist/agents/config.js.map +1 -1
- package/dist/agents/index.d.ts +1 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +1 -1
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/prompts.d.ts +14 -0
- package/dist/agents/prompts.d.ts.map +1 -1
- package/dist/agents/prompts.js +445 -2
- package/dist/agents/prompts.js.map +1 -1
- package/dist/analyze/format.d.ts +72 -0
- package/dist/analyze/format.d.ts.map +1 -0
- package/dist/analyze/format.js +176 -0
- package/dist/analyze/format.js.map +1 -0
- package/dist/analyze/index.d.ts +76 -0
- package/dist/analyze/index.d.ts.map +1 -1
- package/dist/analyze/index.js +165 -2
- package/dist/analyze/index.js.map +1 -1
- package/dist/analyze/prompts.d.ts +3 -2
- package/dist/analyze/prompts.d.ts.map +1 -1
- package/dist/analyze/prompts.js +16 -2
- package/dist/analyze/prompts.js.map +1 -1
- package/dist/analyzer/sarif.d.ts +3 -2
- package/dist/analyzer/sarif.d.ts.map +1 -1
- package/dist/analyzer/sarif.js +29 -3
- package/dist/analyzer/sarif.js.map +1 -1
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +380 -28
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/data.d.ts +11 -0
- package/dist/dashboard/data.d.ts.map +1 -1
- package/dist/dashboard/data.js +12 -0
- package/dist/dashboard/data.js.map +1 -1
- package/dist/dashboard/diagrams.d.ts +81 -12
- package/dist/dashboard/diagrams.d.ts.map +1 -1
- package/dist/dashboard/diagrams.js +750 -362
- package/dist/dashboard/diagrams.js.map +1 -1
- package/dist/dashboard/generate.d.ts +5 -2
- package/dist/dashboard/generate.d.ts.map +1 -1
- package/dist/dashboard/generate.js +2516 -244
- package/dist/dashboard/generate.js.map +1 -1
- package/dist/diff/engine.d.ts +2 -1
- package/dist/diff/engine.d.ts.map +1 -1
- package/dist/diff/engine.js +3 -2
- package/dist/diff/engine.js.map +1 -1
- package/dist/init/index.d.ts.map +1 -1
- package/dist/init/index.js +24 -5
- package/dist/init/index.js.map +1 -1
- package/dist/init/migrate.d.ts +39 -0
- package/dist/init/migrate.d.ts.map +1 -0
- package/dist/init/migrate.js +45 -0
- package/dist/init/migrate.js.map +1 -0
- package/dist/init/templates.d.ts +8 -0
- package/dist/init/templates.d.ts.map +1 -1
- package/dist/init/templates.js +71 -9
- package/dist/init/templates.js.map +1 -1
- package/dist/mcp/lookup.d.ts +1 -0
- package/dist/mcp/lookup.d.ts.map +1 -1
- package/dist/mcp/lookup.js +138 -10
- package/dist/mcp/lookup.js.map +1 -1
- package/dist/mcp/server.d.ts +2 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +20 -8
- package/dist/mcp/server.js.map +1 -1
- package/dist/parser/clear.js +1 -1
- package/dist/parser/clear.js.map +1 -1
- package/dist/parser/feature-filter.d.ts +42 -0
- package/dist/parser/feature-filter.d.ts.map +1 -0
- package/dist/parser/feature-filter.js +109 -0
- package/dist/parser/feature-filter.js.map +1 -0
- package/dist/parser/format.d.ts +24 -0
- package/dist/parser/format.d.ts.map +1 -0
- package/dist/parser/format.js +29 -0
- package/dist/parser/format.js.map +1 -0
- package/dist/parser/index.d.ts +2 -0
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/index.js +1 -0
- package/dist/parser/index.js.map +1 -1
- package/dist/parser/parse-file.d.ts.map +1 -1
- package/dist/parser/parse-file.js +3 -1
- package/dist/parser/parse-file.js.map +1 -1
- package/dist/parser/parse-line.d.ts +3 -0
- package/dist/parser/parse-line.d.ts.map +1 -1
- package/dist/parser/parse-line.js +78 -22
- package/dist/parser/parse-line.js.map +1 -1
- package/dist/parser/parse-project.js +19 -0
- package/dist/parser/parse-project.js.map +1 -1
- package/dist/parser/validate.d.ts +3 -0
- package/dist/parser/validate.d.ts.map +1 -1
- package/dist/parser/validate.js +7 -0
- package/dist/parser/validate.js.map +1 -1
- package/dist/report/index.d.ts +1 -0
- package/dist/report/index.d.ts.map +1 -1
- package/dist/report/index.js +1 -0
- package/dist/report/index.js.map +1 -1
- package/dist/report/report.d.ts.map +1 -1
- package/dist/report/report.js +924 -24
- package/dist/report/report.js.map +1 -1
- package/dist/report/sequence.d.ts +11 -0
- package/dist/report/sequence.d.ts.map +1 -0
- package/dist/report/sequence.js +140 -0
- package/dist/report/sequence.js.map +1 -0
- package/dist/tui/commands.d.ts +1 -0
- package/dist/tui/commands.d.ts.map +1 -1
- package/dist/tui/commands.js +83 -4
- package/dist/tui/commands.js.map +1 -1
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +7 -2
- package/dist/tui/index.js.map +1 -1
- package/dist/types/index.d.ts +57 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/workspace/merge.d.ts.map +1 -1
- package/dist/workspace/merge.js +6 -2
- package/dist/workspace/merge.js.map +1 -1
- package/package.json +1 -1
package/dist/report/report.js
CHANGED
|
@@ -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
|
-
// ──
|
|
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('##
|
|
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('##
|
|
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('##
|
|
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('##
|
|
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('##
|
|
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('##
|
|
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('##
|
|
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('##
|
|
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('##
|
|
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('##
|
|
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('##
|
|
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('##
|
|
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('##
|
|
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
|
|
343
|
+
lines.push(`*Generated from security annotations on ${model.generated_at}.*`);
|
|
227
344
|
return lines.join('\n');
|
|
228
345
|
}
|
|
229
|
-
//
|
|
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
|