@prism-d1/cli 1.0.82 → 1.0.84

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/assets/github-workflows/prism-agent-eval.yml +6 -2
  2. package/dist/assets/github-workflows/prism-ai-metrics.yml +21 -5
  3. package/dist/assets/github-workflows/prism-eval-gate-kiro.yml +55 -25
  4. package/dist/assets/github-workflows/prism-eval-gate.yml +36 -20
  5. package/dist/assets/gitlab-workflows/prism-agent-eval.yml +3 -2
  6. package/dist/assets/gitlab-workflows/prism-ai-metrics.yml +11 -3
  7. package/dist/assets/gitlab-workflows/prism-eval-gate.yml +3 -2
  8. package/dist/assets/infra/lib/lambda/security-agent-processor.ts +76 -43
  9. package/dist/assets/infra/lib/metrics-pipeline-stack.ts +8 -0
  10. package/dist/src/commands/bootstrapper/install-eval-harness.d.ts.map +1 -1
  11. package/dist/src/commands/bootstrapper/install-eval-harness.js +25 -6
  12. package/dist/src/commands/bootstrapper/install-eval-harness.js.map +1 -1
  13. package/dist/src/commands/bootstrapper/install-github-workflows.d.ts +1 -0
  14. package/dist/src/commands/bootstrapper/install-github-workflows.d.ts.map +1 -1
  15. package/dist/src/commands/bootstrapper/install-github-workflows.js +81 -14
  16. package/dist/src/commands/bootstrapper/install-github-workflows.js.map +1 -1
  17. package/dist/src/commands/bootstrapper/install-gitlab-workflows.d.ts.map +1 -1
  18. package/dist/src/commands/bootstrapper/install-gitlab-workflows.js +9 -6
  19. package/dist/src/commands/bootstrapper/install-gitlab-workflows.js.map +1 -1
  20. package/dist/src/commands/bootstrapper/setup-github-oidc.d.ts +8 -2
  21. package/dist/src/commands/bootstrapper/setup-github-oidc.d.ts.map +1 -1
  22. package/dist/src/commands/bootstrapper/setup-github-oidc.js +17 -2
  23. package/dist/src/commands/bootstrapper/setup-github-oidc.js.map +1 -1
  24. package/dist/src/commands/bootstrapper/setup-gitlab-oidc.d.ts +8 -2
  25. package/dist/src/commands/bootstrapper/setup-gitlab-oidc.d.ts.map +1 -1
  26. package/dist/src/commands/bootstrapper/setup-gitlab-oidc.js +17 -2
  27. package/dist/src/commands/bootstrapper/setup-gitlab-oidc.js.map +1 -1
  28. package/dist/src/commands/workshop/verify-setup.d.ts.map +1 -1
  29. package/dist/src/commands/workshop/verify-setup.js +0 -2
  30. package/dist/src/commands/workshop/verify-setup.js.map +1 -1
  31. package/dist/src/utils/root.d.ts +8 -4
  32. package/dist/src/utils/root.d.ts.map +1 -1
  33. package/dist/src/utils/root.js +42 -20
  34. package/dist/src/utils/root.js.map +1 -1
  35. package/dist/src/utils/validate.d.ts +1 -0
  36. package/dist/src/utils/validate.d.ts.map +1 -1
  37. package/dist/src/utils/validate.js +42 -0
  38. package/dist/src/utils/validate.js.map +1 -1
  39. package/package.json +1 -1
@@ -23,6 +23,10 @@ on:
23
23
  - '**/agent*.py'
24
24
  - '**/agent*.ts'
25
25
 
26
+
27
+ env:
28
+ PRISM_AWS_REGION: us-west-2
29
+
26
30
  permissions:
27
31
  id-token: write
28
32
  contents: read
@@ -41,7 +45,7 @@ jobs:
41
45
  uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df # v4.2.1
42
46
  with:
43
47
  role-to-assume: ${{ secrets.PRISM_METRICS_ROLE_ARN }}
44
- aws-region: us-west-2
48
+ aws-region: ${{ env.PRISM_AWS_REGION }}
45
49
 
46
50
  - name: Read team ID from .prism/config.json
47
51
  id: prism-config
@@ -298,7 +302,7 @@ jobs:
298
302
  }')
299
303
 
300
304
  aws events put-events \
301
- --region us-west-2 \
305
+ --region $PRISM_AWS_REGION \
302
306
  --entries "[{
303
307
  \"Source\": \"prism.d1.velocity\",
304
308
  \"DetailType\": \"prism.d1.agent.eval\",
@@ -33,6 +33,10 @@ on:
33
33
  types: [closed]
34
34
  branches: [main, master]
35
35
 
36
+
37
+ env:
38
+ PRISM_AWS_REGION: us-west-2
39
+
36
40
  permissions:
37
41
  id-token: write
38
42
  contents: read
@@ -54,7 +58,7 @@ jobs:
54
58
  uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df # v4.2.1
55
59
  with:
