From b53746665528887263600a97a1e93a6cdc161fb4 Mon Sep 17 00:00:00 2001 From: Russell Stern Date: Thu, 18 Jun 2026 10:48:11 -0400 Subject: [PATCH 1/2] Added vault audit workflows --- .github/workflows/vault-audit-commands.yml | 137 +++++++++++++ .github/workflows/vault-audit-gate.yml | 32 +++ .../workflows/vault-audit-thread-commands.yml | 184 ++++++++++++++++++ 3 files changed, 353 insertions(+) create mode 100644 .github/workflows/vault-audit-commands.yml create mode 100644 .github/workflows/vault-audit-gate.yml create mode 100644 .github/workflows/vault-audit-thread-commands.yml diff --git a/.github/workflows/vault-audit-commands.yml b/.github/workflows/vault-audit-commands.yml new file mode 100644 index 00000000000..3b62273efc0 --- /dev/null +++ b/.github/workflows/vault-audit-commands.yml @@ -0,0 +1,137 @@ +name: Vault Audit Commands + +# Handles vault audit slash commands from authorized reviewers: +# /vault-audit — run the audit +# /vault-audit skip — bypass the audit entirely +# +# To resolve individual findings, reply /resolved directly on each +# blocking finding thread (handled by vault-audit-thread-commands.yml). +# +# Authorization: commenter must have write or admin permission on this repository. + +on: + issue_comment: + types: [created] + +jobs: + handle-command: + if: | + github.event.issue.pull_request != null && + ( + startsWith(github.event.comment.body, '/vault-audit skip ') || + github.event.comment.body == '/vault-audit' || + startsWith(github.event.comment.body, '/vault-audit ') + ) + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + statuses: write + outputs: + run_audit: ${{ steps.cmd.outputs.type == 'run' && steps.auth.outputs.authorized == 'true' }} + head_sha: ${{ steps.pr.outputs.head_sha }} + base_sha: ${{ steps.pr.outputs.base_sha }} + + steps: + - name: Check commenter authorization + id: auth + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + COMMENTER: ${{ github.event.comment.user.login }} + run: | + PERMISSION=$(gh api "/repos/${REPO}/collaborators/${COMMENTER}/permission" \ + --jq '.permission' 2>/dev/null || echo "none") + + if [[ "$PERMISSION" == "write" || "$PERMISSION" == "admin" ]]; then + echo "authorized=true" >> "$GITHUB_OUTPUT" + else + echo "authorized=false" >> "$GITHUB_OUTPUT" + fi + + - name: Reject unauthorized commenter + if: steps.auth.outputs.authorized == 'false' + env: + GH_TOKEN: ${{ github.token }} + COMMENTER: ${{ github.event.comment.user.login }} + PR_NUMBER: ${{ github.event.issue.number }} + run: | + gh pr comment "$PR_NUMBER" \ + --body "⛔ @${COMMENTER} — only authorized reviewers (repository write access) can use vault audit commands." + exit 1 + + - name: Get PR details + id: pr + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.issue.number }} + run: | + PR=$(gh api "/repos/${REPO}/pulls/${PR_NUMBER}" \ + --jq '{head_sha: .head.sha, base_sha: .base.sha}') + echo "head_sha=$(echo "$PR" | jq -r '.head_sha')" >> "$GITHUB_OUTPUT" + echo "base_sha=$(echo "$PR" | jq -r '.base_sha')" >> "$GITHUB_OUTPUT" + + - name: Parse command + id: cmd + env: + # Pass through env to avoid script injection from untrusted comment body + COMMENT_BODY: ${{ github.event.comment.body }} + run: | + if echo "$COMMENT_BODY" | grep -qP '^/vault-audit skip .+'; then + echo "type=skip" >> "$GITHUB_OUTPUT" + REASON="${COMMENT_BODY#/vault-audit skip }" + echo "reason=${REASON}" >> "$GITHUB_OUTPUT" + else + echo "type=run" >> "$GITHUB_OUTPUT" + fi + + # ── /vault-audit skip ──────────────────────────────────────────── + - name: Handle skip + if: steps.cmd.outputs.type == 'skip' + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.issue.number }} + COMMENTER: ${{ github.event.comment.user.login }} + REASON: ${{ steps.cmd.outputs.reason }} + SHA: ${{ steps.pr.outputs.head_sha }} + run: | + gh api "/repos/${REPO}/statuses/${SHA}" \ + --method POST \ + -f state=success \ + -f context="vault-audit" \ + -f description="Vault audit skipped by ${COMMENTER}: ${REASON}" + + gh pr comment "$PR_NUMBER" \ + --body "✅ Vault audit skipped by @${COMMENTER} + + **Reason:** ${REASON} + + > ⚠️ This skip applies to the current HEAD (\`${SHA:0:8}\`). Pushing new vault file changes will re-require an audit." + + # ── /vault-audit ───────────────────────────────────────────────────────── + - name: Acknowledge audit request + if: steps.cmd.outputs.type == 'run' + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.issue.number }} + COMMENTER: ${{ github.event.comment.user.login }} + run: | + gh pr comment "$PR_NUMBER" \ + --body "🔍 Vault audit triggered by @${COMMENTER} — running now. Results will appear as a new comment when complete." + + # Reusable workflows must be called as a top-level job, not a step + run-audit: + needs: handle-command + if: needs.handle-command.outputs.run_audit == 'true' + uses: smartcontractkit/cre-docs/.github/workflows/vault-audit.yml@main + with: + pr_number: ${{ github.event.issue.number }} + head_sha: ${{ needs.handle-command.outputs.head_sha }} + base_sha: ${{ needs.handle-command.outputs.base_sha }} + chainlink_repo: ${{ github.repository }} + secrets: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + CRE_DOCS_TOKEN: ${{ secrets.CRE_DOCS_TOKEN }} + CHAINLINK_TOKEN: ${{ github.token }} diff --git a/.github/workflows/vault-audit-gate.yml b/.github/workflows/vault-audit-gate.yml new file mode 100644 index 00000000000..7c62d5c9145 --- /dev/null +++ b/.github/workflows/vault-audit-gate.yml @@ -0,0 +1,32 @@ +name: Vault Audit Gate + +# Posts a "pending" commit status whenever vault-related files are touched, requiring +# an authorized reviewer to trigger the audit before the PR can merge. + +on: + pull_request: + types: [opened, synchronize, reopened] + paths: + - 'core/capabilities/vault/**' + - 'core/services/ocr2/plugins/vault/**' + - 'core/services/gateway/handlers/vault/**' + - 'core/services/workflows/v2/secrets.go' + - 'system-tests/tests/smoke/cre/vault_don_test.go' + - 'system-tests/tests/smoke/cre/vault_don_test_helpers.go' + - 'system-tests/lib/cre/vault/**' + +jobs: + gate: + runs-on: ubuntu-latest + permissions: + statuses: write + steps: + - name: Post pending status + env: + GH_TOKEN: ${{ github.token }} + run: | + gh api /repos/${{ github.repository }}/statuses/${{ github.event.pull_request.head.sha }} \ + --method POST \ + -f state=pending \ + -f context="vault-audit" \ + -f description="Vault audit required — an authorized reviewer must comment /vault-audit" diff --git a/.github/workflows/vault-audit-thread-commands.yml b/.github/workflows/vault-audit-thread-commands.yml new file mode 100644 index 00000000000..bb76752af87 --- /dev/null +++ b/.github/workflows/vault-audit-thread-commands.yml @@ -0,0 +1,184 @@ +name: Vault Audit Thread Commands + +# Handles /resolved replies on individual vault audit finding threads. +# When an authorized reviewer replies /resolved on a blocking finding thread, +# this workflow checks whether all blocking findings are now resolved and updates +# the commit status accordingly. + +on: + pull_request_review_comment: + types: [created] + +jobs: + handle-resolved: + if: startsWith(github.event.comment.body, '/resolved') + runs-on: ubuntu-latest + permissions: + pull-requests: write + statuses: write + + steps: + - name: Check commenter authorization + id: auth + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + COMMENTER: ${{ github.event.comment.user.login }} + run: | + PERMISSION=$(gh api "/repos/${REPO}/collaborators/${COMMENTER}/permission" \ + --jq '.permission' 2>/dev/null || echo "none") + if [[ "$PERMISSION" == "write" || "$PERMISSION" == "admin" ]]; then + echo "authorized=true" >> "$GITHUB_OUTPUT" + else + echo "authorized=false" >> "$GITHUB_OUTPUT" + fi + + - name: Skip if unauthorized + if: steps.auth.outputs.authorized == 'false' + run: | + echo "Commenter is not authorized — ignoring /resolved." + exit 0 + + - name: Find root comment of this thread + id: thread + if: steps.auth.outputs.authorized == 'true' + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + IN_REPLY_TO: ${{ github.event.comment.in_reply_to_id }} + run: | + # If this isn't a reply at all, it's not a thread response — ignore + if [ -z "$IN_REPLY_TO" ] || [ "$IN_REPLY_TO" = "0" ] || [ "$IN_REPLY_TO" = "null" ]; then + echo "Comment is not a reply — ignoring." + echo "root_id=" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Walk up in_reply_to_id chain to find the root comment of the thread + CURRENT="${IN_REPLY_TO}" + while true; do + PARENT=$(gh api "/repos/${REPO}/pulls/comments/${CURRENT}" \ + --jq '.in_reply_to_id // empty' 2>/dev/null || echo "") + if [ -z "$PARENT" ] || [ "$PARENT" = "0" ] || [ "$PARENT" = "null" ]; then + break + fi + CURRENT="$PARENT" + done + + echo "root_id=${CURRENT}" >> "$GITHUB_OUTPUT" + + - name: Find vault audit meta comment + id: meta + if: steps.thread.outputs.root_id != '' + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + COMMENT=$(gh api "/repos/${REPO}/issues/${PR_NUMBER}/comments" \ + --jq '[.[] | select(.body | contains("vault-audit-meta"))] | last') + + if [ -z "$COMMENT" ] || [ "$COMMENT" = "null" ]; then + echo "No vault audit comment found on this PR — ignoring." + echo "found=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + BODY=$(echo "$COMMENT" | jq -r '.body') + SHA=$(echo "$BODY" | grep -oP '(?<=sha: )[a-f0-9]+') + THREAD_MAP=$(echo "$BODY" | grep -oP '(?<=thread_map: ).+') + + echo "found=true" >> "$GITHUB_OUTPUT" + echo "sha=${SHA}" >> "$GITHUB_OUTPUT" + echo "thread_map=${THREAD_MAP}" >> "$GITHUB_OUTPUT" + + - name: Check if this thread is a known blocking finding + id: finding + if: steps.meta.outputs.found == 'true' + env: + THREAD_MAP: ${{ steps.meta.outputs.thread_map }} + ROOT_ID: ${{ steps.thread.outputs.root_id }} + run: | + FINDING_ID=$(echo "$THREAD_MAP" | jq -r \ + --argjson rid "$ROOT_ID" \ + '[.[] | select(.comment_id == $rid)] | first | .finding_id // empty') + + if [ -z "$FINDING_ID" ]; then + echo "This thread is not a vault audit blocking finding — ignoring." + echo "is_finding=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "is_finding=true" >> "$GITHUB_OUTPUT" + echo "finding_id=${FINDING_ID}" >> "$GITHUB_OUTPUT" + + - name: Check whether all blocking findings are resolved + id: check_all + if: steps.finding.outputs.is_finding == 'true' + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + THREAD_MAP: ${{ steps.meta.outputs.thread_map }} + run: | + ALL_COMMENTS=$(gh api "/repos/${REPO}/pulls/${PR_NUMBER}/comments" --jq '.') + + ALL_RESOLVED=true + UNRESOLVED_IDS="" + TOTAL=0 + RESOLVED_COUNT=0 + + while IFS= read -r entry; do + COMMENT_ID=$(echo "$entry" | jq -r '.comment_id') + FINDING_ID=$(echo "$entry" | jq -r '.finding_id') + TOTAL=$((TOTAL + 1)) + + HAS_RESOLVED=$(echo "$ALL_COMMENTS" | jq \ + --argjson cid "$COMMENT_ID" \ + '[.[] | select(.in_reply_to_id == $cid) | select(.body | startswith("/resolved"))] | length') + + if [ "$HAS_RESOLVED" -gt 0 ]; then + RESOLVED_COUNT=$((RESOLVED_COUNT + 1)) + else + ALL_RESOLVED=false + UNRESOLVED_IDS="${UNRESOLVED_IDS} ${FINDING_ID}" + fi + done < <(echo "$THREAD_MAP" | jq -c '.[]') + + echo "all_resolved=${ALL_RESOLVED}" >> "$GITHUB_OUTPUT" + echo "resolved_count=${RESOLVED_COUNT}" >> "$GITHUB_OUTPUT" + echo "total=${TOTAL}" >> "$GITHUB_OUTPUT" + echo "unresolved_ids=${UNRESOLVED_IDS}" >> "$GITHUB_OUTPUT" + + - name: Update commit status + if: steps.finding.outputs.is_finding == 'true' + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + SHA: ${{ steps.meta.outputs.sha }} + ALL_RESOLVED: ${{ steps.check_all.outputs.all_resolved }} + RESOLVED_COUNT: ${{ steps.check_all.outputs.resolved_count }} + TOTAL: ${{ steps.check_all.outputs.total }} + UNRESOLVED_IDS: ${{ steps.check_all.outputs.unresolved_ids }} + COMMENTER: ${{ github.event.comment.user.login }} + FINDING_ID: ${{ steps.finding.outputs.finding_id }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + if [ "$ALL_RESOLVED" = "true" ]; then + gh api "/repos/${REPO}/statuses/${SHA}" \ + --method POST \ + -f state=success \ + -f context="vault-audit" \ + -f description="Vault audit: all ${TOTAL} finding(s) resolved by reviewers" + else + gh api "/repos/${REPO}/statuses/${SHA}" \ + --method POST \ + -f state=failure \ + -f context="vault-audit" \ + -f description="Vault audit: ${RESOLVED_COUNT}/${TOTAL} finding(s) resolved — outstanding:${UNRESOLVED_IDS}" + + gh pr comment "$PR_NUMBER" \ + --body "✅ **${FINDING_ID}** marked as resolved by @${COMMENTER} (${RESOLVED_COUNT}/${TOTAL} findings resolved). + + Remaining: \`${UNRESOLVED_IDS}\` — reply \`/resolved \` directly on each finding thread to unblock merge." + fi From 2e16319120afbe5af629be47fc1e15b45a945f92 Mon Sep 17 00:00:00 2001 From: Russell Stern Date: Fri, 19 Jun 2026 16:05:04 -0400 Subject: [PATCH 2/2] Switched to using a GATI token --- .github/workflows/vault-audit-commands.yml | 62 ++++++++++++++-------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/.github/workflows/vault-audit-commands.yml b/.github/workflows/vault-audit-commands.yml index 3b62273efc0..5d2d14b967f 100644 --- a/.github/workflows/vault-audit-commands.yml +++ b/.github/workflows/vault-audit-commands.yml @@ -1,7 +1,7 @@ name: Vault Audit Commands # Handles vault audit slash commands from authorized reviewers: -# /vault-audit — run the audit +# /vault-audit — run the audit (dispatches to cre-docs) # /vault-audit skip — bypass the audit entirely # # To resolve individual findings, reply /resolved directly on each @@ -27,10 +27,6 @@ jobs: issues: write pull-requests: write statuses: write - outputs: - run_audit: ${{ steps.cmd.outputs.type == 'run' && steps.auth.outputs.authorized == 'true' }} - head_sha: ${{ steps.pr.outputs.head_sha }} - base_sha: ${{ steps.pr.outputs.base_sha }} steps: - name: Check commenter authorization @@ -75,7 +71,6 @@ jobs: - name: Parse command id: cmd env: - # Pass through env to avoid script injection from untrusted comment body COMMENT_BODY: ${{ github.event.comment.body }} run: | if echo "$COMMENT_BODY" | grep -qP '^/vault-audit skip .+'; then @@ -111,6 +106,46 @@ jobs: > ⚠️ This skip applies to the current HEAD (\`${SHA:0:8}\`). Pushing new vault file changes will re-require an audit." # ── /vault-audit ───────────────────────────────────────────────────────── + - name: Get GATI token for cre-docs dispatch + id: gati + if: steps.cmd.outputs.type == 'run' + uses: smartcontractkit/chainlink-github-actions/apps/setup-github-token@main + with: + app-id: ${{ secrets.GATI_APP_ID }} + private-key: ${{ secrets.GATI_PRIVATE_KEY }} + # Needs contents:write on cre-docs to trigger repository_dispatch + repositories: cre-docs + + - name: Set commit status pending + if: steps.cmd.outputs.type == 'run' + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + SHA: ${{ steps.pr.outputs.head_sha }} + run: | + gh api "/repos/${REPO}/statuses/${SHA}" \ + --method POST \ + -f state=pending \ + -f context="vault-audit" \ + -f description="Vault audit queued..." + + - name: Dispatch audit to cre-docs + if: steps.cmd.outputs.type == 'run' + env: + GH_TOKEN: ${{ steps.gati.outputs.token }} + PR_NUMBER: ${{ github.event.issue.number }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + BASE_SHA: ${{ steps.pr.outputs.base_sha }} + CHAINLINK_REPO: ${{ github.repository }} + run: | + gh api /repos/smartcontractkit/cre-docs/dispatches \ + --method POST \ + -f event_type=vault-audit \ + -F 'client_payload[pr_number]'"=$PR_NUMBER" \ + -f 'client_payload[head_sha]'"=$HEAD_SHA" \ + -f 'client_payload[base_sha]'"=$BASE_SHA" \ + -f 'client_payload[chainlink_repo]'"=$CHAINLINK_REPO" + - name: Acknowledge audit request if: steps.cmd.outputs.type == 'run' env: @@ -120,18 +155,3 @@ jobs: run: | gh pr comment "$PR_NUMBER" \ --body "🔍 Vault audit triggered by @${COMMENTER} — running now. Results will appear as a new comment when complete." - - # Reusable workflows must be called as a top-level job, not a step - run-audit: - needs: handle-command - if: needs.handle-command.outputs.run_audit == 'true' - uses: smartcontractkit/cre-docs/.github/workflows/vault-audit.yml@main - with: - pr_number: ${{ github.event.issue.number }} - head_sha: ${{ needs.handle-command.outputs.head_sha }} - base_sha: ${{ needs.handle-command.outputs.base_sha }} - chainlink_repo: ${{ github.repository }} - secrets: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - CRE_DOCS_TOKEN: ${{ secrets.CRE_DOCS_TOKEN }} - CHAINLINK_TOKEN: ${{ github.token }}