Skip to content

Audit NPM Claude Remediation #175

Audit NPM Claude Remediation

Audit NPM Claude Remediation #175

name: Audit NPM Claude Remediation
on:
workflow_run:
workflows: ["Audit NPM Packages"]
types:
- completed
workflow_dispatch:
permissions:
actions: read
contents: write
id-token: write
issues: write
pull-requests: write
concurrency:
group: audit-npm-claude-remediation-${{ github.event.repository.default_branch }}
cancel-in-progress: false
jobs:
remediate:
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'failure' }}
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 60
env:
NPM_AUDIT_CLAUDE_SLACK_WEBHOOK_URL: ${{ secrets.NPM_AUDIT_CLAUDE_SLACK_WEBHOOK_URL }}
steps:
- name: Checkout default branch
uses: actions/checkout@v5
with:
ref: ${{ github.event.repository.default_branch }}
- name: Install pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
with:
version: 10
- name: Scan default branch audit failures
id: scan
env:
GH_TOKEN: ${{ github.token }}
GITHUB_TOKEN: ${{ github.token }}
run: node .github/scripts/audit-ci-vuln-scan.mjs
- name: Skip when all vulnerabilities are covered
if: ${{ steps.scan.outputs.has_uncovered != 'true' }}
run: |
echo "No uncovered audit-ci vulnerabilities on ${GITHUB_REF_NAME}; skipping Claude."
- name: Run Claude remediation
id: claude
if: ${{ steps.scan.outputs.has_uncovered == 'true' }}
uses: anthropics/claude-code-action@2cc1ac1331eac7a6a96d716dd204dd2888d0fcd2 # v1
with:
anthropic_api_key: ${{ secrets.NPM_AUDIT_CLAUDE_ANTHROPIC_API_KEY }}
base_branch: ${{ github.event.repository.default_branch }}
branch_prefix: claude/audit-ci/
prompt: ${{ steps.scan.outputs.prompt }}
claude_args: |
--max-turns 40
--allowedTools "Read,Edit,MultiEdit,Write,Glob,Grep,LS,WebFetch,WebSearch,TodoWrite,Bash(pnpm:*),Bash(npm:*),Bash(node:*),Bash(git:*),Bash(gh:*),Bash(jq:*),Bash(rg:*),Bash(curl:*),Bash(date:*)"
- name: Ensure remediation PR marker
id: remediation_pr
if: ${{ steps.scan.outputs.has_uncovered == 'true' && steps.claude.outputs.branch_name != '' }}
env:
GH_TOKEN: ${{ github.token }}
BRANCH_NAME: ${{ steps.claude.outputs.branch_name }}
MARKER: ${{ steps.scan.outputs.marker }}
run: |
set -euo pipefail
PR_NUMBER="$(gh pr list --head "$BRANCH_NAME" --state open --json number --jq '.[0].number // empty')"
if [ -z "$PR_NUMBER" ]; then
echo "::warning::Claude did not leave an open PR for branch ${BRANCH_NAME}; marker could not be enforced."
exit 0
fi
PR_URL="$(gh pr view "$PR_NUMBER" --json url --jq '.url')"
PR_TITLE="$(gh pr view "$PR_NUMBER" --json title --jq '.title')"
PR_SUMMARY="$(node -e 'const title = process.argv[1] || ""; console.log(title.replace(/^[A-Za-z]+(?:\([^)]+\))?!?:\s*/, ""));' "$PR_TITLE")"
echo "pr_url=${PR_URL}" >> "$GITHUB_OUTPUT"
echo "summary=${PR_SUMMARY}" >> "$GITHUB_OUTPUT"
gh label create audit-ci-remediation \
--description "Automated audit-ci vulnerability remediation" \
--color "B60205" \
2>/dev/null || true
gh pr edit "$PR_NUMBER" --add-label audit-ci-remediation
BODY_FILE="$(mktemp)"
UPDATED_BODY_FILE="$(mktemp)"
gh pr view "$PR_NUMBER" --json body --jq '.body // ""' > "$BODY_FILE"
node - "$BODY_FILE" "$UPDATED_BODY_FILE" "$MARKER" <<'NODE'
const { readFileSync, writeFileSync } = require("node:fs");
const [, , inputPath, outputPath, marker] = process.argv;
const markerRegex = /<!--\s*audit-ci-vuln-keys:\s*\[[\s\S]*?\]\s*-->/;
let body = readFileSync(inputPath, "utf8");
if (!markerRegex.test(body)) {
if (/^## Summary\b.*$/m.test(body)) {
body = body.replace(/^## Summary\b.*$/m, (heading) => `${heading}\n\n${marker}`);
} else {
body = `## Summary\n\n${marker}\n\n${body.trim()}\n`;
}
}
writeFileSync(outputPath, body);
NODE
gh pr edit "$PR_NUMBER" --body-file "$UPDATED_BODY_FILE"
- name: Request code review
if: ${{ steps.scan.outputs.has_uncovered == 'true' && steps.claude.outputs.branch_name != '' }}
env:
GH_TOKEN: ${{ github.token }}
BRANCH_NAME: ${{ steps.claude.outputs.branch_name }}
run: |
set -euo pipefail
PR_NUMBER="$(gh pr list --head "$BRANCH_NAME" --state open --json number --jq '.[0].number // empty')"
if [ -z "$PR_NUMBER" ]; then
echo "::warning::No open PR for ${BRANCH_NAME}; cannot request reviewers."
exit 0
fi
gh pr edit "$PR_NUMBER" --add-reviewer abimaelmartell,mogery
- name: Notify Slack about remediation PR
if: ${{ steps.remediation_pr.outputs.pr_url != '' && env.NPM_AUDIT_CLAUDE_SLACK_WEBHOOK_URL != '' }}
env:
SLACK_WEBHOOK_URL: ${{ env.NPM_AUDIT_CLAUDE_SLACK_WEBHOOK_URL }}
PR_URL: ${{ steps.remediation_pr.outputs.pr_url }}
SUMMARY: ${{ steps.remediation_pr.outputs.summary }}
run: |
set -euo pipefail
jq -n \
--arg pr_url "$PR_URL" \
--arg summary "$SUMMARY" \
'{pr_url: $pr_url, summary: $summary}' \
| curl --fail-with-body --show-error --silent \
--request POST \
--header "Content-Type: application/json" \
--data @- \
"$SLACK_WEBHOOK_URL"