-
Notifications
You must be signed in to change notification settings - Fork 107
205 lines (194 loc) · 10.3 KB
/
Copy pathsecurity-baseline.yml
File metadata and controls
205 lines (194 loc) · 10.3 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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
name: Security baseline
# Canonical security baseline (TruffleHog secret scan + CodeQL static analysis)
# rolled out to Tier-1 repos per QVAC-21550. This is a thin caller that
# delegates to the org's reusable workflow (authored under QVAC-19055); its
# inputs and behavior are documented in
# tetherto/qvac-actions/docs/security-baseline.md.
#
# Uses plain `pull_request` (never `pull_request_target`): fork PRs run with a
# read-only token and no secrets, and this baseline needs none of the
# privileged context (`codeql-upload: never` below), so the "pwn request"
# surface of `pull_request_target` buys nothing here.
on:
push:
branches:
- main
pull_request:
schedule:
# Weekly full scan (QVAC-19056 rollout). Mondays 07:00 UTC; crons are
# staggered across weekdays per repo to spread scheduled-run load.
- cron: "0 7 * * 1"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
baseline:
# Pinned to the 0.1.0 tag's immutable commit SHA per the org freeze-and-pin
# convention for reusable workflows (never `@main`).
uses: tetherto/qvac-actions/.github/workflows/public-reusable-security.yml@bbb0740e2a16b94371c7439e0e06945c5b68e759 # 0.3.0
permissions:
contents: read
security-events: write
pull-requests: write
actions: read
with:
# Explicit language list: CodeQL autodetect frequently misses C/C++ in
# mixed repos, so we name every scanned surface (JS/TS + Python + C/C++).
languages: c-cpp,javascript-typescript,python
severity-threshold: high
# qvac uses GitHub "Default setup" for code scanning, which rejects SARIF
# from advanced workflows. Skip the upload here; the severity gate and
# TruffleHog still run (SARIF is written locally for the gate to read).
codeql-upload: never
# Buildless C/C++ extraction (no compilation / submodule checkout), so the
# scan doesn't need qvac's clang/vcpkg/bare-make toolchain. Uniform with
# the Tier-1 C/C++ callers; requires qvac-actions >= 0.2.0.
codeql-build-mode: none
# Repo-local CI trust-policy regression tests (fork CI hardening). Kept here
# instead of a dedicated workflow so the coverage rides along with the security
# baseline that already runs on every PR.
# Runs UNPRIVILEGED only (plain pull_request, contents: read, no secrets) —
# never move this under pull_request_target. NOTE: this job is repo-local and
# NOT part of the org reusable baseline above; preserve it across any org
# sync of this caller file.
policy-tests:
name: policy-tests
runs-on: ubuntu-latest
# The job's own work is ~10s; the budget exists almost entirely to absorb
# checkout latency, which is not ours to control — the same --depth=1 fetch
# of this repo has been observed at both 7s and 97s. A cap this close to
# external variance buys nothing and turns a slow morning into a red main.
timeout-minutes: 10
permissions:
contents: read
# Reads the changed-file list from the pull request files API.
pull-requests: read
steps:
- name: Checkout PR code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2
with:
# Default shallow fetch: the changed-file list comes from the PR files
# API below, so this job needs no history beyond the checked-out tree.
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # 6.3.0
with:
node-version: 20
- name: Run trust-policy tests
run: >-
node --test
.github/scripts/test/ci-trust-policy.test.mjs
.github/scripts/test/prebuild-status.test.mjs
.github/scripts/test/publish-gate-policy.test.mjs
# The co-load inventory drifts when the SDK swaps an addon, not when the
# co-load package changes, so this has to run on every PR rather than
# behind the co-load label. It only reads files (no npm install), and a
# stale inventory makes the co-load gate report on an addon the SDK no
# longer ships.
- name: Run co-load addon inventory tests
run: >-
node --test
packages/ggml-coload-smoke/test/addons.unit.test.js
# The trust-policy suite asserts on workflow *text*, so it stays green even
# when an edit destroys a job graph. on-merge-*/on-publish-* never run on a
# PR, and pull_request_target workflows load from the base branch, so this
# is the only place a PR parses its own edited workflows. Only files this PR
# changes are linted — no repo-wide baseline file.
- name: Collect changed workflow files
id: changed-workflows
if: github.event_name == 'pull_request'
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
# The PR files API is the exact changed set, so this needs no local
# history and the checkout stays shallow. The previous `git diff
# base...head` required fetch-depth: 0, whose blob backfill stalled the
# checkout for ~2 minutes of this job's 5-minute budget on push runs.
# It also read a `base.sha` that GitHub leaves stale, which inflated the
# scope with files the PR never touched.
#
# `status != removed` drops deletions: actionlint aborts its entire run
# (reporting nothing at all) if any path on its command line is missing,
# so a PR that deletes a workflow would otherwise lint nothing and pass.
gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" --paginate \
--jq '.[] | select(.status != "removed") | .filename' \
> "$RUNNER_TEMP/changed-all.txt"
# The endpoint hard-caps at 3000 files. Truncation would silently shrink
# the lint scope, so refuse to report a pass we cannot stand behind.
if [ "$(wc -l < "$RUNNER_TEMP/changed-all.txt" | tr -d ' ')" -ge 3000 ]; then
echo "::error title=PR too large to scope automatically::The pull request files API truncates at 3000 files; run actionlint over the changed workflows manually."
exit 1
fi
# grep, not rg: ripgrep is not installed on GitHub-hosted runners, and
# a blanket `|| true` turned that `command not found` into count=0 —
# the gate reported success while linting nothing. grep exits 1 for
# "no matches" (fine) and >1 for a real failure (must not be silent).
set +e
grep -E '^\.github/(workflows|actions)/.*\.ya?ml$' \
"$RUNNER_TEMP/changed-all.txt" > "$RUNNER_TEMP/changed-workflows.txt"
filter_status=$?
set -e
if [ "$filter_status" -gt 1 ]; then
echo "::error title=Could not determine changed workflow files::grep exited ${filter_status}"
exit 1
fi
count=$(wc -l < "$RUNNER_TEMP/changed-workflows.txt" | tr -d ' ')
echo "count=${count}" >> "$GITHUB_OUTPUT"
echo "actionlint scope: ${count} changed workflow/action file(s)"
# `.cursor/rules/no-remote-code-execution.mdc` forbids fetching a remote
# artefact and executing it. actionlint ships no first-party action, so the
# release tarball is pinned to a digest here and verified before extraction —
# a retagged or tampered release fails the checksum and never runs.
# To bump: update both ACTIONLINT_VERSION and ACTIONLINT_SHA256 from
# https://github.com/rhysd/actionlint/releases (actionlint_<ver>_checksums.txt).
- name: Install actionlint
if: github.event_name == 'pull_request' && steps.changed-workflows.outputs.count != '0'
env:
ACTIONLINT_VERSION: 1.7.12
ACTIONLINT_SHA256: 8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8
run: |
set -euo pipefail
curl -sSfL -o "$RUNNER_TEMP/actionlint.tgz" \
"https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"
echo "${ACTIONLINT_SHA256} ${RUNNER_TEMP}/actionlint.tgz" | sha256sum --check --strict
tar -xzf "$RUNNER_TEMP/actionlint.tgz" -C "$RUNNER_TEMP" actionlint
- name: Lint changed workflows with actionlint
if: github.event_name == 'pull_request' && steps.changed-workflows.outputs.count != '0'
run: |
set -euo pipefail
mapfile -t files < "$RUNNER_TEMP/changed-workflows.txt"
# actionlint exits 1 when it reports findings; lint-workflows.mjs decides pass/fail.
"$RUNNER_TEMP/actionlint" -oneline -shellcheck= -pyflakes= "${files[@]}" \
> "$RUNNER_TEMP/actionlint.txt" || true
node .github/scripts/lint-workflows.mjs --input "$RUNNER_TEMP/actionlint.txt"
# The whole fork trust model collapses to fail-open if the fork-ci
# environment loses its required reviewers or is deleted (GitHub silently
# recreates it unprotected on next use). Checked on push/schedule/dispatch
# rather than per-PR: fork PR tokens cannot read environment settings, so
# gating PRs on this would fail for exactly the PRs it is meant to protect.
# Exit 1 means the protection actually drifted (hard fail). Any other
# non-zero means the check could not run — GITHUB_TOKEN has no dedicated
# `environments` permission scope, so a read denial must not be reported
# as drift.
- name: Verify fork-ci environment protection
if: github.event_name != 'pull_request'
env:
GH_TOKEN: ${{ github.token }}
run: |
set +e
node .github/scripts/verify-fork-ci-environment.mjs
status=$?
set -e
if [ "$status" -eq 1 ]; then
echo "::error title=fork-ci environment protection drifted::External fork PRs may run without human approval."
exit 1
fi
if [ "$status" -ne 0 ]; then
echo "::warning title=Could not verify fork-ci environment::Exit ${status}; GITHUB_TOKEN may lack environment read access. Run .github/scripts/verify-fork-ci-environment.mjs locally with an admin token."
fi