56
60
  role-to-assume: ${{ secrets.PRISM_METRICS_ROLE_ARN }}
57
- aws-region: us-west-2
61
+ aws-region: ${{ env.PRISM_AWS_REGION }}
58
62
 
59
63
  - name: Read team ID from .prism/config.json
60
64
  id: prism-config
@@ -86,8 +90,20 @@ jobs:
86
90
 
87
91
  # --- Commit SHAs: immutable facts for deferred origin resolution.
88
92
  # Capped at 50 to bound the event size and the render-time join.
89
- COMMIT_SHAS=$(git rev-list "${BASE_SHA}..${HEAD_SHA}" 2>/dev/null | head -50 | jq -R . | jq -sc . || echo '[]')
90
- TOTAL_COMMITS=$(git rev-list --count "${BASE_SHA}..${HEAD_SHA}" 2>/dev/null || echo 0)
93
+ #
94
+ # --no-merges matters here. HEAD_SHA is the PR's merge_commit_sha, so the
95
+ # range BASE..HEAD includes GitHub's own merge commit, authored by
96
+ # web-flow on GitHub's servers. codeburn attributes commits made on a
97
+ # developer's machine, so that commit can never appear in the attribution
98
+ # store. Counting it puts a structural ceiling on Attribution Coverage:
99
+ # a fully-onboarded team with 3 PRs and 9 authored commits reported
100
+ # 9 attributed / 12 seen = 75%, when the true coverage was 100%.
101
+ #
102
+ # Excluding every merge commit, not just GitHub's, is the right call for
103
+ # this pair: a merge commit introduces no authored lines, so it is not
104
+ # attributable content on either side of the ratio.
105
+ COMMIT_SHAS=$(git rev-list --no-merges "${BASE_SHA}..${HEAD_SHA}" 2>/dev/null | head -50 | jq -R . | jq -sc . || echo '[]')
106
+ TOTAL_COMMITS=$(git rev-list --no-merges --count "${BASE_SHA}..${HEAD_SHA}" 2>/dev/null || echo 0)
91
107
 
92
108
  # --- Lead time: PR open → merge. From the event payload, not git, so
93
109
  # it is unaffected by rebases and squashes.
@@ -173,7 +189,7 @@ jobs:
173
189
  }')
174
190
 
175
191
  aws events put-events \
176
- --region us-west-2 \
192
+ --region $PRISM_AWS_REGION \
177
193
  --entries "[{
178
194
  \"Source\": \"prism.d1.velocity\",
179
195
  \"DetailType\": \"prism.d1.pr\",
@@ -213,7 +229,7 @@ jobs:
213
229
  }')
214
230
 
215
231
  aws events put-events \
216
- --region us-west-2 \
232
+ --region $PRISM_AWS_REGION \
217
233
  --entries "[{
218
234
  \"Source\": \"prism.d1.velocity\",
219
235
  \"DetailType\": \"prism.d1.deploy\",
@@ -15,6 +15,10 @@ on:
15
15
  types: [opened, synchronize, reopened]
16
16
  branches: [main, master]
17
17
 
18
+
19
+ env:
20
+ PRISM_AWS_REGION: us-west-2
21
+
18
22
  permissions:
19
23
  id-token: write
20
24
  contents: read
@@ -213,13 +217,13 @@ jobs:
213
217
  uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df # v4.2.1
214
218
  with:
215
219
  role-to-assume: ${{ env.METRICS_ROLE_ARN }}
216
- aws-region: us-west-2
220
+ aws-region: ${{ env.PRISM_AWS_REGION }}
217
221
  continue-on-error: true
218
222
 
219
223
  - name: Emit prism.d1.eval event
220
224
  if: steps.aws-creds.outcome == 'success'
221
225
  env:
222
- AWS_REGION: us-west-2
226
+ AWS_REGION: ${{ env.PRISM_AWS_REGION }}
223
227
  TEAM_ID: ${{ steps.prism-config.outputs.team_id }}
224
228
  REPO: ${{ github.repository }}
225
229
  REVIEW_SCORE: ${{ steps.review.outputs.score }}
@@ -249,27 +253,25 @@ jobs:
249
253
  eval:{score:($score|tonumber),result:$result,rubric:"kiro-headless",
250
254
  findings:($findings|tonumber),high_findings:($high|tonumber),
251
255
  pr_number:($pr_number|tonumber),method:"kiro-headless"}}')
252
- aws events put-events --region us-west-2 --entries \
256
+ aws events put-events --region $PRISM_AWS_REGION --entries \
253
257
  "[{\"Source\":\"prism.d1.velocity\",\"DetailType\":\"prism.d1.eval\",\"EventBusName\":\"prism-d1-metrics\",\"Detail\":$(echo "${DETAIL}" | jq -Rs '.')}]" || true
254
258
 
255
259
  - name: AWS Continuum security scan (Tier 2 -- deterministic)
256
260
  id: security-gate
257
261
  if: steps.aws-creds.outcome == 'success'
258
262
  env:
