@prism-d1/cli 1.0.93 → 1.0.95
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/github-workflows/prism-eval-gate-kiro.yml +311 -1
- package/dist/assets/gitlab-workflows/prism-eval-gate.yml +333 -116
- package/dist/assets/infra/lib/lambda/velocity-widget.ts +1 -1
- package/dist/src/commands/bedrock-protection/scan-account.d.ts +45 -0
- package/dist/src/commands/bedrock-protection/scan-account.d.ts.map +1 -0
- package/dist/src/commands/bedrock-protection/scan-account.js +105 -0
- package/dist/src/commands/bedrock-protection/scan-account.js.map +1 -0
- package/dist/src/commands/bedrock-protection/scan-repo.d.ts +16 -0
- package/dist/src/commands/bedrock-protection/scan-repo.d.ts.map +1 -0
- package/dist/src/commands/bedrock-protection/scan-repo.js +397 -0
- package/dist/src/commands/bedrock-protection/scan-repo.js.map +1 -0
- package/dist/src/commands/bootstrapper/install-eval-harness.d.ts +5 -5
- package/dist/src/commands/bootstrapper/install-eval-harness.d.ts.map +1 -1
- package/dist/src/commands/bootstrapper/install-eval-harness.js +157 -75
- package/dist/src/commands/bootstrapper/install-eval-harness.js.map +1 -1
- package/dist/src/commands/bootstrapper/install-github-workflows.d.ts +6 -2
- package/dist/src/commands/bootstrapper/install-github-workflows.d.ts.map +1 -1
- package/dist/src/commands/bootstrapper/install-github-workflows.js +36 -48
- package/dist/src/commands/bootstrapper/install-github-workflows.js.map +1 -1
- package/dist/src/commands/bootstrapper/install-gitlab-workflows.d.ts.map +1 -1
- package/dist/src/commands/bootstrapper/install-gitlab-workflows.js +10 -1
- package/dist/src/commands/bootstrapper/install-gitlab-workflows.js.map +1 -1
- package/dist/src/utils/audit-bedrock.d.ts +24 -0
- package/dist/src/utils/audit-bedrock.d.ts.map +1 -0
- package/dist/src/utils/audit-bedrock.js +331 -0
- package/dist/src/utils/audit-bedrock.js.map +1 -0
- package/dist/src/utils/audit-iam.d.ts +6 -0
- package/dist/src/utils/audit-iam.d.ts.map +1 -0
- package/dist/src/utils/audit-iam.js +239 -0
- package/dist/src/utils/audit-iam.js.map +1 -0
- package/dist/src/utils/audit.d.ts +61 -0
- package/dist/src/utils/audit.d.ts.map +1 -0
- package/dist/src/utils/audit.js +90 -0
- package/dist/src/utils/audit.js.map +1 -0
- package/dist/src/utils/aws.d.ts +38 -0
- package/dist/src/utils/aws.d.ts.map +1 -0
- package/dist/src/utils/aws.js +61 -0
- package/dist/src/utils/aws.js.map +1 -0
- package/package.json +1 -1
- package/dist/assets/github-workflows/prism-eval-gate.yml +0 -490
|
@@ -1,490 +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
|
-
# Continuum rejects a title containing anything outside
|
|
298
|
-
# [A-Za-z0-9_-]. REPO_SLUG keeps the dot from the host component
|
|
299
|
-
# (github.com-owner-repo) because SSM parameter names and S3 keys
|
|
300
|
-
# both accept dots and those paths must stay stable. The title is
|
|
301
|
-
# the only consumer that cannot, so it gets its own sanitised form.
|
|
302
|
-
#
|
|
303
|
-
# This broke when CI switched to emitting fully-qualified repo names
|
|
304
|
-
# (github.com/owner/repo) so attribution keys would match codeburn's.
|
|
305
|
-
# A bare owner/repo slug had no dot and validated fine.
|
|
306
|
-
#
|
|
307
|
-
# printf, not echo: tr -c would otherwise rewrite the trailing
|
|
308
|
-
# newline into a hyphen and leave every title ending in one.
|
|
309
|
-
CR_TITLE=$(printf '%s-ci-diff-scan' "${REPO_SLUG}" | tr -c 'A-Za-z0-9_-' '-' | cut -c1-100)
|
|
310
|
-
|
|
311
|
-
# Create the CodeReview
|
|
312
|
-
CR_OUTPUT=$(aws securityagent create-code-review \
|
|
313
|
-
--agent-space-id "$AGENT_SPACE_ID" \
|
|
314
|
-
--title "$CR_TITLE" \
|
|
315
|
-
--service-role "$SERVICE_ROLE" \
|
|
316
|
-
--assets "{\"sourceCode\":[{\"s3Location\":\"s3://${SCAN_BUCKET}/repo/${REPO_SLUG}/latest.zip\"}]}" \
|
|
317
|
-
--output json 2>&1) || true
|
|
318
|
-
# `|| echo ""` is load-bearing. CR_OUTPUT captures stderr via 2>&1,
|
|
319
|
-
# so a failed create leaves AWS's error TEXT here, not JSON. jq exits
|
|
320
|
-
# 5 on a parse error, and under bash -eo pipefail that aborts the
|
|
321
|
-
# whole step -- which made the diagnostic below unreachable and
|
|
322
|
-
# surfaced a bare "Process completed with exit code 5" instead of the
|
|
323
|
-
# actual ValidationException.
|
|
324
|
-
CODE_REVIEW_ID=$(echo "$CR_OUTPUT" | jq -r '.codeReviewId // empty' 2>/dev/null || echo "")
|
|
325
|
-
|
|
326
|
-
if [[ -z "$CODE_REVIEW_ID" ]]; then
|
|
327
|
-
echo " ⚠️ Bootstrap failed:"
|
|
328
|
-
echo " $CR_OUTPUT"
|
|
329
|
-
echo " Run 'prism-cli securityagent setup' to create it manually."
|
|
330
|
-
echo "scan_error=true" >> "$GITHUB_OUTPUT"
|
|
331
|
-
echo "findings=-1" >> "$GITHUB_OUTPUT"
|
|
332
|
-
exit 0
|
|
333
|
-
fi
|
|
334
|
-
|
|
335
|
-
# Persist for future runs
|
|
336
|
-
aws ssm put-parameter \
|
|
337
|
-
--name "/prism/continuum/code-review-id/${REPO_SLUG}" \
|
|
338
|
-
--value "$CODE_REVIEW_ID" \
|
|
339
|
-
--type String --overwrite > /dev/null
|
|
340
|
-
|
|
341
|
-
echo " [x] Code Review created: ${CODE_REVIEW_ID}"
|
|
342
|
-
echo " [x] Saved to SSM"
|
|
343
|
-
fi
|
|
344
|
-
|
|
345
|
-
echo " Using Code Review: ${CODE_REVIEW_ID}"
|
|
346
|
-
|
|
347
|
-
# Upload fresh repo ZIP (context for accurate cross-module analysis)
|
|
348
|
-
git archive HEAD --format=zip -o /tmp/repo.zip
|
|
349
|
-
aws s3 cp /tmp/repo.zip "s3://${SCAN_BUCKET}/repo/${REPO_SLUG}/latest.zip" --quiet
|
|
350
|
-
echo "::endgroup::"
|
|
351
|
-
|
|
352
|
-
echo "::group::Generate and upload diff"
|
|
353
|
-
BASE_SHA="${{ github.event.pull_request.base.sha }}"
|
|
354
|
-
HEAD_SHA="${{ github.sha }}"
|
|
355
|
-
git diff "${BASE_SHA}..${HEAD_SHA}" > /tmp/changes.patch
|
|
356
|
-
|
|
357
|
-
DIFF_SIZE=$(wc -c < /tmp/changes.patch)
|
|
358
|
-
if [[ "$DIFF_SIZE" -eq 0 ]]; then
|
|
359
|
-
echo "No changes to scan."
|
|
360
|
-
echo "findings=0" >> "$GITHUB_OUTPUT"
|
|
361
|
-
exit 0
|
|
362
|
-
fi
|
|
363
|
-
|
|
364
|
-
DIFF_KEY="diffs/${REPO_SLUG}/${{ github.event.pull_request.number }}/${HEAD_SHA}.patch"
|
|
365
|
-
aws s3 cp /tmp/changes.patch "s3://${SCAN_BUCKET}/${DIFF_KEY}" --quiet
|
|
366
|
-
echo "::endgroup::"
|
|
367
|
-
|
|
368
|
-
echo "::group::Start Continuum diff scan"
|
|
369
|
-
JOB_RESPONSE=$(aws securityagent start-code-review-job \
|
|
370
|
-
--agent-space-id "${AGENT_SPACE_ID}" \
|
|
371
|
-
--code-review-id "${CODE_REVIEW_ID}" \
|
|
372
|
-
--diff-source "{\"s3Uri\":\"s3://${SCAN_BUCKET}/${DIFF_KEY}\"}" \
|
|
373
|
-
--output json)
|
|
374
|
-
|
|
375
|
-
JOB_ID=$(echo "$JOB_RESPONSE" | jq -r '.codeReviewJobId')
|
|
376
|
-
echo "Job started: ${JOB_ID}"
|
|
377
|
-
echo "::endgroup::"
|
|
378
|
-
|
|
379
|
-
echo "::group::Poll for completion (timeout: 30 min)"
|
|
380
|
-
for i in $(seq 1 60); do
|
|
381
|
-
sleep 30
|
|
382
|
-
STATUS=$(aws securityagent batch-get-code-review-jobs \
|
|
383
|
-
--agent-space-id "${AGENT_SPACE_ID}" \
|
|
384
|
-
--code-review-job-ids "${JOB_ID}" \
|
|
385
|
-
--query 'codeReviewJobs[0].status' --output text 2>/dev/null || echo "UNKNOWN")
|
|
386
|
-
|
|
387
|
-
echo " Poll ${i}/60: ${STATUS}"
|
|
388
|
-
if [[ "$STATUS" == "COMPLETED" || "$STATUS" == "FAILED" || "$STATUS" == "STOPPED" ]]; then
|
|
389
|
-
break
|
|
390
|
-
fi
|
|
391
|
-
done
|
|
392
|
-
echo "::endgroup::"
|
|
393
|
-
|
|
394
|
-
if [[ "$STATUS" == "FAILED" ]]; then
|
|
395
|
-
echo "::warning::Continuum scan failed — check Security Agent logs"
|
|
396
|
-
echo "scan_error=true" >> "$GITHUB_OUTPUT"
|
|
397
|
-
echo "findings=-1" >> "$GITHUB_OUTPUT"
|
|
398
|
-
exit 0
|
|
399
|
-
fi
|
|
400
|
-
|
|
401
|
-
if [[ "$STATUS" != "COMPLETED" ]]; then
|
|
402
|
-
echo "::warning::Continuum scan timed out (status: ${STATUS})"
|
|
403
|
-
echo "scan_error=true" >> "$GITHUB_OUTPUT"
|
|
404
|
-
echo "findings=-1" >> "$GITHUB_OUTPUT"
|
|
405
|
-
exit 0
|
|
406
|
-
fi
|
|
407
|
-
|
|
408
|
-
echo "::group::Retrieve findings"
|
|
409
|
-
# No empty-array fallback: an API error there is indistinguishable from a
|
|
410
|
-
# clean scan, so it would pass the gate.
|
|
411
|
-
if ! FINDINGS_JSON=$(aws securityagent list-findings \
|
|
412
|
-
--agent-space-id "${AGENT_SPACE_ID}" \
|
|
413
|
-
--code-review-job-id "${JOB_ID}" \
|
|
414
|
-
--output json 2>&1); then
|
|
415
|
-
echo "::error::Continuum scan completed but its findings could not be retrieved: ${FINDINGS_JSON}"
|
|
416
|
-
echo "scan_error=true" >> "$GITHUB_OUTPUT"
|
|
417
|
-
echo "findings=-1" >> "$GITHUB_OUTPUT"
|
|
418
|
-
echo "::endgroup::"
|
|
419
|
-
exit 0
|
|
420
|
-
fi
|
|
421
|
-
|
|
422
|
-
TOTAL=$(echo "$FINDINGS_JSON" | jq '.findingsSummaries | length')
|
|
423
|
-
CRITICAL=$(echo "$FINDINGS_JSON" | jq '[.findingsSummaries[] | select(.riskLevel == "CRITICAL")] | length')
|
|
424
|
-
HIGH=$(echo "$FINDINGS_JSON" | jq '[.findingsSummaries[] | select(.riskLevel == "HIGH")] | length')
|
|
425
|
-
MEDIUM=$(echo "$FINDINGS_JSON" | jq '[.findingsSummaries[] | select(.riskLevel == "MEDIUM")] | length')
|
|
426
|
-
|
|
427
|
-
echo "Findings: ${TOTAL} total (${CRITICAL} critical, ${HIGH} high, ${MEDIUM} medium)"
|
|
428
|
-
echo "findings=${TOTAL}" >> "$GITHUB_OUTPUT"
|
|
429
|
-
echo "critical=${CRITICAL}" >> "$GITHUB_OUTPUT"
|
|
430
|
-
echo "high=${HIGH}" >> "$GITHUB_OUTPUT"
|
|
431
|
-
echo "findings_json=$(echo "$FINDINGS_JSON" | jq -c '.findingsSummaries')" >> "$GITHUB_OUTPUT"
|
|
432
|
-
echo "::endgroup::"
|
|
433
|
-
|
|
434
|
-
# Print findings summary for PR annotations
|
|
435
|
-
if [[ "$TOTAL" -gt 0 ]]; then
|
|
436
|
-
echo "$FINDINGS_JSON" | jq -r '.findingsSummaries[] | "::error ::[\(.riskLevel)] \(.name | .[0:120])"'
|
|
437
|
-
fi
|
|
438
|
-
|
|
439
|
-
- name: Emit Continuum findings to EventBridge
|
|
440
|
-
if: steps.security-gate.outputs.findings != '0' && steps.security-gate.outputs.skipped != 'true'
|
|
441
|
-
env:
|
|
442
|
-
AWS_REGION: ${{ env.PRISM_AWS_REGION }}
|
|
443
|
-
run: |
|
|
444
|
-
PR_NUMBER="${{ github.event.pull_request.number }}"
|
|
445
|
-
REPO="${PRISM_REPO}"
|
|
446
|
-
TEAM_ID="${{ steps.prism-config.outputs.team_id }}"
|
|
447
|
-
FINDINGS='${{ steps.security-gate.outputs.findings_json }}'
|
|
448
|
-
|
|
449
|
-
AI_ORIGIN="human"
|
|
450
|
-
BASE_SHA="${{ github.event.pull_request.base.sha }}"
|
|
451
|
-
HEAD_SHA="${{ github.sha }}"
|
|
452
|
-
for SHA in $(git rev-list "${BASE_SHA}..${HEAD_SHA}"); do
|
|
453
|
-
if git log -1 --format='%B' "${SHA}" | grep -q 'AI-Origin: ai-generated\|AI-Origin: ai-assisted'; then
|
|
454
|
-
AI_ORIGIN="ai-assisted"; break
|
|
455
|
-
fi
|
|
456
|
-
done
|
|
457
|
-
|
|
458
|
-
ENTRIES=$(echo "${FINDINGS}" | jq --arg team_id "${TEAM_ID}" --arg repo "${REPO}" \
|
|
459
|
-
--arg ai_origin "${AI_ORIGIN}" --argjson pr_number "${PR_NUMBER}" '
|
|
460
|
-
to_entries | map(.value as $f |
|
|
461
|
-
{Source:"prism.d1.velocity", DetailType:"prism.d1.security.code_review",
|
|
462
|
-
EventBusName:"prism-d1-metrics",
|
|
463
|
-
Detail:({team_id:$team_id, repo:$repo, timestamp:($f.createdAt // now | tostring), prism_level:3,
|
|
464
|
-
metric:{name:"security_finding",value:1,unit:"count"},
|
|
465
|
-
ai_context:{tool:"continuum",model:"aws-continuum",origin:$ai_origin},
|
|
466
|
-
security_agent_finding:{finding_id:($f.findingId // "cf-pr\($pr_number)-\(.key)"), phase:"code_review",
|
|
467
|
-
severity:($f.riskLevel // "MEDIUM"),
|
|
468
|
-
cwe_id:($f.cweId // null),
|
|
469
|
-
ai_origin:$ai_origin, found_at:($f.createdAt // null)}
|
|
470
|
-
} | tostring)})' 2>/dev/null || echo "[]")
|
|
471
|
-
|
|
472
|
-
TOTAL=$(echo "${ENTRIES}" | jq 'length')
|
|
473
|
-
if [[ "$TOTAL" -gt 0 ]]; then
|
|
474
|
-
for i in $(seq 0 10 $((TOTAL - 1))); do
|
|
475
|
-
aws events put-events --entries "$(echo "${ENTRIES}" | jq ".[$i:$((i+10))]")"
|
|
476
|
-
done
|
|
477
|
-
echo "Emitted ${TOTAL} finding events to EventBridge"
|
|
478
|
-
fi
|
|
479
|
-
|
|
480
|
-
- name: Fail on eval or security issues
|
|
481
|
-
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')
|
|
482
|
-
run: |
|
|
483
|
-
[[ "${{ steps.eval.outputs.overall_result }}" == "FAIL" ]] && echo "❌ Eval gate failed"
|
|
484
|
-
if [[ "${{ steps.security-gate.outputs.scan_error }}" == "true" ]]; then
|
|
485
|
-
echo "⚠️ AWS Continuum scan encountered an error (did not complete successfully)"
|
|
486
|
-
echo " The security gate cannot verify this PR is safe. Re-run the workflow or check Security Agent logs."
|
|
487
|
-
elif [[ "${{ steps.security-gate.outputs.findings }}" != "0" && "${{ steps.security-gate.outputs.skipped }}" != "true" ]]; then
|
|
488
|
-
echo "❌ AWS Continuum found ${{ steps.security-gate.outputs.findings }} security issues (${{ steps.security-gate.outputs.critical }} critical, ${{ steps.security-gate.outputs.high }} high)"
|
|
489
|
-
fi
|
|
490
|
-
exit 1
|