-
Notifications
You must be signed in to change notification settings - Fork 0
174 lines (152 loc) · 7.52 KB
/
Copy pathsecurity-audit.yaml
File metadata and controls
174 lines (152 loc) · 7.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
name: security-audit
# Audits this repo against SECURITY.md. Runs nightly via the schedule
# trigger and on-demand via workflow_dispatch. release.yml dispatches it
# on the release tag and gates publishing on the result — it dispatches
# rather than calling this workflow with `uses:` because a tag `push`
# would propagate as event_name `push`, which claude-code-action rejects.
on:
schedule:
- cron: "21 4 * * *"
workflow_dispatch:
permissions:
contents: read
actions: read
issues: write
id-token: write
jobs:
audit:
runs-on: ubuntu-latest
timeout-minutes: 20
# The AUDIT_PAT (read-only Administration + Secrets + Environments)
# lives in this environment, whose deployment-branch-policy admits
# only `main` and `v*` tags — refs that the §3 rulesets reserve to
# admins. A bot-pushed feature branch cannot reach this job at all
# (GitHub rejects the run before any step starts), so the PAT
# cannot be exfiltrated through a hand-authored workflow on a
# non-admin-gated ref.
environment:
name: security-audit
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 1
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 11.0.6
- name: Install workspace dependencies
run: pnpm install --frozen-lockfile
- name: Verify AUDIT_PAT is provisioned
env:
AUDIT_PAT: ${{ secrets.AUDIT_PAT }}
run: |
[ -n "$AUDIT_PAT" ] && exit 0
echo "FAIL" > audit-status.txt
echo "**FAIL** — \`AUDIT_PAT\` is not present in the \`security-audit\` environment. See [SECURITY.md > CI Validation Contract](https://github.com/$GITHUB_REPOSITORY/blob/main/SECURITY.md#ci-validation-contract) for provisioning." > audit-report.md
echo "::error::AUDIT_PAT secret is not set."
exit 1
- name: Audit against SECURITY.md
uses: anthropics/claude-code-action@4d7e1f0cd85743fdc93b1c8040ab54395da024e2 # v1
env:
# `claude-code-action` resets `GH_TOKEN` to its own internal
# workflow token, so setting `GH_TOKEN` at this scope is
# silently overridden. Expose AUDIT_PAT under its own name
# and have the prompt instruct Claude to prefix `gh api`
# calls with `GH_TOKEN=$AUDIT_PAT`.
AUDIT_PAT: ${{ secrets.AUDIT_PAT }}
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# release.yml dispatches this workflow with the default
# GITHUB_TOKEN, so the actor is `github-actions[bot]`. The
# action refuses bot-initiated runs unless the bot is allow-
# listed. This is safe here: the prompt below is hardcoded (no
# comment/PR/issue injection vector) and the job only runs on
# admin-gated refs behind the `security-audit` environment. The
# nightly `schedule` run has a human actor and is unaffected.
allowed_bots: "github-actions"
# Without an explicit allowlist the action defaults to a
# restrictive set that excludes Bash and Write, so an
# auditing prompt that wants to run `gh api` and produce a
# report file racks up permission denials and exits without
# writing audit-status.txt.
claude_args: '--allowed-tools "Read,Write,Edit,Bash,Grep,Glob"'
prompt: |
Audit this repository against SECURITY.md. The `FAIL IF`
lines are concrete checks; the spec also says the list is
not exhaustive, so flag any other security hole you find.
For each `FAIL IF`, run the mechanical check (`gh api`,
grep, file presence, or a script) and record PASS or FAIL
with concrete evidence — file path and line number, API
response excerpt, or command output. Then do a qualitative
pass over `.github/workflows/`, `.config/tend.yaml`,
`.github/renovate.json`, `scripts/`, and any code that
touches secrets.
The default `$GH_TOKEN` in this step is a workflow
`GITHUB_TOKEN` and does NOT have admin scope. For checks
that need admin access (rulesets bypass actors, repo or
environment secret listings, environment policy details),
prefix `gh api` with `GH_TOKEN=$AUDIT_PAT`. Example:
GH_TOKEN=$AUDIT_PAT gh api repos/$GITHUB_REPOSITORY/rulesets/16757376
`$AUDIT_PAT` is a fine-grained, read-only PAT covering
Administration + Secrets + Environments, guaranteed
present by the previous step. If a prefixed call still
returns 403, record FAIL with the note "PAT scope drifted
from SECURITY.md". Reserve `UNVERIFIABLE` only for
genuinely indeterminable cases (e.g. a transient network
error) and explain why.
Write `audit-report.md` with three sections:
- `## FAIL IF results` — one line per check with PASS/FAIL
and evidence
- `## Qualitative findings` — severity BLOCKER / WARNING / INFO
- `## Summary` — overall PASS or FAIL with a one-paragraph
rationale
Write `PASS` or `FAIL` (no other text) to `audit-status.txt`.
Status is FAIL if any `FAIL IF` is violated or any
qualitative finding is BLOCKER. Do not call `exit` — the
workflow inspects the status file.
- name: Surface result, file or close issue
if: always()
env:
GH_TOKEN: ${{ github.token }}
run: |
set -eo pipefail
STATUS=$(tr -d '[:space:]' < audit-status.txt 2>/dev/null || echo "FAIL")
DATE=$(date -u +%Y-%m-%dT%H:%MZ)
RUN_URL="https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
# Idempotent label creation; ignore "already exists" errors.
gh label create security-audit-failure \
--color B60205 --description "Security audit failure" 2>/dev/null || true
if [ "$STATUS" = "PASS" ]; then
# Auto-close any open audit-failure issues so the issue
# tracker reflects the live state.
for n in $(gh issue list --label security-audit-failure \
--state open --json number --jq '.[].number'); do
gh issue close "$n" --comment "Audit passed at $DATE. [Run]($RUN_URL)"
done
echo "Audit passed."
exit 0
fi
if [ ! -s audit-report.md ]; then
printf '%s\n' \
"Audit step produced no \`audit-report.md\`. See workflow run logs." \
> audit-report.md
fi
{
echo "Audit failed at $DATE. [Run]($RUN_URL)"
echo
cat audit-report.md
} > audit-comment.md
EXISTING=$(gh issue list --label security-audit-failure \
--state open --json number --jq '.[0].number' || true)
if [ -n "$EXISTING" ]; then
gh issue comment "$EXISTING" --body-file audit-comment.md
echo "Appended re-audit failure to issue #$EXISTING"
else
gh issue create \
--title "[security-audit] FAIL on $(date -u +%Y-%m-%d)" \
--label security-audit-failure \
--body-file audit-comment.md
fi
exit 1