259
- AWS_REGION: us-west-2
263
+ AWS_REGION: ${{ env.PRISM_AWS_REGION }}
260
264
  BASE_SHA: ${{ github.event.pull_request.base.sha }}
261
265
  HEAD_SHA: ${{ github.sha }}
262
266
  REPO: ${{ github.repository }}
263
267
  PR_NUMBER: ${{ github.event.pull_request.number }}
264
268
  run: |
265
- # Pre-flight: verify the specific subcommand exists (AWS CLI v2.36+)
266
- if ! aws securityagent start-code-review-job help > /dev/null 2>&1; then
267
- echo "::warning::AWS CLI lacks securityagent code review commands (need v2.36+). Skipping scan."
268
- echo "findings=0" >> "$GITHUB_OUTPUT"
269
- echo "skipped=true" >> "$GITHUB_OUTPUT"
270
- exit 0
271
- fi
272
-
269
+ # Config is read FIRST, on purpose. "Continuum was never deployed" is a
270
+ # deployment choice and may legitimately skip the scan; "Continuum is
271
+ # deployed but the scan could not run" is a failure and must block the
272
+ # merge. Probing the CLI before knowing which case we are in cannot tell
273
+ # them apart, so a missing subcommand used to silently pass the gate on a
274
+ # repo that had opted into scanning.
273
275
  AGENT_SPACE_ID=$(aws ssm get-parameter --name /prism/continuum/agent-space-id --query Parameter.Value --output text 2>/dev/null || echo "")
274
276
  SCAN_BUCKET=$(aws ssm get-parameter --name /prism/continuum/scan-bucket --query Parameter.Value --output text 2>/dev/null || echo "")
275
277
 
@@ -280,6 +282,16 @@ jobs:
280
282
  exit 0
281
283
  fi
282
284
 
285
+ # Continuum IS configured from here on, so every failure below fails
286
+ # closed: scan_error=true and findings=-1, never findings=0.
287
+ if ! aws securityagent start-code-review-job help > /dev/null 2>&1; then
288
+ echo "::error::Continuum is configured but this AWS CLI lacks the securityagent code review commands (need v2.36+)."
289
+ echo " The security scan cannot run, so this PR is not verified. Upgrade the CLI in the workflow."
290
+ echo "scan_error=true" >> "$GITHUB_OUTPUT"
291
+ echo "findings=-1" >> "$GITHUB_OUTPUT"
292
+ exit 0
293
+ fi
294
+
283
295
  REPO_SLUG=$(echo "${REPO}" | tr '/' '-')
284
296
  CODE_REVIEW_ID=$(aws ssm get-parameter --name "/prism/continuum/code-review-id/${REPO_SLUG}" --query Parameter.Value --output text 2>/dev/null || echo "")
285
297
  if [ -z "$CODE_REVIEW_ID" ]; then
@@ -307,10 +319,10 @@ jobs:
307
319
  CODE_REVIEW_ID=$(echo "$CR_OUTPUT" | jq -r '.codeReviewId // empty' 2>/dev/null)
308
320
 
309
321
  if [ -z "$CODE_REVIEW_ID" ]; then
310
- echo "::warning::Bootstrap failed: ${CR_OUTPUT}"
311
- echo " Run 'sudo prism-cli securityagent setup' from your DevBox to create manually."
312
- echo "findings=0" >> "$GITHUB_OUTPUT"
313
- echo "skipped=true" >> "$GITHUB_OUTPUT"
322
+ echo "::error::Could not create the Continuum Code Review resource: ${CR_OUTPUT}"
323
+ echo " Run 'prism-cli securityagent setup' to create it manually."
324
+ echo "scan_error=true" >> "$GITHUB_OUTPUT"
325
+ echo "findings=-1" >> "$GITHUB_OUTPUT"
314
326
  exit 0
315
327
  fi
316
328
 
@@ -325,7 +337,13 @@ jobs:
325
337
  # Diff must be uploaded with .patch extension (API requirement)
326
338
  git diff "${BASE_SHA}..${HEAD_SHA}" > /tmp/changes.patch
327
339
  if [ ! -s /tmp/changes.patch ]; then
340
+ echo "No changes to scan."
341
+ # critical/high must be set explicitly. The gate tests
342
+ # `critical != '0'`, and an unset output is '', which is != '0' in a
343
+ # GitHub expression -- leaving them unset failed the gate on a no-op PR.
328
344
  echo "findings=0" >> "$GITHUB_OUTPUT"
345
+ echo "critical=0" >> "$GITHUB_OUTPUT"
346
+ echo "high=0" >> "$GITHUB_OUTPUT"
329
347
  exit 0
330
348
  fi
331
349
  DIFF_KEY="diffs/${REPO_SLUG}/${PR_NUMBER}/${HEAD_SHA}.patch"
@@ -350,18 +368,26 @@ jobs:
350
368
  done
351
369
 
352
370
  if [ "$STATUS" != "COMPLETED" ]; then
