Repository navigation
240 lines (221 loc) · 9.97 KB
/
Copy pathsecurity.yml
File metadata and controls
240 lines (221 loc) · 9.97 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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# Security scanning — runs on PRs, on push to main, and weekly. The secret scan
# runs on every PR; CodeQL, bandit and the dependency audits are skipped for PRs
# that only touch docs/, mobile/ or Markdown (see the `changes` job).
#
# Complements the CodeRabbit + Greptile app reviews (which fire on PR creation)
# with deterministic checks:
# • gitleaks — secret scanning. HARD FAIL: a leaked credential fails CI
# (not a required check; treat a red run as a merge blocker).
# • CodeQL — Python + JS/TS SAST. Results land in the Security tab.
# • bandit — Python SAST (SARIF → Security tab). Reporting, non-gating.
# • pip-audit — Python dependency advisories. Reporting, non-gating.
# • bun audit — Electron dependency advisories. Reporting, non-gating.
#
# Only the secret scan fails the run. Dependency advisories and bandit findings
# are surfaced as signal (Security tab / job log) rather than blocking every PR
# on a transitive upstream advisory — consistent with the "no ceremony,
# continuous-to-main" cadence.
name: Security
on:
pull_request:
branches: [main]
push:
branches: [main]
schedule:
# Mondays 06:00 UTC — catch advisories disclosed since the last PR.
- cron: "0 6 * * 1"
workflow_dispatch:
env:
# Match ci.yml: run JS actions on Node 24 (GH removes Node 20 in Sep 2026).
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
# Least privilege by default; jobs that upload SARIF opt into security-events.
permissions:
contents: read
# PR branches: a new push cancels the superseded scan (no wasted runners).
# main: every commit keeps its own group, so nothing is cancelled — a merge
# train used to leave a permanent red ✗ ("cancelled") on every intermediate
# commit in the history view even though nothing failed.
concurrency:
group: security-${{ github.ref }}-${{ github.ref == 'refs/heads/main' && github.sha || 'branch' }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
# A pull request whose every changed file is documentation or mobile-only source
# cannot change what CodeQL (Python, JS/TS), bandit or the dependency audits
# analyse, so those four jobs are skipped for it. The secret scan always runs,
# and every push to main, the weekly schedule and manual runs scan everything.
# Any doubt (API failure, deleted/renamed file, 3000+ files) means "true".
changes:
name: Changed paths
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: read
pull-requests: read
outputs:
code: ${{ steps.detect.outputs.code }}
steps:
- id: detect
shell: bash
env:
GH_TOKEN: ${{ github.token }}
EVENT: ${{ github.event_name }}
PR: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
code=true
if [ "$EVENT" = pull_request ]; then
if rows=$(gh api "repos/$REPO/pulls/$PR/files" --paginate --jq '.[] | [.status, .filename] | @tsv') && [ -n "$rows" ]; then
total=$(printf '%s\n' "$rows" | wc -l)
status_rc=0
printf '%s\n' "$rows" | cut -f1 | sort -u | grep -v -x -E 'added|modified' > /dev/null || status_rc=$?
if [ "$total" -lt 3000 ] && [ "$status_rc" -eq 1 ]; then
rc=0
outside=$(printf '%s\n' "$rows" | cut -f2 | grep -v -P '^(docs/|mobile/|\.github/ISSUE_TEMPLATE/|\.github/[^/]+\.md$|[^/]+\.md$)') || rc=$?
[ "$rc" -eq 1 ] && code=false
fi
fi
fi
echo "code=$code" >> "$GITHUB_OUTPUT"
echo "code jobs: $code"
# ── Secret scanning (gating) ─────────────────────────────────────────────
# Full-history scan on push to main; PR-diff scan on pull_request (faster,
# and the action picks the right mode from the event automatically).
secrets:
name: Secret scan (gitleaks)
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
# gitleaks needs full history to scan all commits on push events.
fetch-depth: 0
# No authed git needed after clone; don't persist GITHUB_TOKEN.
persist-credentials: false
- name: gitleaks
uses: gitleaks/gitleaks-action@e0c47f4f8be36e29cdc102c57e68cb5cbf0e8d1e # v3.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# GITLEAKS_LICENSE is only required for GitHub *organizations*; this is
# a personal public repo, so the action runs free without it.
# ── CodeQL SAST (Python + JS/TS) ─────────────────────────────────────────
codeql:
name: CodeQL (${{ matrix.language }})
needs: changes
if: ${{ !cancelled() && needs.changes.outputs.code != 'false' }}
runs-on: ubuntu-22.04
permissions:
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [python, javascript-typescript]
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
with:
languages: ${{ matrix.language }}
# Both targets are interpreted — no compiled build step needed.
build-mode: none
# Scope analysis to shipped product code. The excluded trees never
# ship in the installer's runtime path and produce the bulk of the
# note-level + false-positive findings (file-not-closed in eval
# harnesses, unused alembic migration globals, bind-all in tests,
# path sinks in the legacy Gradio research UI). Queries live in the
# inline config so there's a single source of truth next to
# paths-ignore. paths-ignore is supported here because build-mode is
# `none` (interpreted analysis).
config: |
queries:
- uses: security-and-quality
paths-ignore:
- omnivoice/eval
- research
- tests
- backend/migrations
- "**/*.test.js"
- "**/*.test.jsx"
- "**/*.test.ts"
- "**/*.test.tsx"
- name: Analyze
uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
with:
category: "/language:${{ matrix.language }}"
# ── Python SAST (bandit → SARIF) ─────────────────────────────────────────
bandit:
name: Python SAST (bandit)
needs: changes
if: ${{ !cancelled() && needs.changes.outputs.code != 'false' }}
runs-on: ubuntu-22.04
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- name: Setup Python 3.11
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.11"
# -ll: report MEDIUM+ severity only. -ii: MEDIUM+ confidence only.
# Keeps the SARIF focused on findings worth a human look. The scan step
# is allowed to "fail" (findings present) without failing the job; the
# SARIF upload still runs so results reach the Security tab.
#
# NOTE: the `sarif` output format lives in the `bandit[sarif]` extra
# (pulls in sarif-om + jschema-to-python). Plain `bandit` rejects
# `-f sarif`, so install via the extra spec.
- name: Run bandit
continue-on-error: true
run: |
pipx run --spec 'bandit[sarif]' bandit -r backend/ -ll -ii -f sarif -o bandit.sarif
# continue-on-error: this job is reporting-only. If bandit can't write a
# SARIF for any reason (no findings dir, pipx hiccup), don't fail the job.
- name: Upload bandit SARIF
uses: github/codeql-action/upload-sarif@1190a975f95ce23525efb6a3fc21ea29567c1b52 # v3.38.2
if: always()
continue-on-error: true
with:
sarif_file: bandit.sarif
category: bandit
# ── Dependency advisories (reporting) ────────────────────────────────────
dependencies:
name: Dependency audit
needs: changes
if: ${{ !cancelled() && needs.changes.outputs.code != 'false' }}
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3.2.4
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Setup Python 3.11
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.11"
# Audit the resolved Python environment. Non-gating: a transitive
# advisory with no fix available should not wall off every PR.
- name: pip-audit (Python)
continue-on-error: true
run: |
bash scripts/uv-sync-retry.sh
uv run --with pip-audit pip-audit
# Same bun as package.json packageManager (tests/test_bun_toolchain_pins.py).
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: '1.4.2'
# `bun audit` reports advisories against the root workspace lockfile
# (bun.lock, which covers Electron). Non-gating for the same reason.
- name: bun audit (Electron)
continue-on-error: true
run: |
bun install --frozen-lockfile
bun audit