@prism-d1/cli 1.0.27 → 1.0.28
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/assets/eval-harness/README.md +114 -0
- package/dist/assets/eval-harness/eval-config.json +10 -0
- package/dist/assets/eval-harness/rubrics/agent-quality.json +79 -0
- package/dist/assets/eval-harness/rubrics/api-response-quality.json +45 -0
- package/dist/assets/eval-harness/rubrics/code-quality.json +98 -0
- package/dist/assets/eval-harness/rubrics/security-compliance.json +145 -0
- package/dist/assets/eval-harness/rubrics/spec-compliance.json +67 -0
- package/dist/assets/eval-harness/run-eval.sh +122 -0
- package/dist/assets/github-workflows/README.md +110 -0
- package/dist/assets/github-workflows/prism-agent-eval.yml +313 -0
- package/dist/assets/github-workflows/prism-ai-metrics.yml +261 -0
- package/dist/assets/github-workflows/prism-dora-weekly.yml +334 -0
- package/dist/assets/github-workflows/prism-eval-gate.yml +310 -0
- package/dist/assets/infra/bin/app.ts +56 -0
- package/dist/assets/infra/cdk.json +12 -0
- package/dist/assets/infra/lib/api-stack.ts +347 -0
- package/dist/assets/infra/lib/constructs/bedrock-guardrail-construct.ts +201 -0
- package/dist/assets/infra/lib/constructs/guardrail-enforcer-construct.ts +59 -0
- package/dist/assets/infra/lib/constructs/prism-vpc-construct.ts +75 -0
- package/dist/assets/infra/lib/constructs/security-agent-construct.ts +266 -0
- package/dist/assets/infra/lib/dashboard-stack.ts +1392 -0
- package/dist/assets/infra/lib/lambda/api-handler.ts +477 -0
- package/dist/assets/infra/lib/lambda/defect-correlator.ts +142 -0
- package/dist/assets/infra/lib/lambda/exfiltration-detector.ts +100 -0
- package/dist/assets/infra/lib/lambda/layers/guardrail-enforcer/nodejs/guardrail-enforcer.js +53 -0
- package/dist/assets/infra/lib/lambda/metrics-processor.ts +748 -0
- package/dist/assets/infra/lib/lambda/security-agent-processor.ts +231 -0
- package/dist/assets/infra/lib/lambda/security-remediation-tracker.ts +120 -0
- package/dist/assets/infra/lib/lambda/security-response-automator.ts +130 -0
- package/dist/assets/infra/lib/lambda/spec-to-code-calculator.ts +123 -0
- package/dist/assets/infra/lib/metrics-pipeline-stack.ts +701 -0
- package/dist/assets/infra/package.json +23 -0
- package/dist/assets/infra/tsconfig.json +24 -0
- package/dist/src/commands/bootstrapper/install-eval-harness.d.ts.map +1 -1
- package/dist/src/commands/bootstrapper/install-eval-harness.js +3 -4
- package/dist/src/commands/bootstrapper/install-eval-harness.js.map +1 -1
- package/dist/src/commands/bootstrapper/install-git-hooks.d.ts.map +1 -1
- package/dist/src/commands/bootstrapper/install-git-hooks.js +2 -5
- package/dist/src/commands/bootstrapper/install-git-hooks.js.map +1 -1
- package/dist/src/commands/securityagent/setup.d.ts.map +1 -1
- package/dist/src/commands/securityagent/setup.js +2 -3
- package/dist/src/commands/securityagent/setup.js.map +1 -1
- package/dist/src/commands/workshop/deploy-infra.d.ts.map +1 -1
- package/dist/src/commands/workshop/deploy-infra.js +2 -3
- package/dist/src/commands/workshop/deploy-infra.js.map +1 -1
- package/dist/src/commands/workshop/generate-demo-data.d.ts.map +1 -1
- package/dist/src/commands/workshop/generate-demo-data.js +3 -8
- package/dist/src/commands/workshop/generate-demo-data.js.map +1 -1
- package/dist/src/commands/workshop/perform-pen-test.d.ts.map +1 -1
- package/dist/src/commands/workshop/perform-pen-test.js +5 -14
- package/dist/src/commands/workshop/perform-pen-test.js.map +1 -1
- package/dist/src/utils/root.d.ts +6 -0
- package/dist/src/utils/root.d.ts.map +1 -1
- package/dist/src/utils/root.js +29 -0
- package/dist/src/utils/root.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DynamoDBClient,
|
|
3
|
+
QueryCommand,
|
|
4
|
+
ScanCommand,
|
|
5
|
+
} from '@aws-sdk/client-dynamodb';
|
|
6
|
+
import {
|
|
7
|
+
EventBridgeClient,
|
|
8
|
+
PutEventsCommand,
|
|
9
|
+
} from '@aws-sdk/client-eventbridge';
|
|
10
|
+
|
|
11
|
+
const dynamoClient = new DynamoDBClient({});
|
|
12
|
+
const eventBridgeClient = new EventBridgeClient({});
|
|
13
|
+
|
|
14
|
+
const EVENTS_TABLE = process.env.EVENTS_TABLE!;
|
|
15
|
+
const METADATA_TABLE = process.env.METADATA_TABLE!;
|
|
16
|
+
const EVENT_BUS_NAME = process.env.EVENT_BUS_NAME!;
|
|
17
|
+
|
|
18
|
+
interface SecurityAgentPayload {
|
|
19
|
+
findings: Array<{
|
|
20
|
+
id: string;
|
|
21
|
+
type: string; // 'design_review' | 'code_review' | 'pen_test'
|
|
22
|
+
severity: string;
|
|
23
|
+
cvss?: number;
|
|
24
|
+
title: string;
|
|
25
|
+
description: string;
|
|
26
|
+
category: string;
|
|
27
|
+
cwe_id?: string;
|
|
28
|
+
exploit_validated?: boolean;
|
|
29
|
+
remediation: string;
|
|
30
|
+
compliance?: string[];
|
|
31
|
+
repository: string;
|
|
32
|
+
pr_number?: number;
|
|
33
|
+
commit_sha?: string;
|
|
34
|
+
spec_ref?: string;
|
|
35
|
+
environment?: string;
|
|
36
|
+
found_at: string;
|
|
37
|
+
}>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Processes AWS Security Agent findings.
|
|
42
|
+
* Normalizes payloads, enriches with PRISM context (team_id, ai_origin),
|
|
43
|
+
* and emits prism.d1.security.<phase> events to EventBridge.
|
|
44
|
+
*
|
|
45
|
+
* Triggered by:
|
|
46
|
+
* - POST /security-findings (webhook from Security Agent)
|
|
47
|
+
* - Scheduled poll (15-min fallback for preview)
|
|
48
|
+
*/
|
|
49
|
+
export async function handler(event: any): Promise<any> {
|
|
50
|
+
// Handle API Gateway events
|
|
51
|
+
let payload: SecurityAgentPayload;
|
|
52
|
+
if (event.body) {
|
|
53
|
+
payload = JSON.parse(typeof event.body === 'string' ? event.body : JSON.stringify(event.body));
|
|
54
|
+
} else if (event.findings) {
|
|
55
|
+
payload = event;
|
|
56
|
+
} else {
|
|
57
|
+
console.log('No findings in event, skipping');
|
|
58
|
+
return { statusCode: 200, body: JSON.stringify({ message: 'No findings' }) };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(`Processing ${payload.findings.length} Security Agent findings`);
|
|
62
|
+
|
|
63
|
+
const entries = [];
|
|
64
|
+
|
|
65
|
+
for (const finding of payload.findings) {
|
|
66
|
+
const phase = normalizePhase(finding.type);
|
|
67
|
+
const detailType = `prism.d1.security.${phase}`;
|
|
68
|
+
|
|
69
|
+
// Enrich with team_id from metadata table
|
|
70
|
+
const teamId = await lookupTeamId(finding.repository);
|
|
71
|
+
|
|
72
|
+
// Enrich with AI origin from commit events
|
|
73
|
+
const aiOrigin = await lookupAiOrigin(
|
|
74
|
+
teamId,
|
|
75
|
+
finding.repository,
|
|
76
|
+
finding.commit_sha ?? null,
|
|
77
|
+
finding.pr_number ?? null,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const prismEvent = {
|
|
81
|
+
team_id: teamId,
|
|
82
|
+
repo: finding.repository,
|
|
83
|
+
timestamp: finding.found_at,
|
|
84
|
+
prism_level: 3,
|
|
85
|
+
metric: {
|
|
86
|
+
name: 'security_finding',
|
|
87
|
+
value: 1,
|
|
88
|
+
unit: 'count',
|
|
89
|
+
},
|
|
90
|
+
ai_context: {
|
|
91
|
+
tool: 'security-agent',
|
|
92
|
+
model: 'aws-security-agent',
|
|
93
|
+
origin: aiOrigin,
|
|
94
|
+
},
|
|
95
|
+
security_agent_finding: {
|
|
96
|
+
finding_id: finding.id,
|
|
97
|
+
phase,
|
|
98
|
+
severity: finding.severity.toUpperCase(),
|
|
99
|
+
cvss_score: finding.cvss ?? null,
|
|
100
|
+
title: finding.title,
|
|
101
|
+
description: finding.description,
|
|
102
|
+
category: finding.category,
|
|
103
|
+
cwe_id: finding.cwe_id ?? null,
|
|
104
|
+
exploit_validated: finding.exploit_validated ?? false,
|
|
105
|
+
remediation_guidance: finding.remediation,
|
|
106
|
+
compliance_mappings: finding.compliance ?? [],
|
|
107
|
+
ai_origin: aiOrigin,
|
|
108
|
+
pr_number: finding.pr_number ?? null,
|
|
109
|
+
commit_sha: finding.commit_sha ?? null,
|
|
110
|
+
spec_ref: finding.spec_ref ?? null,
|
|
111
|
+
environment: finding.environment ?? 'unknown',
|
|
112
|
+
found_at: finding.found_at,
|
|
113
|
+
remediated_at: null,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
entries.push({
|
|
118
|
+
Source: 'prism.d1.velocity',
|
|
119
|
+
DetailType: detailType,
|
|
120
|
+
EventBusName: EVENT_BUS_NAME,
|
|
121
|
+
Detail: JSON.stringify(prismEvent),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Emit in batches of 10
|
|
126
|
+
for (let i = 0; i < entries.length; i += 10) {
|
|
127
|
+
await eventBridgeClient.send(
|
|
128
|
+
new PutEventsCommand({ Entries: entries.slice(i, i + 10) }),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log(`Emitted ${entries.length} security finding events`);
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
statusCode: 200,
|
|
136
|
+
body: JSON.stringify({ message: 'OK', findingsProcessed: entries.length }),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function normalizePhase(type: string): string {
|
|
141
|
+
const phaseMap: Record<string, string> = {
|
|
142
|
+
design_review: 'design_review',
|
|
143
|
+
code_review: 'code_review',
|
|
144
|
+
pen_test: 'pen_test',
|
|
145
|
+
penetration_test: 'pen_test',
|
|
146
|
+
design: 'design_review',
|
|
147
|
+
code: 'code_review',
|
|
148
|
+
pentest: 'pen_test',
|
|
149
|
+
};
|
|
150
|
+
return phaseMap[type.toLowerCase()] ?? 'code_review';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function lookupTeamId(repo: string): Promise<string> {
|
|
154
|
+
try {
|
|
155
|
+
// The metadata table has PK=team_id, SK=repo.
|
|
156
|
+
// We can't query by repo directly without a GSI, so we scan
|
|
157
|
+
// with a filter. This is acceptable because the metadata table
|
|
158
|
+
// has one row per team+repo (small table).
|
|
159
|
+
const result = await dynamoClient.send(
|
|
160
|
+
new ScanCommand({
|
|
161
|
+
TableName: METADATA_TABLE,
|
|
162
|
+
FilterExpression: 'repo = :repo',
|
|
163
|
+
ExpressionAttributeValues: {
|
|
164
|
+
':repo': { S: repo },
|
|
165
|
+
},
|
|
166
|
+
Limit: 1,
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
if (result.Items && result.Items.length > 0) {
|
|
171
|
+
return result.Items[0].team_id?.S ?? 'unknown';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return 'unknown';
|
|
175
|
+
} catch {
|
|
176
|
+
return 'unknown';
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function lookupAiOrigin(
|
|
181
|
+
teamId: string,
|
|
182
|
+
repo: string,
|
|
183
|
+
commitSha: string | null,
|
|
184
|
+
prNumber: number | null,
|
|
185
|
+
): Promise<'ai-generated' | 'ai-assisted' | 'human' | 'unknown'> {
|
|
186
|
+
if (!commitSha && !prNumber) return 'unknown';
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
// Query recent commit events for this repo to find AI origin
|
|
190
|
+
const pk = `${teamId}#${repo}`;
|
|
191
|
+
const now = new Date();
|
|
192
|
+
const lookback = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); // 7 days
|
|
193
|
+
|
|
194
|
+
const result = await dynamoClient.send(
|
|
195
|
+
new QueryCommand({
|
|
196
|
+
TableName: EVENTS_TABLE,
|
|
197
|
+
KeyConditionExpression: 'pk = :pk AND sk >= :start',
|
|
198
|
+
FilterExpression: 'detail_type = :dt',
|
|
199
|
+
ExpressionAttributeValues: {
|
|
200
|
+
':pk': { S: pk },
|
|
201
|
+
':start': { S: lookback.toISOString() },
|
|
202
|
+
':dt': { S: 'prism.d1.commit' },
|
|
203
|
+
},
|
|
204
|
+
ScanIndexForward: false, // Most recent first
|
|
205
|
+
Limit: 20,
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
if (result.Items && result.Items.length > 0) {
|
|
210
|
+
// Count AI vs human commits to determine predominant origin
|
|
211
|
+
let aiCount = 0;
|
|
212
|
+
let humanCount = 0;
|
|
213
|
+
for (const item of result.Items) {
|
|
214
|
+
const data = JSON.parse(item.data?.S ?? '{}');
|
|
215
|
+
const origin = data.ai_context?.origin ?? 'human';
|
|
216
|
+
if (origin === 'ai-generated' || origin === 'ai-assisted') {
|
|
217
|
+
aiCount++;
|
|
218
|
+
} else {
|
|
219
|
+
humanCount++;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (aiCount > humanCount) return 'ai-assisted';
|
|
223
|
+
if (humanCount > aiCount) return 'human';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return 'unknown';
|
|
227
|
+
} catch (err) {
|
|
228
|
+
console.error('Failed to look up AI origin:', err);
|
|
229
|
+
return 'unknown';
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DynamoDBClient,
|
|
3
|
+
QueryCommand,
|
|
4
|
+
} from '@aws-sdk/client-dynamodb';
|
|
5
|
+
import {
|
|
6
|
+
EventBridgeClient,
|
|
7
|
+
PutEventsCommand,
|
|
8
|
+
} from '@aws-sdk/client-eventbridge';
|
|
9
|
+
|
|
10
|
+
const dynamoClient = new DynamoDBClient({});
|
|
11
|
+
const eventBridgeClient = new EventBridgeClient({});
|
|
12
|
+
|
|
13
|
+
const EVENTS_TABLE = process.env.EVENTS_TABLE!;
|
|
14
|
+
const EVENT_BUS_NAME = process.env.EVENT_BUS_NAME!;
|
|
15
|
+
|
|
16
|
+
interface PrEvent {
|
|
17
|
+
source: string;
|
|
18
|
+
'detail-type': string;
|
|
19
|
+
detail: {
|
|
20
|
+
team_id: string;
|
|
21
|
+
repo: string;
|
|
22
|
+
timestamp: string;
|
|
23
|
+
ai_context?: { origin: string };
|
|
24
|
+
pr?: { merged_at?: string; number?: number };
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Triggered by merged PR events.
|
|
30
|
+
* Queries for open Security Agent findings on this repo,
|
|
31
|
+
* checks if the PR resolves any, and emits remediation events.
|
|
32
|
+
*/
|
|
33
|
+
export async function handler(event: PrEvent): Promise<void> {
|
|
34
|
+
const detail = event.detail;
|
|
35
|
+
const mergedAt = detail.pr?.merged_at ?? detail.timestamp;
|
|
36
|
+
|
|
37
|
+
console.log(`Checking for resolved security findings in ${detail.repo}`);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// Query for open security findings on this repo (unremediated)
|
|
41
|
+
const securityDetailTypes = [
|
|
42
|
+
'prism.d1.security.design_review',
|
|
43
|
+
'prism.d1.security.code_review',
|
|
44
|
+
'prism.d1.security.pen_test',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
for (const detailType of securityDetailTypes) {
|
|
48
|
+
const result = await dynamoClient.send(
|
|
49
|
+
new QueryCommand({
|
|
50
|
+
TableName: EVENTS_TABLE,
|
|
51
|
+
IndexName: 'by-detail-type',
|
|
52
|
+
KeyConditionExpression: 'detail_type = :dt',
|
|
53
|
+
ExpressionAttributeValues: {
|
|
54
|
+
':dt': { S: detailType },
|
|
55
|
+
},
|
|
56
|
+
ScanIndexForward: false,
|
|
57
|
+
Limit: 50,
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
if (!result.Items || result.Items.length === 0) continue;
|
|
62
|
+
|
|
63
|
+
for (const item of result.Items) {
|
|
64
|
+
const data = JSON.parse(item.data?.S ?? '{}');
|
|
65
|
+
const finding = data.security_agent_finding;
|
|
66
|
+
if (!finding || finding.remediated_at) continue;
|
|
67
|
+
|
|
68
|
+
// Check if finding matches this repo
|
|
69
|
+
if (data.repo !== detail.repo) continue;
|
|
70
|
+
|
|
71
|
+
// Calculate remediation time
|
|
72
|
+
const foundAt = new Date(finding.found_at);
|
|
73
|
+
const mergedTime = new Date(mergedAt);
|
|
74
|
+
const remediationHours = (mergedTime.getTime() - foundAt.getTime()) / (1000 * 60 * 60);
|
|
75
|
+
|
|
76
|
+
if (remediationHours < 0) continue; // Finding is newer than PR
|
|
77
|
+
|
|
78
|
+
const remediationEvent = {
|
|
79
|
+
team_id: detail.team_id,
|
|
80
|
+
repo: detail.repo,
|
|
81
|
+
timestamp: mergedAt,
|
|
82
|
+
prism_level: 3,
|
|
83
|
+
metric: {
|
|
84
|
+
name: 'security_remediation',
|
|
85
|
+
value: remediationHours,
|
|
86
|
+
unit: 'hours',
|
|
87
|
+
},
|
|
88
|
+
ai_context: detail.ai_context,
|
|
89
|
+
security_remediation: {
|
|
90
|
+
finding_id: finding.finding_id,
|
|
91
|
+
severity: finding.severity,
|
|
92
|
+
remediation_time_hours: Math.round(remediationHours * 100) / 100,
|
|
93
|
+
remediated_by_origin: detail.ai_context?.origin ?? 'unknown',
|
|
94
|
+
fix_pr_number: detail.pr?.number ?? null,
|
|
95
|
+
finding_phase: finding.phase,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
await eventBridgeClient.send(
|
|
100
|
+
new PutEventsCommand({
|
|
101
|
+
Entries: [
|
|
102
|
+
{
|
|
103
|
+
Source: 'prism.d1.velocity',
|
|
104
|
+
DetailType: 'prism.d1.security.remediation',
|
|
105
|
+
EventBusName: EVENT_BUS_NAME,
|
|
106
|
+
Detail: JSON.stringify(remediationEvent),
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
console.log(
|
|
113
|
+
`Remediation event: finding ${finding.finding_id} (${finding.severity}) fixed in ${remediationHours.toFixed(1)}h by ${detail.ai_context?.origin ?? 'unknown'}`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch (err) {
|
|
118
|
+
console.error('Failed to track security remediation:', err);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DynamoDBClient,
|
|
3
|
+
PutItemCommand,
|
|
4
|
+
} from '@aws-sdk/client-dynamodb';
|
|
5
|
+
import {
|
|
6
|
+
EventBridgeClient,
|
|
7
|
+
PutEventsCommand,
|
|
8
|
+
} from '@aws-sdk/client-eventbridge';
|
|
9
|
+
|
|
10
|
+
const dynamoClient = new DynamoDBClient({});
|
|
11
|
+
const eventBridgeClient = new EventBridgeClient({});
|
|
12
|
+
|
|
13
|
+
const EVENTS_TABLE = process.env.EVENTS_TABLE!;
|
|
14
|
+
const EVENT_BUS_NAME = process.env.EVENT_BUS_NAME!;
|
|
15
|
+
const GUARDRAIL_ID = process.env.GUARDRAIL_ID ?? '';
|
|
16
|
+
|
|
17
|
+
interface SecurityFindingEvent {
|
|
18
|
+
source: string;
|
|
19
|
+
'detail-type': string;
|
|
20
|
+
detail: {
|
|
21
|
+
team_id: string;
|
|
22
|
+
repo: string;
|
|
23
|
+
timestamp: string;
|
|
24
|
+
security_agent_finding?: {
|
|
25
|
+
finding_id: string;
|
|
26
|
+
phase: string;
|
|
27
|
+
severity: string;
|
|
28
|
+
category: string;
|
|
29
|
+
exploit_validated: boolean;
|
|
30
|
+
title: string;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Auto-responds to critical/high Security Agent findings.
|
|
37
|
+
* Actions:
|
|
38
|
+
* 1. Write "security penalty" to events table for eval gate consumption
|
|
39
|
+
* 2. Emit SecurityCriticalFindingCount metric (triggers alarm)
|
|
40
|
+
* 3. Log for SNS escalation (SNS topic wired via alarm action)
|
|
41
|
+
*/
|
|
42
|
+
export async function handler(event: SecurityFindingEvent): Promise<void> {
|
|
43
|
+
const detail = event.detail;
|
|
44
|
+
const finding = detail.security_agent_finding;
|
|
45
|
+
|
|
46
|
+
if (!finding) {
|
|
47
|
+
console.log('No security_agent_finding in event, skipping');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const severity = finding.severity.toUpperCase();
|
|
52
|
+
if (severity !== 'CRITICAL' && severity !== 'HIGH') {
|
|
53
|
+
console.log(`Finding severity ${severity} does not require automated response`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log(
|
|
58
|
+
`CRITICAL/HIGH security finding: ${finding.finding_id} (${severity}) - ${finding.title}`,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// 1. Write security penalty record for eval gate consumption
|
|
62
|
+
const ttl = Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60; // 30 days
|
|
63
|
+
try {
|
|
64
|
+
await dynamoClient.send(
|
|
65
|
+
new PutItemCommand({
|
|
66
|
+
TableName: EVENTS_TABLE,
|
|
67
|
+
Item: {
|
|
68
|
+
pk: { S: `${detail.team_id}#${detail.repo}` },
|
|
69
|
+
sk: { S: `penalty#${detail.timestamp}` },
|
|
70
|
+
detail_type: { S: 'prism.d1.security.penalty' },
|
|
71
|
+
finding_id: { S: finding.finding_id },
|
|
72
|
+
data: {
|
|
73
|
+
S: JSON.stringify({
|
|
74
|
+
team_id: detail.team_id,
|
|
75
|
+
repo: detail.repo,
|
|
76
|
+
finding_id: finding.finding_id,
|
|
77
|
+
severity,
|
|
78
|
+
category: finding.category,
|
|
79
|
+
exploit_validated: finding.exploit_validated,
|
|
80
|
+
title: finding.title,
|
|
81
|
+
phase: finding.phase,
|
|
82
|
+
}),
|
|
83
|
+
},
|
|
84
|
+
ttl: { N: ttl.toString() },
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
console.log(`Security penalty written for finding ${finding.finding_id}`);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
console.error('Failed to write security penalty:', err);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 2. Emit critical finding metric (triggers SecurityCriticalFinding alarm)
|
|
94
|
+
try {
|
|
95
|
+
const alertEvent = {
|
|
96
|
+
team_id: detail.team_id,
|
|
97
|
+
repo: detail.repo,
|
|
98
|
+
timestamp: detail.timestamp,
|
|
99
|
+
prism_level: 1,
|
|
100
|
+
metric: {
|
|
101
|
+
name: 'SecurityCriticalFindingCount',
|
|
102
|
+
value: 1,
|
|
103
|
+
unit: 'count',
|
|
104
|
+
},
|
|
105
|
+
security_agent_finding: finding,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
await eventBridgeClient.send(
|
|
109
|
+
new PutEventsCommand({
|
|
110
|
+
Entries: [
|
|
111
|
+
{
|
|
112
|
+
Source: 'prism.d1.velocity',
|
|
113
|
+
DetailType: event['detail-type'],
|
|
114
|
+
EventBusName: EVENT_BUS_NAME,
|
|
115
|
+
Detail: JSON.stringify(alertEvent),
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.error('Failed to emit critical finding alert:', err);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 3. Log for operational awareness
|
|
125
|
+
console.warn(
|
|
126
|
+
`ACTION REQUIRED: ${severity} security finding in ${detail.repo}: ${finding.title} (${finding.finding_id}). ` +
|
|
127
|
+
`Phase: ${finding.phase}. Exploit validated: ${finding.exploit_validated}. ` +
|
|
128
|
+
`Category: ${finding.category}. Guardrail ID: ${GUARDRAIL_ID || 'not configured'}.`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DynamoDBClient,
|
|
3
|
+
QueryCommand,
|
|
4
|
+
} from '@aws-sdk/client-dynamodb';
|
|
5
|
+
import {
|
|
6
|
+
EventBridgeClient,
|
|
7
|
+
PutEventsCommand,
|
|
8
|
+
} from '@aws-sdk/client-eventbridge';
|
|
9
|
+
|
|
10
|
+
const dynamoClient = new DynamoDBClient({});
|
|
11
|
+
const eventBridgeClient = new EventBridgeClient({});
|
|
12
|
+
|
|
13
|
+
const EVENTS_TABLE = process.env.EVENTS_TABLE!;
|
|
14
|
+
const EVENT_BUS_NAME = process.env.EVENT_BUS_NAME!;
|
|
15
|
+
|
|
16
|
+
interface PrMergedEvent {
|
|
17
|
+
source: string;
|
|
18
|
+
'detail-type': string;
|
|
19
|
+
detail: {
|
|
20
|
+
team_id: string;
|
|
21
|
+
repo: string;
|
|
22
|
+
timestamp: string;
|
|
23
|
+
ai_context?: {
|
|
24
|
+
tool: string;
|
|
25
|
+
model: string;
|
|
26
|
+
origin: string;
|
|
27
|
+
};
|
|
28
|
+
pr?: {
|
|
29
|
+
spec_ref?: string;
|
|
30
|
+
merged_at?: string;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Triggered by merged PR events that have a Spec-Ref.
|
|
37
|
+
* Calculates spec-to-code hours by finding the earliest commit
|
|
38
|
+
* referencing the same spec and computing the time delta to PR merge.
|
|
39
|
+
*/
|
|
40
|
+
export async function handler(event: PrMergedEvent): Promise<void> {
|
|
41
|
+
const detail = event.detail;
|
|
42
|
+
const specRef = detail.pr?.spec_ref;
|
|
43
|
+
|
|
44
|
+
if (!specRef) {
|
|
45
|
+
console.log('No spec reference in PR event, skipping');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log(`Calculating spec-to-code for spec: ${specRef}`);
|
|
50
|
+
|
|
51
|
+
// Query for the earliest commit event in this repo that references this spec.
|
|
52
|
+
// spec_ref is stored as a top-level DynamoDB attribute by the metrics-processor.
|
|
53
|
+
try {
|
|
54
|
+
const result = await dynamoClient.send(
|
|
55
|
+
new QueryCommand({
|
|
56
|
+
TableName: EVENTS_TABLE,
|
|
57
|
+
KeyConditionExpression: 'pk = :pk',
|
|
58
|
+
FilterExpression: 'spec_ref = :spec_ref AND detail_type = :dt',
|
|
59
|
+
ExpressionAttributeValues: {
|
|
60
|
+
':pk': { S: `${detail.team_id}#${detail.repo}` },
|
|
61
|
+
':spec_ref': { S: specRef },
|
|
62
|
+
':dt': { S: 'prism.d1.commit' },
|
|
63
|
+
},
|
|
64
|
+
ScanIndexForward: true, // Oldest first
|
|
65
|
+
Limit: 1,
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
if (!result.Items || result.Items.length === 0) {
|
|
70
|
+
console.log(`No commit events found referencing spec: ${specRef}`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const earliestSpecCommitTime = result.Items[0].sk?.S;
|
|
75
|
+
if (!earliestSpecCommitTime) {
|
|
76
|
+
console.log('Could not determine spec commit timestamp');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const mergedAt = detail.pr?.merged_at ?? detail.timestamp;
|
|
81
|
+
const specTime = new Date(earliestSpecCommitTime);
|
|
82
|
+
const mergeTime = new Date(mergedAt);
|
|
83
|
+
const specToCodeHours = (mergeTime.getTime() - specTime.getTime()) / (1000 * 60 * 60);
|
|
84
|
+
|
|
85
|
+
if (specToCodeHours < 0) {
|
|
86
|
+
console.log('Negative spec-to-code time, likely data issue');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const specEvent = {
|
|
91
|
+
team_id: detail.team_id,
|
|
92
|
+
repo: detail.repo,
|
|
93
|
+
timestamp: detail.timestamp,
|
|
94
|
+
prism_level: 3,
|
|
95
|
+
metric: {
|
|
96
|
+
name: 'spec_to_code_hours',
|
|
97
|
+
value: Math.round(specToCodeHours * 100) / 100,
|
|
98
|
+
unit: 'hours',
|
|
99
|
+
},
|
|
100
|
+
ai_context: detail.ai_context,
|
|
101
|
+
ai_dora: {
|
|
102
|
+
spec_to_code_hours: Math.round(specToCodeHours * 100) / 100,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
await eventBridgeClient.send(
|
|
107
|
+
new PutEventsCommand({
|
|
108
|
+
Entries: [
|
|
109
|
+
{
|
|
110
|
+
Source: 'prism.d1.velocity',
|
|
111
|
+
DetailType: 'prism.d1.pr',
|
|
112
|
+
EventBusName: EVENT_BUS_NAME,
|
|
113
|
+
Detail: JSON.stringify(specEvent),
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
console.log(`Spec-to-code: ${specToCodeHours.toFixed(2)} hours for spec ${specRef}`);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.error('Failed to calculate spec-to-code hours:', err);
|
|
122
|
+
}
|
|
123
|
+
}
|