353
- echo "::warning::Continuum scan did not complete (status: ${STATUS}) -- treating as skipped"
354
- echo "findings=0" >> "$GITHUB_OUTPUT"
355
- echo "skipped=true" >> "$GITHUB_OUTPUT"
371
+ echo "::error::Continuum scan did not complete (status: ${STATUS})."
372
+ echo " An incomplete scan is not a clean scan -- failing closed. Re-run, or check the Continuum job logs."
373
+ echo "scan_error=true" >> "$GITHUB_OUTPUT"
374
+ echo "findings=-1" >> "$GITHUB_OUTPUT"
356
375
  exit 0
357
376
  fi
358
377
 
359
378
  # NOTE: list-findings takes agent-space-id + code-review-job-id only.
360
379
  # Response field is 'findingsSummaries', risk field is 'riskLevel'.
361
- FINDINGS_JSON=$(aws securityagent list-findings \
380
+ # No `|| echo '{"findingsSummaries":[]}'` fallback: an API error there is
381
+ # indistinguishable from a clean scan, so it would pass the gate.
382
+ if ! FINDINGS_JSON=$(aws securityagent list-findings \
362
383
  --agent-space-id "${AGENT_SPACE_ID}" \
363
384
  --code-review-job-id "${JOB_ID}" \
364
- --output json 2>/dev/null || echo '{"findingsSummaries":[]}')
385
+ --output json 2>&1); then
386
+ echo "::error::Continuum scan completed but its findings could not be retrieved: ${FINDINGS_JSON}"
387
+ echo "scan_error=true" >> "$GITHUB_OUTPUT"
388
+ echo "findings=-1" >> "$GITHUB_OUTPUT"
389
+ exit 0
390
+ fi
365
391
 
366
392
  TOTAL=$(echo "$FINDINGS_JSON" | jq '.findingsSummaries | length')
367
393
  CRITICAL=$(echo "$FINDINGS_JSON" | jq '[.findingsSummaries[] | select(.riskLevel == "CRITICAL")] | length')
@@ -385,7 +411,7 @@ jobs:
385
411
  if: steps.security-gate.outputs.skipped != 'true' && steps.security-gate.outputs.findings != '0' && steps.security-gate.outputs.findings != ''
386
412
  continue-on-error: true
387
413
  env:
388
- AWS_REGION: us-west-2
414
+ AWS_REGION: ${{ env.PRISM_AWS_REGION }}
389
415
  TEAM_ID: ${{ steps.prism-config.outputs.team_id }}
390
416
  REPO: ${{ github.repository }}
391
417
  PR_NUMBER: ${{ github.event.pull_request.number }}
@@ -436,14 +462,14 @@ jobs:
436
462
  if [ "$TOTAL" -gt 0 ]; then
437
463
  # EventBridge accepts max 10 entries per call
438
464
  for i in $(seq 0 10 $((TOTAL - 1))); do
439
- aws events put-events --region us-west-2 \
465
+ aws events put-events --region $PRISM_AWS_REGION \
440
466
  --entries "$(echo "${ENTRIES}" | jq -c ".[$i:$((i+10))]")" || true
441
467
  done
442
468
  echo "Emitted ${TOTAL} Continuum finding events to EventBridge"
443
469
  fi
444
470
 
445
471
  - name: Fail on eval gate failure
446
- if: ${{ steps.review.outputs.result == 'FAIL' || steps.review.outputs.result == 'ERROR' || (steps.security-gate.outputs.skipped != 'true' && (steps.security-gate.outputs.critical != '0' || steps.security-gate.outputs.high != '0')) }}
472
+ if: ${{ steps.review.outputs.result == 'FAIL' || steps.review.outputs.result == 'ERROR' || steps.security-gate.outputs.scan_error == 'true' || (steps.security-gate.outputs.skipped != 'true' && (steps.security-gate.outputs.critical != '0' || steps.security-gate.outputs.high != '0')) }}
447
473
  env:
448
474
  REVIEW_RESULT: ${{ steps.review.outputs.result }}
449
475
  REVIEW_HIGH: ${{ steps.review.outputs.high }}
@@ -451,6 +477,7 @@ jobs:
451
477
  REVIEW_SUMMARY: ${{ steps.review.outputs.summary }}
452
478
  SEC_CRITICAL: ${{ steps.security-gate.outputs.critical }}
453
479
  SEC_HIGH: ${{ steps.security-gate.outputs.high }}
480
+ SEC_SCAN_ERROR: ${{ steps.security-gate.outputs.scan_error }}
454
481
  run: |
455
482
  if [ "${REVIEW_RESULT}" = "ERROR" ]; then
456
483
  echo "❌ Eval gate failed closed: review output could not be parsed"
@@ -458,7 +485,10 @@ jobs:
458
485
  echo "❌ Kiro found ${REVIEW_HIGH:-?} high-severity issues (score: ${REVIEW_SCORE:-?})"
459
486
  echo "Summary: ${REVIEW_SUMMARY}"
460
487
  fi
