@prism-d1/cli 1.0.92 → 1.0.94

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.
@@ -1,470 +0,0 @@
1
- # prism-eval-gate.yml — Evaluate AI-generated code quality on PRs.
2
- #
3
- # Copy this file to .github/workflows/prism-eval-gate.yml in your repo.
4
- #
5
- # Required setup:
6
- # - OIDC provider configured for GitHub Actions in your AWS account
7
- # - IAM role with bedrock:InvokeModel and events:PutEvents permissions
8
- # - Repository secret PRISM_METRICS_ROLE_ARN set to the OIDC role ARN
9
- # - .prism/eval-harness/ directory in your repo (run: bash prism-cli bootstrapper install-eval-harness)
10
- # - At least one rubric JSON in .prism/eval-harness/rubrics/
11
-
12
- name: PRISM Eval Gate
13
-
14
- on:
15
- pull_request:
16
- types: [opened, synchronize, reopened]
17
- branches: [main, master]
18
-
19
-
20
- env:
21
- PRISM_AWS_REGION: us-west-2
22
- PRISM_REPO: github.com/${{ github.repository }}
23
-
24
- permissions:
25
- id-token: write
26
- contents: read
27
- pull-requests: write
28
-
29
- jobs:
30
- eval-gate:
31
- runs-on: ubuntu-latest
32
- steps:
33
- - name: Checkout
34
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
35
- with:
36
- fetch-depth: 0
37
-
38
- - name: Configure AWS credentials (OIDC)
39
- uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df # v4.2.1
40
- with:
41
- role-to-assume: ${{ secrets.PRISM_METRICS_ROLE_ARN }}
42
- aws-region: ${{ env.PRISM_AWS_REGION }}
43
-
44
- - name: Read team ID
45
- id: prism-config
46
- run: |
47
- TEAM_ID=$(jq -r '.team_id // "no_team"' .prism/config.json 2>/dev/null || echo "no_team")
48
- echo "team_id=${TEAM_ID}" >> "$GITHUB_OUTPUT"
49
-
50
- - name: Install dependencies
51
- run: sudo apt-get update -qq && sudo apt-get install -y -qq jq bc
52
-
53
- - name: Identify AI-generated changes
54
- id: detect-ai
55
- run: |
56
- BASE_SHA="${{ github.event.pull_request.base.sha }}"
57
- HEAD_SHA="${{ github.sha }}"
58
-
59
- AI_SHAS=""
60
- for SHA in $(git rev-list "${BASE_SHA}..${HEAD_SHA}"); do
61
- if git log -1 --format='%B' "${SHA}" | grep -q 'AI-Origin: ai-generated\|AI-Origin: ai-assisted'; then
62
- AI_SHAS="${AI_SHAS} ${SHA}"
63
- fi
64
- done
65
- AI_SHAS=$(echo "${AI_SHAS}" | xargs)
66
-
67
- if [[ -z "${AI_SHAS}" ]]; then
68
- echo "has_ai_changes=false" >> "$GITHUB_OUTPUT"
69
- exit 0
70
- fi
71
-
72
- echo "has_ai_changes=true" >> "$GITHUB_OUTPUT"
73
- echo "ai_shas=${AI_SHAS}" >> "$GITHUB_OUTPUT"
74
-
75
- # Collect changed source files from AI commits
76
- CHANGED_FILES=""
77
- for SHA in ${AI_SHAS}; do
78
- FILES=$(git diff-tree --no-commit-id --name-only -r "${SHA}" \
79
- | grep -E '\.(ts|js|py|go|java|rs|tsx|jsx)$' | grep -v '\.test\.\|\.spec\.' || true)
80
- CHANGED_FILES="${CHANGED_FILES}\n${FILES}"
81
- done
82
-
83
- echo -e "${CHANGED_FILES}" | sort -u | grep -v '^$' > /tmp/ai-changed-files.txt || true
84
- FILE_COUNT=$(wc -l < /tmp/ai-changed-files.txt | tr -d ' ')
85
- echo "file_count=${FILE_COUNT}" >> "$GITHUB_OUTPUT"
86
-
87
- - name: Run evaluations
88
- id: eval
89
- if: steps.detect-ai.outputs.has_ai_changes == 'true'
90
- run: |
91
- set -o pipefail
92
- chmod +x .prism/eval-harness/run-eval.sh
93
-
94
- # Find spec from commit trailers
95
- BASE_SHA="${{ github.event.pull_request.base.sha }}"
96
- HEAD_SHA="${{ github.sha }}"
97
- SPEC_REF=$(git log "${BASE_SHA}..${HEAD_SHA}" --format='%(trailers:key=Spec,valueonly)' | head -1 | tr -d '[:space:]' || true)
98
- SPEC_FLAG=""
99
- [[ -n "${SPEC_REF}" && -f "${SPEC_REF}" ]] && SPEC_FLAG="--spec ${SPEC_REF}"
100
-
101
- PASS_COUNT=0
102
- FAIL_COUNT=0
103
- TOTAL_SCORE=0
104
- TOTAL_HALLUCINATIONS=0
105
- RESULTS=""
106
-
107
- while IFS= read -r FILE; do
108
- [[ -z "${FILE}" || ! -f "${FILE}" ]] && continue
109
-
110
- # Auto-select rubric based on file path
111
- if echo "${FILE}" | grep -qiE '(agent|assistant|orchestrat|workflow|chain)'; then
112
- RUBRIC=".prism/eval-harness/rubrics/agent-quality.json"
113
- elif echo "${FILE}" | grep -qiE '(auth|security|guard|policy|iam|permission|crypto)'; then
114
- RUBRIC=".prism/eval-harness/rubrics/security-compliance.json"
115
- elif echo "${FILE}" | grep -qiE '(api|handler|route|controller)'; then
116
- RUBRIC=".prism/eval-harness/rubrics/api-response-quality.json"
117
- else
118
- RUBRIC=".prism/eval-harness/rubrics/code-quality.json"
119
- fi
120
-
121
- # Fall back to first available rubric if selected doesn't exist
122
- [[ -f "${RUBRIC}" ]] || RUBRIC=$(ls .prism/eval-harness/rubrics/*.json 2>/dev/null | head -1)
123
- [[ -f "${RUBRIC}" ]] || { echo "No rubrics found"; exit 1; }
124
-
125
- echo "Evaluating: ${FILE} (rubric: $(basename "${RUBRIC}" .json))"
126
-
127
- set +e
128
- OUTPUT=$(./.prism/eval-harness/run-eval.sh "${RUBRIC}" "${FILE}" ${SPEC_FLAG} 2>&1)
129
- EXIT_CODE=$?
130
- set -e
131
-
132
- SCORE=$(echo "${OUTPUT}" | grep '^Score:' | awk '{print $2}' || echo "0")
133
- RESULT=$(echo "${OUTPUT}" | grep '^Result:' | awk '{print $2}' || echo "UNKNOWN")
134
- HALLUC=$(echo "${OUTPUT}" | grep '^Hallucinations:' | awk '{print $2}' || echo "0")
135
-
136
- TOTAL_SCORE=$(echo "${TOTAL_SCORE} + ${SCORE}" | bc -l)
137
- TOTAL_HALLUCINATIONS=$((TOTAL_HALLUCINATIONS + HALLUC))
138
-
139
- if [[ "${RESULT}" == "SKIP" ]]; then
140
- continue
141
- elif [[ "${RESULT}" == "PASS" ]]; then
142
- PASS_COUNT=$((PASS_COUNT + 1))
143
- else
144
- FAIL_COUNT=$((FAIL_COUNT + 1))
145
- fi
146
-
147
- RESULTS="${RESULTS}\n| \`${FILE}\` | $(basename "${RUBRIC}" .json) | ${SCORE} | ${RESULT} |"
148
- done < /tmp/ai-changed-files.txt
149
-
150
- TOTAL_FILES=$((PASS_COUNT + FAIL_COUNT))
151
- AVG_SCORE="0"
152
- [[ "${TOTAL_FILES}" -gt 0 ]] && AVG_SCORE=$(echo "scale=4; ${TOTAL_SCORE} / ${TOTAL_FILES}" | bc -l)
153
-
154
- echo "pass_count=${PASS_COUNT}" >> "$GITHUB_OUTPUT"
155
- echo "fail_count=${FAIL_COUNT}" >> "$GITHUB_OUTPUT"
156
- echo "avg_score=${AVG_SCORE}" >> "$GITHUB_OUTPUT"
157
- echo "total_files=${TOTAL_FILES}" >> "$GITHUB_OUTPUT"
158
- echo "hallucinations=${TOTAL_HALLUCINATIONS}" >> "$GITHUB_OUTPUT"
159
- echo -e "${RESULTS}" > /tmp/eval-results-table.txt
160
-
161
- [[ "${FAIL_COUNT}" -gt 0 ]] && echo "overall_result=FAIL" >> "$GITHUB_OUTPUT" || echo "overall_result=PASS" >> "$GITHUB_OUTPUT"
162
-
163
- - name: Post PR comment
164
- if: always()
165
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
166
- with:
167
- script: |
168
- const fs = require('fs');
169
- const hasAI = '${{ steps.detect-ai.outputs.has_ai_changes }}' === 'true';
170
-
171
- let body;
172
- if (!hasAI) {
173
- body = ':white_check_mark: **PRISM Eval Gate** — No AI-generated code detected. Skipped.';
174
- } else {
175
- const avg = '${{ steps.eval.outputs.avg_score }}';
176
- const pass = '${{ steps.eval.outputs.pass_count }}';
177
- const fail = '${{ steps.eval.outputs.fail_count }}';
178
- const total = '${{ steps.eval.outputs.total_files }}';
179
- const result = '${{ steps.eval.outputs.overall_result }}';
180
- const halluc = '${{ steps.eval.outputs.hallucinations }}';
181
- const emoji = result === 'PASS' ? ':white_check_mark:' : ':x:';
182
- const table = fs.existsSync('/tmp/eval-results-table.txt')
183
- ? fs.readFileSync('/tmp/eval-results-table.txt', 'utf8').trim() : '';
184
-
185
- body = `## ${emoji} PRISM Eval Gate — ${result}\n\n` +
186
- `**Score**: ${avg} | **Files**: ${total} | **Pass**: ${pass} | **Fail**: ${fail} | **Hallucinations**: ${halluc}\n\n` +
187
- `| File | Rubric | Score | Result |\n|---|---|---|---|\n${table}\n\n` +
188
- `> Threshold: 0.82 | Model: claude-sonnet-4-20250514`;
189
- }
190
-
191
- const { data: comments } = await github.rest.issues.listComments({
192
- owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number
193
- });
194
- const existing = comments.find(c => c.body.includes('PRISM Eval Gate'));
195
- const params = { owner: context.repo.owner, repo: context.repo.repo, body };
196
-
197
- if (existing) {
198
- await github.rest.issues.updateComment({ ...params, comment_id: existing.id });
199
- } else {
200
- await github.rest.issues.createComment({ ...params, issue_number: context.issue.number });
201
- }
202
-
203
- - name: Emit prism.d1.eval event
204
- if: steps.detect-ai.outputs.has_ai_changes == 'true'
205
- run: |
206
- TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
207
- EVAL_EVENT=$(jq -n \
208
- --arg team_id "${{ steps.prism-config.outputs.team_id }}" \
209
- --arg repo "${PRISM_REPO}" \
210
- --arg timestamp "${TIMESTAMP}" \
211
- --argjson score "${{ steps.eval.outputs.avg_score }}" \
212
- --arg result "${{ steps.eval.outputs.overall_result }}" \
213
- --argjson total_files "${{ steps.eval.outputs.total_files }}" \
214
- --argjson pass_count "${{ steps.eval.outputs.pass_count }}" \
215
- --argjson fail_count "${{ steps.eval.outputs.fail_count }}" \
216
- --argjson hallucinations "${{ steps.eval.outputs.hallucinations }}" \
217
- --arg pr_number "${{ github.event.pull_request.number }}" \
218
- '{
219
- team_id: $team_id, repo: $repo, timestamp: $timestamp, prism_level: 2,
220
- metric: {name: "eval_score", value: $score, unit: "score"},
221
- ai_context: {tool: "bedrock-eval", model: "anthropic.claude-sonnet-4-20250514", origin: "ai-generated"},
222
- eval: {total_files: $total_files, pass_count: $pass_count, fail_count: $fail_count,
223
- result: $result, pr_number: ($pr_number | tonumber), hallucinations_caught: $hallucinations}
224
- }')
225
- aws events put-events --region $PRISM_AWS_REGION --entries "[{
226
- \"Source\":\"prism.d1.velocity\",\"DetailType\":\"prism.d1.eval\",
227
- \"EventBusName\":\"prism-d1-metrics\",
228
- \"Detail\":$(echo "${EVAL_EVENT}" | jq -c '.' | jq -Rs '.')}]"
229
-
230
- - name: Upgrade AWS CLI v2 (securityagent code review requires v2.36+)
231
- run: |
232
- curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o /tmp/awscliv2.zip
233
- curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip.sha256" -o /tmp/awscliv2.zip.sha256
234
- cd /tmp && sha256sum -c awscliv2.zip.sha256
235
- unzip -qo /tmp/awscliv2.zip -d /tmp
236
- sudo /tmp/aws/install --update --install-dir /usr/local/aws-cli --bin-dir /usr/local/bin 2>/dev/null || \
237
- sudo /tmp/aws/install --install-dir /usr/local/aws-cli --bin-dir /usr/local/bin 2>/dev/null || true
238
- rm -rf /tmp/aws /tmp/awscliv2.zip /tmp/awscliv2.zip.sha256
239
- hash -r
240
- echo "AWS CLI: $(aws --version)"
241
- echo "Has create-code-review: $(aws securityagent create-code-review help 2>&1 | head -1)"
242
-
243
- - name: AWS Continuum diff scan
244
- id: security-gate
245
- env:
246
- AWS_REGION: ${{ env.PRISM_AWS_REGION }}
247
- run: |
248
- echo "::group::Retrieve Continuum config from SSM"
249
- AGENT_SPACE_ID=$(aws ssm get-parameter --name /prism/continuum/agent-space-id --query Parameter.Value --output text 2>/dev/null || echo "")
250
- SCAN_BUCKET=$(aws ssm get-parameter --name /prism/continuum/scan-bucket --query Parameter.Value --output text 2>/dev/null || echo "")
251
- SERVICE_ROLE=$(aws ssm get-parameter --name /prism/continuum/service-role-arn --query Parameter.Value --output text 2>/dev/null || echo "")
252
- echo "::endgroup::"
253
-
254
- # Skip if core infrastructure not deployed
255
- if [[ -z "$AGENT_SPACE_ID" || -z "$SCAN_BUCKET" ]]; then
256
- echo "⚠️ AWS Continuum not configured (agent-space-id or scan-bucket missing). Skipping security scan."
257
- echo "findings=0" >> "$GITHUB_OUTPUT"
258
- echo "skipped=true" >> "$GITHUB_OUTPUT"
259
- exit 0
260
- fi
261
-
262
- # Continuum IS configured from here on, so every failure below fails
263
- # closed. The CLI probe runs after the config lookup on purpose: before
264
- # it, "never deployed" and "deployed but cannot scan" are
265
- # indistinguishable, and the old order silently passed the gate on a repo
266
- # that had opted into scanning.
267
- if ! aws securityagent start-code-review-job help > /dev/null 2>&1; then
268
- echo "::error::Continuum is configured but this AWS CLI lacks the securityagent code review commands (need v2.36+)."
269
- echo " The security scan cannot run, so this PR is not verified."
270
- echo "scan_error=true" >> "$GITHUB_OUTPUT"
271
- echo "findings=-1" >> "$GITHUB_OUTPUT"
272
- exit 0
273
- fi
274
-
275
- # Fallback for service role if SSM param not yet deployed
276
- if [[ -z "$SERVICE_ROLE" ]]; then
277
- ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
278
- SERVICE_ROLE="arn:aws:iam::${ACCOUNT_ID}:role/prism-d1-security-agent-prism-d1-security"
279
- echo "::notice::service-role-arn SSM param not found, using convention: ${SERVICE_ROLE}"
280
- fi
281
-
282
- # Derive repo slug for per-repo SSM key (org/repo → org-repo)
283
- REPO_SLUG=$(echo "${{ github.repository }}" | tr '/' '-')
284
-
285
- echo "::group::Resolve Code Review for ${REPO_SLUG}"
286
- CODE_REVIEW_ID=$(aws ssm get-parameter --name "/prism/continuum/code-review-id/${REPO_SLUG}" --query Parameter.Value --output text 2>/dev/null || echo "")
287
-
288
- if [[ -z "$CODE_REVIEW_ID" ]]; then
289
- echo " No Code Review found for ${REPO_SLUG} -- bootstrapping..."
290
- echo " CLI version: $(aws --version 2>&1 | head -1)"
291
- echo " CLI path: $(which aws)"
292
-
293
- # Upload repo ZIP first (required as source context for the CodeReview)
294
- git archive HEAD --format=zip -o /tmp/repo.zip
295
- aws s3 cp /tmp/repo.zip "s3://${SCAN_BUCKET}/repo/${REPO_SLUG}/latest.zip" --quiet
296
-
297
- # Create the CodeReview
298
- CR_OUTPUT=$(aws securityagent create-code-review \
299
- --agent-space-id "$AGENT_SPACE_ID" \
300
- --title "${REPO_SLUG}-ci-diff-scan" \
301
- --service-role "$SERVICE_ROLE" \
302
- --assets "{\"sourceCode\":[{\"s3Location\":\"s3://${SCAN_BUCKET}/repo/${REPO_SLUG}/latest.zip\"}]}" \
303
- --output json 2>&1) || true
304
- CODE_REVIEW_ID=$(echo "$CR_OUTPUT" | jq -r '.codeReviewId // empty' 2>/dev/null)
305
-
306
- if [[ -z "$CODE_REVIEW_ID" ]]; then
307
- echo " ⚠️ Bootstrap failed:"
308
- echo " $CR_OUTPUT"
309
- echo " Run 'prism-cli securityagent setup' to create it manually."
310
- echo "scan_error=true" >> "$GITHUB_OUTPUT"
311
- echo "findings=-1" >> "$GITHUB_OUTPUT"
312
- exit 0
313
- fi
314
-
315
- # Persist for future runs
316
- aws ssm put-parameter \
317
- --name "/prism/continuum/code-review-id/${REPO_SLUG}" \
318
- --value "$CODE_REVIEW_ID" \
319
- --type String --overwrite > /dev/null
320
-
321
- echo " [x] Code Review created: ${CODE_REVIEW_ID}"
322
- echo " [x] Saved to SSM"
323
- fi
324
-
325
- echo " Using Code Review: ${CODE_REVIEW_ID}"
326
-
327
- # Upload fresh repo ZIP (context for accurate cross-module analysis)
328
- git archive HEAD --format=zip -o /tmp/repo.zip
329
- aws s3 cp /tmp/repo.zip "s3://${SCAN_BUCKET}/repo/${REPO_SLUG}/latest.zip" --quiet
330
- echo "::endgroup::"
331
-
332
- echo "::group::Generate and upload diff"
333
- BASE_SHA="${{ github.event.pull_request.base.sha }}"
334
- HEAD_SHA="${{ github.sha }}"
335
- git diff "${BASE_SHA}..${HEAD_SHA}" > /tmp/changes.patch
336
-
337
- DIFF_SIZE=$(wc -c < /tmp/changes.patch)
338
- if [[ "$DIFF_SIZE" -eq 0 ]]; then
339
- echo "No changes to scan."
340
- echo "findings=0" >> "$GITHUB_OUTPUT"
341
- exit 0
342
- fi
343
-
344
- DIFF_KEY="diffs/${REPO_SLUG}/${{ github.event.pull_request.number }}/${HEAD_SHA}.patch"
345
- aws s3 cp /tmp/changes.patch "s3://${SCAN_BUCKET}/${DIFF_KEY}" --quiet
346
- echo "::endgroup::"
347
-
348
- echo "::group::Start Continuum diff scan"
349
- JOB_RESPONSE=$(aws securityagent start-code-review-job \
350
- --agent-space-id "${AGENT_SPACE_ID}" \
351
- --code-review-id "${CODE_REVIEW_ID}" \
352
- --diff-source "{\"s3Uri\":\"s3://${SCAN_BUCKET}/${DIFF_KEY}\"}" \
353
- --output json)
354
-
355
- JOB_ID=$(echo "$JOB_RESPONSE" | jq -r '.codeReviewJobId')
356
- echo "Job started: ${JOB_ID}"
357
- echo "::endgroup::"
358
-
359
- echo "::group::Poll for completion (timeout: 30 min)"
360
- for i in $(seq 1 60); do
361
- sleep 30
362
- STATUS=$(aws securityagent batch-get-code-review-jobs \
363
- --agent-space-id "${AGENT_SPACE_ID}" \
364
- --code-review-job-ids "${JOB_ID}" \
365
- --query 'codeReviewJobs[0].status' --output text 2>/dev/null || echo "UNKNOWN")
366
-
367
- echo " Poll ${i}/60: ${STATUS}"
368
- if [[ "$STATUS" == "COMPLETED" || "$STATUS" == "FAILED" || "$STATUS" == "STOPPED" ]]; then
369
- break
370
- fi
371
- done
372
- echo "::endgroup::"
373
-
374
- if [[ "$STATUS" == "FAILED" ]]; then
375
- echo "::warning::Continuum scan failed — check Security Agent logs"
376
- echo "scan_error=true" >> "$GITHUB_OUTPUT"
377
- echo "findings=-1" >> "$GITHUB_OUTPUT"
378
- exit 0
379
- fi
380
-
381
- if [[ "$STATUS" != "COMPLETED" ]]; then
382
- echo "::warning::Continuum scan timed out (status: ${STATUS})"
383
- echo "scan_error=true" >> "$GITHUB_OUTPUT"
384
- echo "findings=-1" >> "$GITHUB_OUTPUT"
385
- exit 0
386
- fi
387
-
388
- echo "::group::Retrieve findings"
389
- # No empty-array fallback: an API error there is indistinguishable from a
390
- # clean scan, so it would pass the gate.
391
- if ! FINDINGS_JSON=$(aws securityagent list-findings \
392
- --agent-space-id "${AGENT_SPACE_ID}" \
393
- --code-review-job-id "${JOB_ID}" \
394
- --output json 2>&1); then
395
- echo "::error::Continuum scan completed but its findings could not be retrieved: ${FINDINGS_JSON}"
396
- echo "scan_error=true" >> "$GITHUB_OUTPUT"
397
- echo "findings=-1" >> "$GITHUB_OUTPUT"
398
- echo "::endgroup::"
399
- exit 0
400
- fi
401
-
402
- TOTAL=$(echo "$FINDINGS_JSON" | jq '.findingsSummaries | length')
403
- CRITICAL=$(echo "$FINDINGS_JSON" | jq '[.findingsSummaries[] | select(.riskLevel == "CRITICAL")] | length')
404
- HIGH=$(echo "$FINDINGS_JSON" | jq '[.findingsSummaries[] | select(.riskLevel == "HIGH")] | length')
405
- MEDIUM=$(echo "$FINDINGS_JSON" | jq '[.findingsSummaries[] | select(.riskLevel == "MEDIUM")] | length')
406
-
407
- echo "Findings: ${TOTAL} total (${CRITICAL} critical, ${HIGH} high, ${MEDIUM} medium)"
408
- echo "findings=${TOTAL}" >> "$GITHUB_OUTPUT"
409
- echo "critical=${CRITICAL}" >> "$GITHUB_OUTPUT"
410
- echo "high=${HIGH}" >> "$GITHUB_OUTPUT"
411
- echo "findings_json=$(echo "$FINDINGS_JSON" | jq -c '.findingsSummaries')" >> "$GITHUB_OUTPUT"
412
- echo "::endgroup::"
413
-
414
- # Print findings summary for PR annotations
415
- if [[ "$TOTAL" -gt 0 ]]; then
416
- echo "$FINDINGS_JSON" | jq -r '.findingsSummaries[] | "::error ::[\(.riskLevel)] \(.name | .[0:120])"'
417
- fi
418
-
419
- - name: Emit Continuum findings to EventBridge
420
- if: steps.security-gate.outputs.findings != '0' && steps.security-gate.outputs.skipped != 'true'
421
- env:
422
- AWS_REGION: ${{ env.PRISM_AWS_REGION }}
423
- run: |
424
- PR_NUMBER="${{ github.event.pull_request.number }}"
425
- REPO="${PRISM_REPO}"
426
- TEAM_ID="${{ steps.prism-config.outputs.team_id }}"
427
- FINDINGS='${{ steps.security-gate.outputs.findings_json }}'
428
-
429
- AI_ORIGIN="human"
430
- BASE_SHA="${{ github.event.pull_request.base.sha }}"
431
- HEAD_SHA="${{ github.sha }}"
432
- for SHA in $(git rev-list "${BASE_SHA}..${HEAD_SHA}"); do
433
- if git log -1 --format='%B' "${SHA}" | grep -q 'AI-Origin: ai-generated\|AI-Origin: ai-assisted'; then
434
- AI_ORIGIN="ai-assisted"; break
435
- fi
436
- done
437
-
438
- ENTRIES=$(echo "${FINDINGS}" | jq --arg team_id "${TEAM_ID}" --arg repo "${REPO}" \
439
- --arg ai_origin "${AI_ORIGIN}" --argjson pr_number "${PR_NUMBER}" '
440
- to_entries | map(.value as $f |
441
- {Source:"prism.d1.velocity", DetailType:"prism.d1.security.code_review",
442
- EventBusName:"prism-d1-metrics",
443
- Detail:({team_id:$team_id, repo:$repo, timestamp:($f.createdAt // now | tostring), prism_level:3,
444
- metric:{name:"security_finding",value:1,unit:"count"},
445
- ai_context:{tool:"continuum",model:"aws-continuum",origin:$ai_origin},
446
- security_agent_finding:{finding_id:($f.findingId // "cf-pr\($pr_number)-\(.key)"), phase:"code_review",
447
- severity:($f.riskLevel // "MEDIUM"),
448
- cwe_id:($f.cweId // null),
449
- ai_origin:$ai_origin, found_at:($f.createdAt // null)}
450
- } | tostring)})' 2>/dev/null || echo "[]")
451
-
452
- TOTAL=$(echo "${ENTRIES}" | jq 'length')
453
- if [[ "$TOTAL" -gt 0 ]]; then
454
- for i in $(seq 0 10 $((TOTAL - 1))); do
455
- aws events put-events --entries "$(echo "${ENTRIES}" | jq ".[$i:$((i+10))]")"
456
- done
457
- echo "Emitted ${TOTAL} finding events to EventBridge"
458
- fi
459
-
460
- - name: Fail on eval or security issues
461
- if: steps.eval.outputs.overall_result == 'FAIL' || steps.security-gate.outputs.scan_error == 'true' || (steps.security-gate.outputs.findings != '0' && steps.security-gate.outputs.skipped != 'true')
462
- run: |
463
- [[ "${{ steps.eval.outputs.overall_result }}" == "FAIL" ]] && echo "❌ Eval gate failed"
464
- if [[ "${{ steps.security-gate.outputs.scan_error }}" == "true" ]]; then
465
- echo "⚠️ AWS Continuum scan encountered an error (did not complete successfully)"
466
- echo " The security gate cannot verify this PR is safe. Re-run the workflow or check Security Agent logs."
467
- elif [[ "${{ steps.security-gate.outputs.findings }}" != "0" && "${{ steps.security-gate.outputs.skipped }}" != "true" ]]; then
468
- echo "❌ AWS Continuum found ${{ steps.security-gate.outputs.findings }} security issues (${{ steps.security-gate.outputs.critical }} critical, ${{ steps.security-gate.outputs.high }} high)"
469
- fi
470
- exit 1