heron-ai 0.2.2 → 0.4.0
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/dist/bin/heron.js +31 -2
- package/dist/bin/heron.js.map +1 -1
- package/dist/src/analysis/analyzer.d.ts +1 -1
- package/dist/src/analysis/analyzer.d.ts.map +1 -1
- package/dist/src/analysis/analyzer.js +120 -6
- package/dist/src/analysis/analyzer.js.map +1 -1
- package/dist/src/analysis/risk-scorer.d.ts +32 -0
- package/dist/src/analysis/risk-scorer.d.ts.map +1 -1
- package/dist/src/analysis/risk-scorer.js +134 -0
- package/dist/src/analysis/risk-scorer.js.map +1 -1
- package/dist/src/commands/diff.d.ts +17 -0
- package/dist/src/commands/diff.d.ts.map +1 -0
- package/dist/src/commands/diff.js +63 -0
- package/dist/src/commands/diff.js.map +1 -0
- package/dist/src/compliance/control-mappings.d.ts +21 -0
- package/dist/src/compliance/control-mappings.d.ts.map +1 -0
- package/dist/src/compliance/control-mappings.js +182 -0
- package/dist/src/compliance/control-mappings.js.map +1 -0
- package/dist/src/compliance/frameworks.d.ts +24 -0
- package/dist/src/compliance/frameworks.d.ts.map +1 -0
- package/dist/src/compliance/frameworks.js +55 -0
- package/dist/src/compliance/frameworks.js.map +1 -0
- package/dist/src/compliance/index.d.ts +9 -0
- package/dist/src/compliance/index.d.ts.map +1 -0
- package/dist/src/compliance/index.js +8 -0
- package/dist/src/compliance/index.js.map +1 -0
- package/dist/src/compliance/mapper.d.ts +126 -0
- package/dist/src/compliance/mapper.d.ts.map +1 -0
- package/dist/src/compliance/mapper.js +443 -0
- package/dist/src/compliance/mapper.js.map +1 -0
- package/dist/src/compliance/types.d.ts +120 -0
- package/dist/src/compliance/types.d.ts.map +1 -0
- package/dist/src/compliance/types.js +99 -0
- package/dist/src/compliance/types.js.map +1 -0
- package/dist/src/diff/differ.d.ts +9 -0
- package/dist/src/diff/differ.d.ts.map +1 -0
- package/dist/src/diff/differ.js +52 -0
- package/dist/src/diff/differ.js.map +1 -0
- package/dist/src/interview/interviewer.d.ts +2 -0
- package/dist/src/interview/interviewer.d.ts.map +1 -1
- package/dist/src/interview/interviewer.js.map +1 -1
- package/dist/src/interview/protocol.d.ts.map +1 -1
- package/dist/src/interview/protocol.js +28 -5
- package/dist/src/interview/protocol.js.map +1 -1
- package/dist/src/interview/questions.d.ts.map +1 -1
- package/dist/src/interview/questions.js +55 -0
- package/dist/src/interview/questions.js.map +1 -1
- package/dist/src/llm/client.d.ts +26 -1
- package/dist/src/llm/client.d.ts.map +1 -1
- package/dist/src/llm/client.js +108 -15
- package/dist/src/llm/client.js.map +1 -1
- package/dist/src/llm/prompts.d.ts +27 -1
- package/dist/src/llm/prompts.d.ts.map +1 -1
- package/dist/src/llm/prompts.js +133 -1
- package/dist/src/llm/prompts.js.map +1 -1
- package/dist/src/report/generator.d.ts +1 -7
- package/dist/src/report/generator.d.ts.map +1 -1
- package/dist/src/report/generator.js +47 -236
- package/dist/src/report/generator.js.map +1 -1
- package/dist/src/report/templates.d.ts +2 -1
- package/dist/src/report/templates.d.ts.map +1 -1
- package/dist/src/report/templates.js +436 -84
- package/dist/src/report/templates.js.map +1 -1
- package/dist/src/report/types.d.ts +34 -19
- package/dist/src/report/types.d.ts.map +1 -1
- package/dist/src/report/types.js +8 -4
- package/dist/src/report/types.js.map +1 -1
- package/dist/src/server/index.d.ts +1 -1
- package/dist/src/server/index.d.ts.map +1 -1
- package/dist/src/server/index.js +212 -55
- package/dist/src/server/index.js.map +1 -1
- package/dist/src/server/sessions.d.ts +10 -0
- package/dist/src/server/sessions.d.ts.map +1 -1
- package/dist/src/server/sessions.js +73 -9
- package/dist/src/server/sessions.js.map +1 -1
- package/dist/src/util/provided.d.ts +49 -0
- package/dist/src/util/provided.d.ts.map +1 -0
- package/dist/src/util/provided.js +83 -0
- package/dist/src/util/provided.js.map +1 -0
- package/dist/src/util/systems.d.ts +15 -0
- package/dist/src/util/systems.d.ts.map +1 -0
- package/dist/src/util/systems.js +41 -0
- package/dist/src/util/systems.js.map +1 -0
- package/package.json +1 -1
- package/skills/heron-audit/bin/heron-update-check +13 -4
|
@@ -1,30 +1,52 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
import { isProvided, UNKNOWN_PLACEHOLDER } from '../util/provided.js';
|
|
2
|
+
import { isBusinessSystem } from '../util/systems.js';
|
|
3
|
+
// ─── AAP-43 P1 #5: overall regulatory status ──────────────────────────────
|
|
4
|
+
/**
|
|
5
|
+
* Reduce all activated framework flags into a single status label + gap
|
|
6
|
+
* counter. Replaces the prior EU/US/UK jurisdiction matrix which couldn't
|
|
7
|
+
* vary without US/UK frameworks in the OSS registry.
|
|
8
|
+
*
|
|
9
|
+
* Labels (descending severity):
|
|
10
|
+
* - "Action Required" — at least one action-required flag
|
|
11
|
+
* - "Needs Clarification" — at least one clarification-needed flag
|
|
12
|
+
* - "Review" — at least one warning-level flag
|
|
13
|
+
* - "Not Triggered" — no activated framework flags
|
|
14
|
+
*/
|
|
15
|
+
function summarizeOverallStatus(c) {
|
|
16
|
+
const all = (c.all ?? []);
|
|
17
|
+
if (all.length === 0)
|
|
18
|
+
return 'Not Triggered';
|
|
19
|
+
let label;
|
|
20
|
+
if (all.some(f => f.severity === 'action-required'))
|
|
21
|
+
label = 'Action Required';
|
|
22
|
+
else if (all.some(f => f.severity === 'clarification-needed'))
|
|
23
|
+
label = 'Needs Clarification';
|
|
24
|
+
else if (all.some(f => f.severity === 'warning'))
|
|
25
|
+
label = 'Review';
|
|
26
|
+
else
|
|
27
|
+
label = 'Not Triggered';
|
|
28
|
+
const mandatoryGaps = all.filter(f => f.tier === 'mandatory' && f.severity !== 'info').length;
|
|
29
|
+
const voluntaryGaps = all.filter(f => f.tier === 'voluntary' && f.severity !== 'info').length;
|
|
30
|
+
const parts = [];
|
|
31
|
+
if (mandatoryGaps > 0)
|
|
32
|
+
parts.push(`${mandatoryGaps} mandatory-framework gap${mandatoryGaps === 1 ? '' : 's'}`);
|
|
33
|
+
if (voluntaryGaps > 0)
|
|
34
|
+
parts.push(`${voluntaryGaps} voluntary-framework gap${voluntaryGaps === 1 ? '' : 's'}`);
|
|
35
|
+
const suffix = parts.length > 0 ? ` (${parts.join(', ')})` : '';
|
|
36
|
+
return `${label}${suffix}`;
|
|
16
37
|
}
|
|
38
|
+
// isBusinessSystem lives in src/util/systems.ts (shared with analyzer + mapper).
|
|
17
39
|
export function renderMarkdownReport(report) {
|
|
18
40
|
const sections = [
|
|
19
41
|
renderHeader(report),
|
|
20
42
|
renderScopeAndMethodology(report),
|
|
21
43
|
renderSummary(report),
|
|
22
44
|
renderAgentProfile(report),
|
|
23
|
-
renderFindings(report.risks),
|
|
45
|
+
renderFindings(report.risks, report.compliance),
|
|
24
46
|
renderSystems(report.systems),
|
|
25
47
|
renderPositiveFindings(report),
|
|
26
48
|
renderVerdict(report),
|
|
27
|
-
report.
|
|
49
|
+
report.compliance ? renderRegulatoryCompliance(report.compliance, report) : null,
|
|
28
50
|
report.dataQuality ? renderDataQuality(report.dataQuality) : null,
|
|
29
51
|
renderTranscript(report.transcript),
|
|
30
52
|
renderDisclaimer(),
|
|
@@ -33,30 +55,25 @@ export function renderMarkdownReport(report) {
|
|
|
33
55
|
}
|
|
34
56
|
// ─── Header ──────────────────────────────────────────────────────────────────
|
|
35
57
|
function renderHeader(report) {
|
|
36
|
-
|
|
58
|
+
// Reviewer feedback (2026-04-25): the prior `!!` exclamation marker on
|
|
59
|
+
// HIGH/CRITICAL headers ("Risk Level: HIGH !!") was called out as
|
|
60
|
+
// "not a serious-document tone" — CISOs do not want excitement in audit
|
|
61
|
+
// headers. The `**Risk Level**: HIGH` label itself is already strong;
|
|
62
|
+
// the riskIcon adds nothing and undercuts credibility. Dropped.
|
|
37
63
|
const dqPart = report.dataQuality ? ` | **Data Quality**: ${report.dataQuality.score}/100` : '';
|
|
38
|
-
//
|
|
64
|
+
// AAP-43 P1 #5: single overall regulatory status label (replaces
|
|
65
|
+
// EU/US/UK matrix). The matrix implied we'd analyzed each jurisdiction,
|
|
66
|
+
// but we don't know the deployer's jurisdiction and only EU-mandatory
|
|
67
|
+
// frameworks are in OSS scope (see AAP-42). A single label + gap counter
|
|
68
|
+
// is honest: "here is the highest unresolved severity across activated
|
|
69
|
+
// frameworks, and how many mandatory vs voluntary gaps there are."
|
|
39
70
|
let regLine = '';
|
|
40
|
-
if (report.
|
|
41
|
-
|
|
42
|
-
const regParts = [];
|
|
43
|
-
const summarizeJurisdiction = (flags) => {
|
|
44
|
-
if (flags.some(f => f.severity === 'action-required'))
|
|
45
|
-
return 'Action Required';
|
|
46
|
-
if (flags.some(f => f.severity === 'clarification-needed'))
|
|
47
|
-
return 'Needs Clarification';
|
|
48
|
-
if (flags.some(f => f.severity === 'warning'))
|
|
49
|
-
return 'Review';
|
|
50
|
-
return 'Clear';
|
|
51
|
-
};
|
|
52
|
-
regParts.push(`EU: ${summarizeJurisdiction(rc.eu)}`);
|
|
53
|
-
regParts.push(`US: ${summarizeJurisdiction(rc.us)}`);
|
|
54
|
-
regParts.push(`UK: ${summarizeJurisdiction(rc.uk)}`);
|
|
55
|
-
regLine = `\n**Regulatory**: ${regParts.join(' | ')}`;
|
|
71
|
+
if (report.compliance) {
|
|
72
|
+
regLine = `\n**Regulatory Status**: ${summarizeOverallStatus(report.compliance)}`;
|
|
56
73
|
}
|
|
57
74
|
return `# Agent Access Audit Report
|
|
58
75
|
|
|
59
|
-
**Generated**: ${report.metadata.date} | **Agent**: ${report.metadata.target} | **Risk Level**: ${report.overallRiskLevel.toUpperCase()}
|
|
76
|
+
**Generated**: ${report.metadata.date} | **Agent**: ${report.metadata.target} | **Risk Level**: ${report.overallRiskLevel.toUpperCase()}${dqPart}${regLine}`;
|
|
60
77
|
}
|
|
61
78
|
// ─── Scope & Methodology ────────────────────────────────────────────────────
|
|
62
79
|
function renderScopeAndMethodology(report) {
|
|
@@ -120,9 +137,17 @@ function renderSummary(report) {
|
|
|
120
137
|
const dashboard = `| Risk | Systems | Findings |
|
|
121
138
|
|------|---------|----------|
|
|
122
139
|
| **${report.overallRiskLevel.toUpperCase()}** | ${systemCount} | ${findingsParts.join(', ')} |`;
|
|
140
|
+
let methodology = '';
|
|
141
|
+
if (report.compliance) {
|
|
142
|
+
const c = report.compliance;
|
|
143
|
+
const activated = (c.frameworksActivated ?? []);
|
|
144
|
+
const names = activated.map(id => frameworkShortName(id)).filter(Boolean);
|
|
145
|
+
const fwList = names.length > 0 ? names.join(', ') : 'see Regulatory Compliance section';
|
|
146
|
+
methodology = `\n\n> **Risk methodology** anchored to ${names.length} frameworks: ${fwList}. Mapping version: \`${c.mappingVersion}\`.`;
|
|
147
|
+
}
|
|
123
148
|
return `## Executive Summary
|
|
124
149
|
|
|
125
|
-
${dashboard}
|
|
150
|
+
${dashboard}${methodology}
|
|
126
151
|
|
|
127
152
|
${report.summary}`;
|
|
128
153
|
}
|
|
@@ -131,12 +156,12 @@ function renderAgentProfile(report) {
|
|
|
131
156
|
const lines = [`- **Purpose**: ${report.agentPurpose}`];
|
|
132
157
|
if (report.agentTrigger)
|
|
133
158
|
lines.push(`- **Trigger**: ${report.agentTrigger}`);
|
|
134
|
-
if (report.agentOwner
|
|
159
|
+
if (isProvided(report.agentOwner)) {
|
|
135
160
|
lines.push(`- **Owner**: ${report.agentOwner}`);
|
|
136
161
|
}
|
|
137
162
|
// Frequency from first system if available
|
|
138
163
|
const freq = report.systems[0]?.frequencyAndVolume;
|
|
139
|
-
if (freq
|
|
164
|
+
if (isProvided(freq))
|
|
140
165
|
lines.push(`- **Frequency**: ${freq}`);
|
|
141
166
|
return `## Agent Profile
|
|
142
167
|
|
|
@@ -179,26 +204,32 @@ function renderSystemCard(sys) {
|
|
|
179
204
|
const rows = [];
|
|
180
205
|
const risk = computeSystemRisk(sys);
|
|
181
206
|
// Scopes
|
|
182
|
-
const scopes = sys.scopesRequested.filter(
|
|
183
|
-
rows.push(`| **Scopes granted** | ${scopes.length > 0 ? scopes.join(', ') :
|
|
184
|
-
const needed = sys.scopesNeeded.filter(
|
|
207
|
+
const scopes = sys.scopesRequested.filter(isProvided);
|
|
208
|
+
rows.push(`| **Scopes granted** | ${scopes.length > 0 ? scopes.join(', ') : `*${UNKNOWN_PLACEHOLDER}*`} |`);
|
|
209
|
+
const needed = sys.scopesNeeded.filter(isProvided);
|
|
185
210
|
if (needed.length > 0) {
|
|
186
211
|
rows.push(`| **Scopes needed** | ${needed.join(', ')} |`);
|
|
187
212
|
}
|
|
188
|
-
const excessive = sys.scopesDelta;
|
|
213
|
+
const excessive = sys.scopesDelta.filter(isProvided);
|
|
189
214
|
if (excessive.length > 0) {
|
|
190
215
|
rows.push(`| **Excessive** | ${excessive.join(', ')} |`);
|
|
191
216
|
}
|
|
192
217
|
// Data sensitivity
|
|
193
|
-
if (sys.dataSensitivity
|
|
218
|
+
if (isProvided(sys.dataSensitivity)) {
|
|
194
219
|
rows.push(`| **Data** | ${sys.dataSensitivity} |`);
|
|
195
220
|
}
|
|
221
|
+
else {
|
|
222
|
+
rows.push(`| **Data** | *${UNKNOWN_PLACEHOLDER}* |`);
|
|
223
|
+
}
|
|
196
224
|
// Blast radius
|
|
197
225
|
rows.push(`| **Blast radius** | ${sys.blastRadius} |`);
|
|
198
226
|
// Frequency
|
|
199
|
-
if (sys.frequencyAndVolume
|
|
227
|
+
if (isProvided(sys.frequencyAndVolume)) {
|
|
200
228
|
rows.push(`| **Frequency** | ${sys.frequencyAndVolume} |`);
|
|
201
229
|
}
|
|
230
|
+
else {
|
|
231
|
+
rows.push(`| **Frequency** | *${UNKNOWN_PLACEHOLDER}* |`);
|
|
232
|
+
}
|
|
202
233
|
// Write operations — inline in card
|
|
203
234
|
if (sys.writeOperations.length > 0) {
|
|
204
235
|
const writesSummary = sys.writeOperations.map(w => {
|
|
@@ -214,20 +245,87 @@ function renderSystemCard(sys) {
|
|
|
214
245
|
${rows.join('\n')}`;
|
|
215
246
|
}
|
|
216
247
|
// ─── Findings ───────────────────────────────────────────────────────────────
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
248
|
+
/**
|
|
249
|
+
* Infer which compliance finding type best matches a risk by keyword matching
|
|
250
|
+
* on the risk's title and description. Returns the top-matching finding type
|
|
251
|
+
* or undefined if no strong match.
|
|
252
|
+
*/
|
|
253
|
+
function inferFindingType(risk) {
|
|
254
|
+
const text = `${risk.title} ${risk.description}`.toLowerCase();
|
|
255
|
+
if (/permission|scope|access.?control|excessive|least.?privilege|oauth/i.test(text))
|
|
256
|
+
return 'excessive-access';
|
|
257
|
+
if (/write|irreversible|delete|create|modify|append/i.test(text))
|
|
258
|
+
return 'write-risk';
|
|
259
|
+
if (/pii|personal.?data|sensitive|privacy|data.?protection/i.test(text))
|
|
260
|
+
return 'sensitive-data';
|
|
261
|
+
if (/scope.?creep|purpose.?limit|beyond.*need|unnecessary/i.test(text))
|
|
262
|
+
return 'scope-creep';
|
|
263
|
+
if (/classif|decision|scor|rank|profil|bias|discriminat/i.test(text))
|
|
264
|
+
return 'decisions-about-people';
|
|
265
|
+
if (/regulat|compliance|health|sector/i.test(text))
|
|
266
|
+
return 'regulatory-flags';
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Get framework basis string for a finding type from the compliance flags.
|
|
271
|
+
* Returns top 3 mandatory framework controls, formatted as "GDPR Art. 25, EU AI Act Art. 10".
|
|
272
|
+
*/
|
|
273
|
+
function getFrameworkBasis(findingType, compliance) {
|
|
274
|
+
if (!compliance)
|
|
275
|
+
return '—';
|
|
276
|
+
const flags = compliance.all.filter((f) => f.triggeredBy === findingType && f.tier === 'mandatory');
|
|
277
|
+
if (flags.length === 0) {
|
|
278
|
+
// Try voluntary if no mandatory
|
|
279
|
+
const volFlags = compliance.all.filter((f) => f.triggeredBy === findingType);
|
|
280
|
+
if (volFlags.length === 0)
|
|
281
|
+
return '—';
|
|
282
|
+
return volFlags.slice(0, 3).map(f => `${f.frameworkId === 'eu-ai-act' ? 'EU AI Act' : f.framework.split(' — ')[0]}`).join(', ');
|
|
283
|
+
}
|
|
284
|
+
// Show top 3 mandatory, framework name + first control ID
|
|
285
|
+
return flags.slice(0, 3).map(f => {
|
|
286
|
+
const name = f.frameworkId === 'eu-ai-act' ? 'EU AI Act' : f.framework.split(' — ')[0];
|
|
287
|
+
const ctrl = (f.controlIds ?? [])[0] ?? '';
|
|
288
|
+
return ctrl ? `${name} ${ctrl}` : name;
|
|
289
|
+
}).join(', ');
|
|
290
|
+
}
|
|
291
|
+
function renderFindings(risks, compliance) {
|
|
292
|
+
if (risks.length === 0) {
|
|
293
|
+
return `## Findings\n\n_No risks identified._`;
|
|
294
|
+
}
|
|
295
|
+
const sorted = [...risks].sort((a, b) => severityOrder(b.severity) - severityOrder(a.severity));
|
|
296
|
+
const renderRow = (r, i) => {
|
|
222
297
|
const id = `HERON-${String(i + 1).padStart(3, '0')}`;
|
|
298
|
+
const findingType = inferFindingType(r);
|
|
299
|
+
const basis = findingType ? getFrameworkBasis(findingType, compliance) : '—';
|
|
223
300
|
const remediation = r.mitigation ?? '—';
|
|
224
|
-
return `| ${id} | ${r.severity.toUpperCase()} | ${r.title} | ${r.description} | ${remediation} |`;
|
|
225
|
-
}
|
|
301
|
+
return `| ${id} | ${r.severity.toUpperCase()} | ${basis} | ${r.title} | ${r.description} | ${remediation} |`;
|
|
302
|
+
};
|
|
303
|
+
const tableHeader = `| ID | Severity | Framework Basis | Finding | Description | Recommendation |
|
|
304
|
+
|----|----------|-----------------|---------|-------------|----------------|`;
|
|
305
|
+
// AAP-43 P2 #7: Top-N triage. A flat 4+ finding table reads as "everything
|
|
306
|
+
// is equal weight." A senior auditor triages: here's the real issue, and
|
|
307
|
+
// here's the long tail. Split at 3; fold the rest into a collapsed section
|
|
308
|
+
// so readers still have access without being buried.
|
|
309
|
+
if (sorted.length <= 3) {
|
|
310
|
+
const rows = sorted.map(renderRow).join('\n');
|
|
311
|
+
return `## Findings\n\n${tableHeader}\n${rows}`;
|
|
312
|
+
}
|
|
313
|
+
const top = sorted.slice(0, 3).map(renderRow).join('\n');
|
|
314
|
+
const rest = sorted.slice(3).map((r, i) => renderRow(r, i + 3)).join('\n');
|
|
226
315
|
return `## Findings
|
|
227
316
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
${
|
|
317
|
+
### Top 3 Findings
|
|
318
|
+
|
|
319
|
+
${tableHeader}
|
|
320
|
+
${top}
|
|
321
|
+
|
|
322
|
+
<details>
|
|
323
|
+
<summary><strong>Additional findings (${sorted.length - 3})</strong></summary>
|
|
324
|
+
|
|
325
|
+
${tableHeader}
|
|
326
|
+
${rest}
|
|
327
|
+
|
|
328
|
+
</details>`;
|
|
231
329
|
}
|
|
232
330
|
// ─── Positive Findings ─────────────────────────────────────────────────────
|
|
233
331
|
function renderPositiveFindings(report) {
|
|
@@ -239,8 +337,23 @@ function renderPositiveFindings(report) {
|
|
|
239
337
|
positives.push('All write operations are reversible');
|
|
240
338
|
}
|
|
241
339
|
// No excessive scopes
|
|
340
|
+
// Reviewer feedback (2026-04-25): a single report had both
|
|
341
|
+
// "No excessive permissions detected" AND a HIGH "Broad Google OAuth
|
|
342
|
+
// write scope exceeds stated single-sheet/single-folder need" finding —
|
|
343
|
+
// a direct internal contradiction. Root cause: the LLM put the broad-
|
|
344
|
+
// scope finding into `risks` (with a HIGH severity) but did not populate
|
|
345
|
+
// `scopesDelta`, so the structural counter said zero excessive scopes.
|
|
346
|
+
// Gate the positive on BOTH: zero scopesDelta entries AND no high-
|
|
347
|
+
// severity risk that the finding-type inferrer classifies as access /
|
|
348
|
+
// excessive-permissions / scope-creep.
|
|
242
349
|
const totalExcessive = systems.reduce((n, s) => n + s.scopesDelta.length, 0);
|
|
243
|
-
|
|
350
|
+
const hasAccessRisk = report.risks.some((r) => {
|
|
351
|
+
if (r.severity !== 'high' && r.severity !== 'critical')
|
|
352
|
+
return false;
|
|
353
|
+
const t = inferFindingType(r);
|
|
354
|
+
return t === 'excessive-access' || t === 'scope-creep';
|
|
355
|
+
});
|
|
356
|
+
if (totalExcessive === 0 && systems.length > 0 && !hasAccessRisk) {
|
|
244
357
|
positives.push('No excessive permissions detected — follows least-privilege principle');
|
|
245
358
|
}
|
|
246
359
|
// Limited blast radius
|
|
@@ -290,14 +403,14 @@ function renderVerdict(report) {
|
|
|
290
403
|
if (!isBusinessSystem(sys))
|
|
291
404
|
continue;
|
|
292
405
|
for (const scope of sys.scopesDelta) {
|
|
293
|
-
if (scope
|
|
406
|
+
if (isProvided(scope)) {
|
|
294
407
|
if (!excessiveBySystem.has(sys.systemId))
|
|
295
408
|
excessiveBySystem.set(sys.systemId, []);
|
|
296
409
|
excessiveBySystem.get(sys.systemId).push(scope);
|
|
297
410
|
}
|
|
298
411
|
}
|
|
299
412
|
for (const scope of sys.scopesNeeded) {
|
|
300
|
-
if (!sys.scopesRequested.includes(scope) && scope
|
|
413
|
+
if (!sys.scopesRequested.includes(scope) && isProvided(scope)) {
|
|
301
414
|
if (!missingBySystem.has(sys.systemId))
|
|
302
415
|
missingBySystem.set(sys.systemId, []);
|
|
303
416
|
missingBySystem.get(sys.systemId).push(scope);
|
|
@@ -337,36 +450,275 @@ ${items}
|
|
|
337
450
|
|
|
338
451
|
</details>`;
|
|
339
452
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
453
|
+
const CATEGORIES = [
|
|
454
|
+
{ key: 'privacy', title: 'Privacy' },
|
|
455
|
+
{ key: 'ip', title: 'IP' },
|
|
456
|
+
{ key: 'consumer-protection', title: 'Consumer Protection' },
|
|
457
|
+
{ key: 'sector-specific', title: 'Sector-Specific' },
|
|
458
|
+
];
|
|
459
|
+
// ─── Applicability Summary Table ─────────────────────────────────────────
|
|
460
|
+
/** Human-readable descriptions for why a framework didn't fire. */
|
|
461
|
+
const NOT_TRIGGERED_REASONS = {
|
|
462
|
+
'gdpr': 'No personal data signals detected',
|
|
463
|
+
'eu-ai-act': 'No applicable signals detected',
|
|
464
|
+
};
|
|
465
|
+
/** Short applicability condition for mandatory frameworks that DID fire. */
|
|
466
|
+
const APPLICABILITY_CONDITIONS = {
|
|
467
|
+
'gdpr': 'If you serve EU data subjects',
|
|
468
|
+
'eu-ai-act': 'If AI placed on EU market or outputs used in EU',
|
|
469
|
+
};
|
|
470
|
+
/** Map finding types to human-readable gap descriptions. */
|
|
471
|
+
const GAP_LABELS = {
|
|
472
|
+
'excessive-access': 'Excessive permissions',
|
|
473
|
+
'write-risk': 'Write operation risks',
|
|
474
|
+
'sensitive-data': 'Data handling',
|
|
475
|
+
'scope-creep': 'Scope exceeds purpose',
|
|
476
|
+
'decisions-about-people': 'Automated decision-making',
|
|
477
|
+
'regulatory-flags': 'Regulatory concerns',
|
|
478
|
+
};
|
|
479
|
+
/** Excluded from gap counting — always fires as methodology anchor, not a real gap. */
|
|
480
|
+
const GAP_EXCLUDED = new Set(['risk-score']);
|
|
481
|
+
function getGaps(frameworkId, allFlags) {
|
|
482
|
+
const flags = allFlags.filter(f => f.frameworkId === frameworkId && !GAP_EXCLUDED.has(f.triggeredBy));
|
|
483
|
+
// Also exclude decisions-about-people when it says "No decisions" (impact = none)
|
|
484
|
+
const meaningful = flags.filter(f => !(f.triggeredBy === 'decisions-about-people' && /no decisions about people/i.test(f.description)));
|
|
485
|
+
const uniqueTypes = [...new Set(meaningful.map(f => f.triggeredBy))];
|
|
486
|
+
return uniqueTypes.map(t => GAP_LABELS[t] ?? t);
|
|
487
|
+
}
|
|
488
|
+
function formatGaps(gaps) {
|
|
489
|
+
if (gaps.length === 0)
|
|
490
|
+
return { status: '✅ No gaps', details: '—' };
|
|
491
|
+
return {
|
|
492
|
+
status: `⚠️ ${gaps.length} gap${gaps.length > 1 ? 's' : ''}`,
|
|
493
|
+
details: gaps.join(', '),
|
|
354
494
|
};
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
495
|
+
}
|
|
496
|
+
function renderApplicabilitySummary(c) {
|
|
497
|
+
const activated = new Set(c.frameworksActivated ?? []);
|
|
498
|
+
const allFlags = (c.all ?? []);
|
|
499
|
+
const mandatoryFrameworks = [
|
|
500
|
+
{ id: 'eu-ai-act', name: 'EU AI Act' },
|
|
501
|
+
{ id: 'gdpr', name: 'GDPR' },
|
|
502
|
+
];
|
|
503
|
+
const voluntaryFrameworks = [
|
|
504
|
+
{ id: 'iso-42001', name: 'ISO/IEC 42001' },
|
|
505
|
+
{ id: 'aiuc-1', name: 'AIUC-1 (Q2-2026)' },
|
|
506
|
+
{ id: 'nist-ai-rmf', name: 'NIST AI RMF' },
|
|
507
|
+
];
|
|
508
|
+
// EU AI Act classification scope label — single line replaces the prior
|
|
509
|
+
// two-entry split (`eu-ai-act` + `eu-ai-act-high-risk`).
|
|
510
|
+
const euClassification = c.euAiActClassification;
|
|
511
|
+
const mandatoryRows = mandatoryFrameworks.map(fw => {
|
|
512
|
+
const isActive = activated.has(fw.id);
|
|
513
|
+
if (!isActive) {
|
|
514
|
+
const reason = NOT_TRIGGERED_REASONS[fw.id] ?? 'No matching signals';
|
|
515
|
+
return `| ${fw.name} | ✅ Not applicable | ${reason} |`;
|
|
516
|
+
}
|
|
517
|
+
const gaps = getGaps(fw.id, allFlags);
|
|
518
|
+
let displayName = fw.name;
|
|
519
|
+
if (fw.id === 'eu-ai-act' && euClassification) {
|
|
520
|
+
const cls = euClassification.classification;
|
|
521
|
+
if (cls === 'high-risk' && euClassification.annexIIICategories.length > 0) {
|
|
522
|
+
displayName = `${fw.name} — High-Risk (Annex III ${euClassification.annexIIICategories.join(', ')})`;
|
|
523
|
+
}
|
|
524
|
+
else if (cls === 'limited') {
|
|
525
|
+
displayName = `${fw.name} — Limited-Risk (Art. 50 transparency)`;
|
|
526
|
+
}
|
|
527
|
+
else if (cls === 'prohibited') {
|
|
528
|
+
displayName = `${fw.name} — Prohibited Practice`;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (gaps.length > 0) {
|
|
532
|
+
const condition = APPLICABILITY_CONDITIONS[fw.id] ?? '';
|
|
533
|
+
return `| ${displayName} | ⚠️ ${gaps.length} gap${gaps.length > 1 ? 's' : ''} | ${gaps.join(', ')} — ${condition} |`;
|
|
534
|
+
}
|
|
535
|
+
const condition = APPLICABILITY_CONDITIONS[fw.id] ?? 'Check applicability';
|
|
536
|
+
return `| ${displayName} | ⚠️ Check | ${condition} |`;
|
|
537
|
+
});
|
|
538
|
+
const voluntaryRows = voluntaryFrameworks.map(fw => {
|
|
539
|
+
const gaps = getGaps(fw.id, allFlags);
|
|
540
|
+
const { status, details } = formatGaps(gaps);
|
|
541
|
+
return `| ${fw.name} | ${status} | ${details} |`;
|
|
542
|
+
});
|
|
543
|
+
return `### Applicability Summary
|
|
364
544
|
|
|
365
|
-
|
|
545
|
+
| Framework | Status | Gaps Found |
|
|
546
|
+
|-----------|--------|------------|
|
|
547
|
+
| **Mandatory Law** | | |
|
|
548
|
+
${mandatoryRows.join('\n')}
|
|
549
|
+
| **Voluntary Frameworks** | | |
|
|
550
|
+
${voluntaryRows.join('\n')}`;
|
|
551
|
+
}
|
|
552
|
+
// ─── Finding-first detail (replaces framework-first tier sections) ────────
|
|
553
|
+
/**
|
|
554
|
+
* Build agent-specific gap description from actual report data.
|
|
555
|
+
* Falls back to generic text if no specific context available.
|
|
556
|
+
*/
|
|
557
|
+
function buildGapDescription(findingType, report) {
|
|
558
|
+
const systems = report?.systems?.filter(isBusinessSystem) ?? [];
|
|
559
|
+
const systemNames = systems.map(s => s.systemId).join(', ');
|
|
560
|
+
const excessiveScopes = systems.flatMap(s => s.scopesDelta?.map(d => `${s.systemId}: ${d}`) ?? []);
|
|
561
|
+
const writes = systems.flatMap(s => s.writeOperations?.map(w => `${w.operation} → ${w.target}`) ?? []);
|
|
562
|
+
const hasIrreversible = systems.some(s => s.writeOperations?.some(w => !w.reversible));
|
|
563
|
+
const dataSensitivities = [...new Set(systems.map(s => s.dataSensitivity).filter(Boolean))];
|
|
564
|
+
const decisionDetails = report?.decisionMakingDetails ?? '';
|
|
565
|
+
switch (findingType) {
|
|
566
|
+
case 'excessive-access':
|
|
567
|
+
if (excessiveScopes.length > 0) {
|
|
568
|
+
return `Agent holds permissions beyond stated need on ${systems.length} system(s). Excessive scopes detected: ${excessiveScopes.join('; ')}. Narrow each to the minimum required scope.`;
|
|
569
|
+
}
|
|
570
|
+
return `Agent holds permissions beyond stated need on ${systemNames || 'connected systems'}. Review and narrow scopes to the minimum required (least-privilege).`;
|
|
571
|
+
case 'write-risk':
|
|
572
|
+
if (writes.length > 0) {
|
|
573
|
+
const qualifier = hasIrreversible ? 'including irreversible operations' : 'all reported as reversible';
|
|
574
|
+
return `Agent performs ${writes.length} write operation(s) (${qualifier}): ${writes.join('; ')}. Require approval, monitoring, and rollback paths for high-impact operations.`;
|
|
575
|
+
}
|
|
576
|
+
return 'Write operations detected that can affect users or downstream systems. Require approval, monitoring, and rollback paths.';
|
|
577
|
+
case 'sensitive-data':
|
|
578
|
+
if (dataSensitivities.length > 0) {
|
|
579
|
+
return `Agent processes ${dataSensitivities.join(', ')} data across ${systemNames || 'connected systems'}. Ensure lawful basis under GDPR Art. 6, data minimization (Art. 5(1)(c)), and breach-readiness (Art. 33).`;
|
|
580
|
+
}
|
|
581
|
+
return 'Agent processes personal data. Ensure lawful basis, data minimization, and breach-readiness.';
|
|
582
|
+
case 'scope-creep':
|
|
583
|
+
return `Requested scopes on ${systemNames || 'one or more systems'} exceed what is needed for the stated purpose. Review purpose-limitation (GDPR Art. 5(1)(b)) and change-management process.`;
|
|
584
|
+
case 'decisions-about-people':
|
|
585
|
+
if (decisionDetails) {
|
|
586
|
+
return `Agent makes or influences automated decisions affecting individuals: "${decisionDetails.slice(0, 150)}". Requires human oversight, contestability, transparency, and data-subject rights (GDPR Art. 22).`;
|
|
587
|
+
}
|
|
588
|
+
return 'Agent makes or influences automated decisions affecting individuals. Requires human oversight, contestability, transparency, and data-subject rights.';
|
|
589
|
+
case 'regulatory-flags':
|
|
590
|
+
return 'Agent may operate in a regulated domain. Clarify the agent\'s domain to determine sector-specific obligations.';
|
|
591
|
+
default:
|
|
592
|
+
return '';
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
/** Short framework display names for the "Affects" line. */
|
|
596
|
+
function frameworkShortName(id) {
|
|
597
|
+
const names = {
|
|
598
|
+
'eu-ai-act': 'EU AI Act',
|
|
599
|
+
'gdpr': 'GDPR',
|
|
600
|
+
'iso-42001': 'ISO 42001',
|
|
601
|
+
'aiuc-1': 'AIUC-1 (Q2-2026)',
|
|
602
|
+
'nist-ai-rmf': 'NIST AI RMF',
|
|
603
|
+
};
|
|
604
|
+
return names[id] ?? id;
|
|
605
|
+
}
|
|
606
|
+
function renderFindingFirstDetail(c, report) {
|
|
607
|
+
const allFlags = (c.all ?? []);
|
|
608
|
+
// Group flags by finding type (triggeredBy)
|
|
609
|
+
const byFinding = new Map();
|
|
610
|
+
for (const f of allFlags) {
|
|
611
|
+
if (GAP_EXCLUDED.has(f.triggeredBy))
|
|
612
|
+
continue;
|
|
613
|
+
if (f.triggeredBy === 'decisions-about-people' && /no decisions about people/i.test(f.description))
|
|
614
|
+
continue;
|
|
615
|
+
const arr = byFinding.get(f.triggeredBy) ?? [];
|
|
616
|
+
arr.push(f);
|
|
617
|
+
byFinding.set(f.triggeredBy, arr);
|
|
618
|
+
}
|
|
619
|
+
if (byFinding.size === 0) {
|
|
620
|
+
return `### Compliance Detail\n\n_No compliance gaps identified from current signals._\n`;
|
|
621
|
+
}
|
|
622
|
+
let out = `### Compliance Detail\n\n`;
|
|
623
|
+
for (const [findingType, flags] of byFinding) {
|
|
624
|
+
const label = GAP_LABELS[findingType] ?? findingType;
|
|
625
|
+
const description = buildGapDescription(findingType, report);
|
|
626
|
+
// Group controls by framework for the "Affects" line.
|
|
627
|
+
// Reviewer feedback (2026-04-25): the prior "+N more" truncation
|
|
628
|
+
// ("AIUC-1 (A001, A002, A005, +1 more)") hides the very citations the
|
|
629
|
+
// report is asserting — in an audit deliverable, you don't redact
|
|
630
|
+
// your evidence. The earlier AAP-43 P2 #9 cap (3 per framework) was
|
|
631
|
+
// motivated by readability, not by citation hygiene. With the
|
|
632
|
+
// table-layout: fixed + overflow-wrap CSS now in place, long control
|
|
633
|
+
// lists wrap cleanly inside their cells, so we show the full list.
|
|
634
|
+
const byFramework = new Map();
|
|
635
|
+
for (const f of flags) {
|
|
636
|
+
const fwName = frameworkShortName(f.frameworkId);
|
|
637
|
+
const existing = byFramework.get(fwName) ?? [];
|
|
638
|
+
for (const ctrl of (f.controlIds ?? [])) {
|
|
639
|
+
if (!existing.includes(ctrl))
|
|
640
|
+
existing.push(ctrl);
|
|
641
|
+
}
|
|
642
|
+
byFramework.set(fwName, existing);
|
|
643
|
+
}
|
|
644
|
+
const affectsParts = [...byFramework.entries()].map(([fw, ctrls]) => ctrls.length === 0 ? fw : `${fw} (${ctrls.join(', ')})`);
|
|
645
|
+
out += `#### ${label}\n\n`;
|
|
646
|
+
out += `${description}\n\n`;
|
|
647
|
+
out += `**Affects:** ${affectsParts.join(' · ')}\n\n`;
|
|
648
|
+
}
|
|
649
|
+
return out;
|
|
650
|
+
}
|
|
651
|
+
// ─── Obligations Requiring Further Review ─────────────────────────────────
|
|
652
|
+
function renderObligationsChecklist(c, report) {
|
|
653
|
+
const activated = new Set(c.frameworksActivated ?? []);
|
|
654
|
+
const rows = [];
|
|
655
|
+
// AAP-43 P1 #3: GDPR obligations are signal-gated, not dumped as a 14-row
|
|
656
|
+
// boilerplate. Each row requires an explicit signal; if no PII/decisions/
|
|
657
|
+
// transfer signals fire, the table is skipped entirely.
|
|
658
|
+
const hasGdpr = activated.has('gdpr');
|
|
659
|
+
const signals = c.signals;
|
|
660
|
+
if (hasGdpr && signals) {
|
|
661
|
+
// ── PII-driven obligations ──────────────────────────────────────────
|
|
662
|
+
if (signals.hasPII) {
|
|
663
|
+
rows.push({ obligation: 'GDPR Art. 6', action: 'Decide and document WHY you are allowed to process this data (e.g. legitimate business interest — must document a balancing test)' });
|
|
664
|
+
rows.push({ obligation: 'GDPR Art. 13/14', action: 'Tell people you are collecting their data: what, why, how long, and their rights' });
|
|
665
|
+
rows.push({ obligation: 'GDPR Art. 15', action: 'Be ready to show someone all data you hold on them if they ask' });
|
|
666
|
+
rows.push({ obligation: 'GDPR Art. 17', action: "Be ready to delete someone's data from all systems if they ask" });
|
|
667
|
+
rows.push({ obligation: 'GDPR Art. 30', action: 'Keep a written log of what personal data you process, why, and who has access' });
|
|
668
|
+
rows.push({ obligation: 'GDPR Art. 5(1)(e)', action: 'Set rules for how long you keep data — then actually delete it on schedule' });
|
|
669
|
+
}
|
|
670
|
+
// ── Profiling / automated decisions ─────────────────────────────────
|
|
671
|
+
if (signals.hasDecisionsAboutPeople) {
|
|
672
|
+
rows.push({ obligation: 'GDPR Art. 21', action: 'Let people opt out of being profiled for sales/marketing — you must stop if they object' });
|
|
673
|
+
}
|
|
674
|
+
// ── Processor contracts ─────────────────────────────────────────────
|
|
675
|
+
if (signals.hasPII && signals.hasExternalProcessors) {
|
|
676
|
+
rows.push({ obligation: 'GDPR Art. 28', action: 'Sign data processing contracts with every service you send data to (Google, Apify, etc.)' });
|
|
677
|
+
}
|
|
678
|
+
// ── DPIA: large-scale OR decisions OR sensitive PII ─────────────────
|
|
679
|
+
if (signals.hasLargeScaleProcessing || signals.hasDecisionsAboutPeople || signals.hasSensitivePII) {
|
|
680
|
+
rows.push({ obligation: 'GDPR Art. 35', action: 'Do a privacy impact assessment before going live (large-scale / profiling / sensitive data → likely required)' });
|
|
681
|
+
}
|
|
682
|
+
// ── International transfer ──────────────────────────────────────────
|
|
683
|
+
if (signals.hasPII && signals.hasInternationalTransfer) {
|
|
684
|
+
rows.push({ obligation: 'GDPR Arts. 44-49', action: 'Data leaves the EU (e.g. to US-based Google/Apify) — you need a legal basis for that transfer (SCCs, adequacy decision, etc.)' });
|
|
685
|
+
}
|
|
686
|
+
// ── Art. 22 automated-decisions safeguard ───────────────────────────
|
|
687
|
+
if (signals.hasDecisionsAboutPeople) {
|
|
688
|
+
rows.push({ obligation: 'GDPR Art. 22', action: 'AI makes decisions about people: ensure a human can review, people can contest, and the logic is explainable' });
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
// Always applicable — baseline operational obligations
|
|
692
|
+
rows.push({ obligation: 'Credentials', action: 'Store API keys/tokens in a secrets manager (not in code or env files), rotate them regularly' });
|
|
693
|
+
rows.push({ obligation: 'Platform ToS', action: 'Check you are not violating the rules of LinkedIn, Google, or other connected services (scraping, rate limits, usage policies)' });
|
|
694
|
+
rows.push({ obligation: 'Incident response', action: 'Have a plan: if data leaks, who do you notify and within what timeframe? (EU: 72 hours to regulator)' });
|
|
695
|
+
if (rows.length === 0)
|
|
696
|
+
return '';
|
|
697
|
+
const tableRows = rows.map(r => `| ${r.obligation} | ${r.action} |`).join('\n');
|
|
698
|
+
return `### Obligations Requiring Further Review
|
|
366
699
|
|
|
367
|
-
|
|
700
|
+
The following cannot be assessed from this interview alone — the deployer must address independently:
|
|
368
701
|
|
|
369
|
-
|
|
702
|
+
| Obligation | Action Required |
|
|
703
|
+
|------------|-----------------|
|
|
704
|
+
${tableRows}`;
|
|
705
|
+
}
|
|
706
|
+
export function renderStructuredCompliance(c, report) {
|
|
707
|
+
return [
|
|
708
|
+
`## Regulatory Compliance`,
|
|
709
|
+
``,
|
|
710
|
+
`### Methodology`,
|
|
711
|
+
``,
|
|
712
|
+
`Findings are anchored to EU AI Act 2024/1689, GDPR 2016/679, ISO/IEC 42001 (AI management system), AIUC-1 (agent-native standard, pinned to Q2-2026 release 2026-04-15), and NIST AI RMF 1.0 (US-origin voluntary risk-management framework; GOVERN/MAP/MEASURE/MANAGE). Mapping version: \`${c.mappingVersion}\`. EU AI Act is a single framework entry; Annex III high-risk obligations are surfaced as a classification scope label on that entry (replacing the prior two-entry split). Control mappings are indicative — they show which framework clauses a finding typically activates and do not constitute legal advice.`,
|
|
713
|
+
``,
|
|
714
|
+
renderApplicabilitySummary(c),
|
|
715
|
+
``,
|
|
716
|
+
renderFindingFirstDetail(c, report),
|
|
717
|
+
renderObligationsChecklist(c, report),
|
|
718
|
+
].join('\n');
|
|
719
|
+
}
|
|
720
|
+
function renderRegulatoryCompliance(compliance, report) {
|
|
721
|
+
return renderStructuredCompliance(compliance, report);
|
|
370
722
|
}
|
|
371
723
|
// ─── Disclaimer ─────────────────────────────────────────────────────────────
|
|
372
724
|
function renderDisclaimer() {
|