461
- if [ "${SEC_CRITICAL:-0}" -gt 0 ] || [ "${SEC_HIGH:-0}" -gt 0 ]; then
488
+ if [ "${SEC_SCAN_ERROR}" = "true" ]; then
489
+ echo "❌ Security gate failed closed: the Continuum scan produced no verdict."
490
+ echo " This PR has NOT been security scanned. Re-run the workflow, or check the Continuum job logs."
491
+ elif [ "${SEC_CRITICAL:-0}" -gt 0 ] || [ "${SEC_HIGH:-0}" -gt 0 ]; then
462
492
  echo "❌ Continuum found ${SEC_CRITICAL:-0} critical, ${SEC_HIGH:-0} high security findings"
463
493
  fi
464
494
  exit 1
@@ -16,6 +16,10 @@ on:
16
16
  types: [opened, synchronize, reopened]
17
17
  branches: [main, master]
18
18
 
19
+
20
+ env:
21
+ PRISM_AWS_REGION: us-west-2
22
+
19
23
  permissions:
20
24
  id-token: write
21
25
  contents: read
@@ -34,7 +38,7 @@ jobs:
34
38
  uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df # v4.2.1
35
39
  with:
36
40
  role-to-assume: ${{ secrets.PRISM_METRICS_ROLE_ARN }}
37
- aws-region: us-west-2
41
+ aws-region: ${{ env.PRISM_AWS_REGION }}
38
42
 
39
43
  - name: Read team ID
40
44
  id: prism-config
@@ -217,12 +221,12 @@ jobs:
217
221
  eval: {total_files: $total_files, pass_count: $pass_count, fail_count: $fail_count,
218
222
  result: $result, pr_number: ($pr_number | tonumber), hallucinations_caught: $hallucinations}
219
223
  }')
220
- aws events put-events --region us-west-2 --entries "[{
224
+ aws events put-events --region $PRISM_AWS_REGION --entries "[{
221
225
  \"Source\":\"prism.d1.velocity\",\"DetailType\":\"prism.d1.eval\",
222
226
  \"EventBusName\":\"prism-d1-metrics\",
223
227
  \"Detail\":$(echo "${EVAL_EVENT}" | jq -c '.' | jq -Rs '.')}]"
224
228
 
225
- - name: Upgrade AWS CLI v2 (securityagent code review requires v2.35+)
229
+ - name: Upgrade AWS CLI v2 (securityagent code review requires v2.36+)
226
230
  run: |
227
231
  curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o /tmp/awscliv2.zip
228
232
  curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip.sha256" -o /tmp/awscliv2.zip.sha256
@@ -238,16 +242,8 @@ jobs:
238
242
  - name: AWS Continuum diff scan
239
243
  id: security-gate
240
244
  env:
241
- AWS_REGION: us-west-2
245
+ AWS_REGION: ${{ env.PRISM_AWS_REGION }}
242
246
  run: |
243
- # Pre-flight: verify AWS CLI has securityagent subcommand
244
- if ! aws securityagent start-code-review-job help > /dev/null 2>&1; then
245
- echo "::warning::AWS CLI does not support 'securityagent code review' commands after upgrade. Skipping scan."
246
- echo "findings=0" >> "$GITHUB_OUTPUT"
247
- echo "skipped=true" >> "$GITHUB_OUTPUT"
248
- exit 0
249
- fi
250
-
251
247
  echo "::group::Retrieve Continuum config from SSM"
252
248
  AGENT_SPACE_ID=$(aws ssm get-parameter --name /prism/continuum/agent-space-id --query Parameter.Value --output text 2>/dev/null || echo "")
253
249
  SCAN_BUCKET=$(aws ssm get-parameter --name /prism/continuum/scan-bucket --query Parameter.Value --output text 2>/dev/null || echo "")
@@ -262,6 +258,19 @@ jobs:
262
258
  exit 0
263
259
  fi
264
260
 
261
+ # Continuum IS configured from here on, so every failure below fails
262
+ # closed. The CLI probe runs after the config lookup on purpose: before
263
+ # it, "never deployed" and "deployed but cannot scan" are
264
+ # indistinguishable, and the old order silently passed the gate on a repo
265
+ # that had opted into scanning.
266
+ if ! aws securityagent start-code-review-job help > /dev/null 2>&1; then
267
+ echo "::error::Continuum is configured but this AWS CLI lacks the securityagent code review commands (need v2.36+)."
268
+ echo " The security scan cannot run, so this PR is not verified."
269
+ echo "scan_error=true" >> "$GITHUB_OUTPUT"
270
+ echo "findings=-1" >> "$GITHUB_OUTPUT"
271
+ exit 0
272
+ fi
273
+
265
274
  # Fallback for service role if SSM param not yet deployed
266
275
  if [[ -z "$SERVICE_ROLE" ]]; then
267
276
  ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
@@ -296,10 +305,9 @@ jobs:
296
305
  if [[ -z "$CODE_REVIEW_ID" ]]; then
297
306
  echo " ⚠️ Bootstrap failed:"
298
307
  echo " $CR_OUTPUT"
299
- echo " Run 'sudo prism-cli securityagent setup' from your DevBox to create manually."
300
- echo " Skipping security scan (non-blocking)."
301
- echo "findings=0" >> "$GITHUB_OUTPUT"
302
- echo "skipped=true" >> "$GITHUB_OUTPUT"
308
+ echo " Run 'prism-cli securityagent setup' to create it manually."
309
+ echo "scan_error=true" >> "$GITHUB_OUTPUT"
310
+ echo "findings=-1" >> "$GITHUB_OUTPUT"
303
311
  exit 0
304
312
  fi
305
313
 
@@ -377,10 +385,18 @@ jobs:
377
385
  fi
378
386
 
379
387
  echo "::group::Retrieve findings"
380
- FINDINGS_JSON=$(aws securityagent list-findings \
388
+ # No empty-array fallback: an API error there is indistinguishable from a
389
+ # clean scan, so it would pass the gate.
390
+ if ! FINDINGS_JSON=$(aws securityagent list-findings \
381
391
  --agent-space-id "${AGENT_SPACE_ID}" \
382
392
  --code-review-job-id "${JOB_ID}" \
383
- --output json 2>/dev/null || echo '{"findingsSummaries":[]}')
393
+ --output json 2>&1); then
394
+ echo "::error::Continuum scan completed but its findings could not be retrieved: ${FINDINGS_JSON}"
395
+ echo "scan_error=true" >> "$GITHUB_OUTPUT"
396
+ echo "findings=-1" >> "$GITHUB_OUTPUT"
397
+ echo "::endgroup::"
398
+ exit 0
399
+ fi
384
400
 
385
401
  TOTAL=$(echo "$FINDINGS_JSON" | jq '.findingsSummaries | length')
386
402
  CRITICAL=$(echo "$FINDINGS_JSON" | jq '[.findingsSummaries[] | select(.riskLevel == "CRITICAL")] | length')
@@ -402,7 +418,7 @@ jobs:
402
418
  - name: Emit Continuum findings to EventBridge
403
419
  if: steps.security-gate.outputs.findings != '0' && steps.security-gate.outputs.skipped != 'true'
404
420
  env:
405
- AWS_REGION: us-west-2
421
+ AWS_REGION: ${{ env.PRISM_AWS_REGION }}
406
422
  run: |
407
423
  PR_NUMBER="${{ github.event.pull_request.number }}"
408
424
  REPO="${{ github.repository }}"
@@ -441,7 +457,7 @@ jobs:
441
457
  fi
442
458
 
443
459
  - name: Fail on eval or security issues
444
- if: steps.eval.outputs.overall_result == 'FAIL' || (steps.security-gate.outputs.findings != '0' && steps.security-gate.outputs.skipped != 'true')
460
+ 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')
445
461
  run: |
446
462
  [[ "${{ steps.eval.outputs.overall_result }}" == "FAIL" ]] && echo "❌ Eval gate failed"
447
463
  if [[ "${{ steps.security-gate.outputs.scan_error }}" == "true" ]]; then
@@ -6,7 +6,7 @@
6
6
  #
7
7
  # Required setup:
8
8
  # - CI/CD variable PRISM_METRICS_ROLE_ARN (IAM role with bedrock:InvokeModel + events:PutEvents)
9
- # - CI/CD variable AWS_REGION (default: us-west-2)
9
+ # - PRISM_AWS_REGION variable in the variables: block below controls the region
10
10
  # - .prism/config.json committed to the repo
11
11
  # - .prism/eval-harness/ installed (run: prism-cli bootstrapper install-eval-harness)
12
12
  # - (Optional) Agent entry point supports --mock flag for dry-run execution
@@ -26,7 +26,8 @@ prism-agent-eval:
26
26
  - "**/*agent*.py"
27
27
  - "**/*agent*.ts"
28
28
  variables:
29
- AWS_DEFAULT_REGION: us-west-2
29
+ PRISM_AWS_REGION: us-west-2
30
+ AWS_DEFAULT_REGION: $PRISM_AWS_REGION
30
31
  before_script:
31
32
  - apt-get update -qq && apt-get install -y -qq jq bc git curl unzip
32
33
  - curl -s "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o /tmp/awscliv2.zip
@@ -32,7 +32,8 @@ prism-ai-metrics:
32
32
  rules:
33
33
  - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push"'
34
34
  variables:
35
- AWS_DEFAULT_REGION: us-west-2
35
+ PRISM_AWS_REGION: us-west-2
36
+ AWS_DEFAULT_REGION: $PRISM_AWS_REGION
36
37
  before_script:
37
38
  - yum install -y -q jq git
38
39
  - git fetch --unshallow || true
@@ -74,8 +75,15 @@ prism-ai-metrics:
74
75
  HEAD_SHA="${CI_COMMIT_SHA}"
75
76
 
76
77
  # --- Commit SHAs: immutable facts for deferred origin resolution.
77
- COMMIT_SHAS=$(git rev-list "${BASE_SHA}..${HEAD_SHA}" 2>/dev/null | head -50 | jq -R . | jq -sc . || echo '[]')
78
- TOTAL_COMMITS=$(git rev-list --count "${BASE_SHA}..${HEAD_SHA}" 2>/dev/null || echo "1")
78
+ #
79
+ # --no-merges for the same reason as the GitHub workflow: under a
80
+ # merge-commit strategy CI_COMMIT_SHA is the merge commit GitLab created,
81
+ # which no developer machine authored and codeburn can therefore never
82
+ # attribute. Counting it caps Attribution Coverage below 100% for a fully
83
+ # onboarded team. Harmless under squash or fast-forward, where the range
84
+ # holds no merge commits to exclude.
85
+ COMMIT_SHAS=$(git rev-list --no-merges "${BASE_SHA}..${HEAD_SHA}" 2>/dev/null | head -50 | jq -R . | jq -sc . || echo '[]')
86
+ TOTAL_COMMITS=$(git rev-list --no-merges --count "${BASE_SHA}..${HEAD_SHA}" 2>/dev/null || echo "1")
79
87
 
80
88
  # --- Lead time: MR open → now (this job runs on the post-merge push).
81
89
  LEAD_TIME_SECONDS=0
@@ -6,7 +6,7 @@
6
6
  #
7
7
  # Required setup:
8
8
  # - CI/CD variable PRISM_METRICS_ROLE_ARN (IAM role with bedrock:InvokeModel + events:PutEvents)
9
- # - CI/CD variable AWS_REGION (default: us-west-2)
9
+ # - PRISM_AWS_REGION variable in the variables: block below controls the region
10
10
  # - .prism/eval-harness/ directory in your repo (run: prism-cli bootstrapper install-eval-harness)
11
11
  # - At least one rubric JSON in .prism/eval-harness/rubrics/
12
12
 
@@ -21,7 +21,8 @@ prism-eval-gate:
21
21
  rules:
22
22
  - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
23
23
  variables:
24
- AWS_DEFAULT_REGION: us-west-2
24
+ PRISM_AWS_REGION: us-west-2
25
+ AWS_DEFAULT_REGION: $PRISM_AWS_REGION
25
26
  before_script:
26
27
  - yum install -y -q jq bc git
27
28
  - git fetch --unshallow || true
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  DynamoDBClient,
3
- QueryCommand,
3
+ GetItemCommand,
4
4
  ScanCommand,
5
5
  } from '@aws-sdk/client-dynamodb';
6
6
  import {
@@ -14,6 +14,9 @@ const eventBridgeClient = new EventBridgeClient({});
14
14
  const EVENTS_TABLE = process.env.EVENTS_TABLE!;
15
15
  const METADATA_TABLE = process.env.METADATA_TABLE!;
16
16
  const EVENT_BUS_NAME = process.env.EVENT_BUS_NAME!;
17
+ // Optional: unset when the OTEL collector is skipped, in which case no
18
+ // attribution exists to join against and AI origin stays 'unknown'.
19
+ const AI_USAGE_TABLE = process.env.AI_USAGE_TABLE;
17
20
 
18
21
  interface SecurityAgentPayload {
19
22
  findings: Array<{
@@ -177,55 +180,85 @@ async function lookupTeamId(repo: string): Promise<string> {
177
180
  }
178
181
  }
179
182
 
183
+ /**
184
+ * Candidate `REPO#` partition keys for one repository name.
185
+ *
186
+ * The two sides of this join spell the repo differently and nothing normalizes
187
+ * either. codeburn sets the `git.repo` span attribute from the git remote, so
188
+ * the attribution store holds a host-qualified path (`github.com/owner/name`).
189
+ * CI sends `GITHUB_REPOSITORY`, which is bare (`owner/name`). A single GetItem
190
+ * on whichever form the caller happened to pass would miss roughly half the
191
+ * time — and miss silently, since a missing item is indistinguishable from an
192
+ * unattributed commit.
193
+ *
194
+ * Rather than guess, try the value as given and then the plausible
195
+ * translations: add a host if it looks bare, strip one if it looks qualified.
196
+ */
197
+ function repoKeyCandidates(repo: string): string[] {
198
+ const bare = repo.replace(/^https?:\/\//, '').replace(/\.git$/, '').replace(/\/+$/, '');
199
+ const candidates = [bare];
200
+ const segments = bare.split('/');
201
+ if (segments.length === 2) {
202
+ // owner/name → try the hosts codeburn would have recorded.
203
+ candidates.push(`github.com/${bare}`, `gitlab.com/${bare}`);
204
+ } else if (segments.length > 2) {
205
+ // host/owner/name → try dropping the host.
206
+ candidates.push(segments.slice(1).join('/'));
207
+ }
208
+ return [...new Set(candidates)];
209
+ }
210
+
211
+ /**
212
+ * Resolves whether the code a finding lands on was AI-written.
213
+ *
214
+ * Reads the attribution store, which is the only place that answer now lives.
215
+ * This previously queried the events table for `prism.d1.commit` and counted
216
+ * ai vs human across the last 7 days — but nothing emits `prism.d1.commit`.
217
+ * The git hooks that were once expected to only ever wrote commit-message
218
+ * trailers, so the query never matched and every finding was tagged 'unknown',
219
+ * silently, with the CISO dashboard's AI-code-risk panel downstream of it.
220
+ *
221
+ * The per-commit verdict is frozen onto the COMMIT# item at ingest by
222
+ * otel-receiver (`ai_origin`), so a point lookup is enough — no span join, and
223
+ * no dependence on a span TTL that is shorter than the commit TTL.
224
+ */
180
225
  async function lookupAiOrigin(
181
226
  teamId: string,
182
227
  repo: string,
183
228
  commitSha: string | null,
184
229
  prNumber: number | null,
185
230
  ): 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
- );
231
+ // A PR number alone cannot identify a commit in the attribution store, whose
232
+ // key is REPO#<repo>/COMMIT#<sha>. Returning 'unknown' is correct here rather
233
+ // than guessing from the repo's recent history.
234
+ if (!commitSha) return 'unknown';
235
+ if (!AI_USAGE_TABLE) return 'unknown';
208
236
 
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';
237
+ for (const candidate of repoKeyCandidates(repo)) {
238
+ try {
239
+ const result = await dynamoClient.send(
240
+ new GetItemCommand({
241
+ TableName: AI_USAGE_TABLE,
242
+ Key: {
243
+ pk: { S: `REPO#${candidate}` },
244
+ sk: { S: `COMMIT#${commitSha}` },
245
+ },
246
+ ProjectionExpression: 'ai_origin',
247
+ }),
248
+ );
249
+ const origin = result.Item?.ai_origin?.S;
250
+ if (origin === 'ai-generated' || origin === 'human') return origin;
251
+ // Item exists but carries no frozen verdict (written before write-time
252
+ // resolution). Treat as unresolved rather than trying another key.
253
+ if (result.Item) return 'unknown';
254
+ } catch (err) {
255
+ console.error(`AI origin lookup failed for REPO#${candidate}:`, err);
256
+ return 'unknown';
224
257
  }
225
-
226
- return 'unknown';
227
- } catch (err) {
228
- console.error('Failed to look up AI origin:', err);
229
- return 'unknown';
230
258
  }
259
+
260
+ // No attribution for this commit — the author's machine is not onboarded to
261
+ // codeburn sync. Distinct from 'human', which is a positive verdict.
262
+ console.log(`No attribution found for ${repo}@${commitSha} (team ${teamId}, PR ${prNumber ?? 'n/a'})`);
263
+ return 'unknown';
231
264
  }
@@ -839,6 +839,10 @@ export class MetricsPipelineStack extends cdk.Stack {
839
839
  EVENTS_TABLE: this.eventsTable.tableName,
840
840
  METADATA_TABLE: this.metadataTable.tableName,
841
841
  EVENT_BUS_NAME: this.eventBus.eventBusName,
842
+ // Findings are tagged AI vs human by looking the commit up in the
843
+ // attribution store. Left unset when the OTEL collector is skipped,
844
+ // in which case there is no attribution and origin stays 'unknown'.
845
+ AI_USAGE_TABLE: this.aiUsageTable.tableName,
842
846
  },
843
847
  logRetention: logs.RetentionDays.ONE_MONTH,
844
848
  description: 'Normalizes AWS Security Agent findings and emits to PRISM pipeline',
@@ -846,6 +850,10 @@ export class MetricsPipelineStack extends cdk.Stack {
846
850
 
847
851
  this.eventsTable.grantReadData(securityAgentProcessor);
848
852
  this.metadataTable.grantReadData(securityAgentProcessor);
853
+ // Read-only: the processor resolves a finding's AI origin from the frozen
854
+ // ai_origin on the COMMIT# item. grantReadData also grants kms:Decrypt on
855
+ // the table's customer-managed key, which a bare IAM action would not.
856
+ this.aiUsageTable.grantReadData(securityAgentProcessor);
849
857
  this.eventBus.grantPutEventsTo(securityAgentProcessor);
850
858
 
851
859
  // Security Remediation Tracker
@@ -1 +1 @@
1
- {"version":3,"file":"install-eval-harness.d.ts","sourceRoot":"","sources":["../../../../src/commands/bootstrapper/install-eval-harness.ts"],"names":[],"mappings":";;;;;;;;;;;iBA2DqB;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE;;AAVvI,wBA8CE"}
1
+ {"version":3,"file":"install-eval-harness.d.ts","sourceRoot":"","sources":["../../../../src/commands/bootstrapper/install-eval-harness.ts"],"names":[],"mappings":";;;;;;;;;;;iBAmEqB;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE;;AAVvI,wBAwDE"}