-
Notifications
You must be signed in to change notification settings - Fork 0
336 lines (286 loc) · 11.4 KB
/
Copy pathsecurity.yml
File metadata and controls
336 lines (286 loc) · 11.4 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
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
name: Security Scanning
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * 1'
workflow_dispatch:
permissions:
contents: read
statuses: write
jobs:
audit:
name: Dependency Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: '22'
cache: 'npm'
- run: npm ci --ignore-scripts
# scripts/audit-with-exceptions.mjs is the single vulnerability allowlist
# (finding M-19). It fails on an undocumented finding, an exception past
# its reviewBy date, a stale exception, and a severity increase since the
# assessment — all of which the removed scripts/production-audit.mjs let
# through.
- name: Production dependency audit — desktop (moderate+, documented exceptions)
run: node scripts/audit-with-exceptions.mjs
- name: Install server dependencies
run: npm ci --ignore-scripts
working-directory: server
# Previously `npm audit --production --audit-level=high || true`, i.e. a
# step that could not fail (finding M-18).
- name: Production dependency audit — server (moderate+, documented exceptions)
run: node scripts/audit-with-exceptions.mjs --scope=server
# Informational only, and labelled as such: the full tree including dev
# dependencies is not what ships.
- name: Full dependency audit including dev (informational)
run: npm audit || true
- name: Check for outdated dependencies (informational)
run: npm outdated || true
secret-scan:
name: Committed Secret Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
# Full history: the scheduled run scans every blob reachable from any
# ref, and a shallow clone would silently reduce that to the tip
# commit while still reporting a successful history scan.
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: '22'
# No `npm ci`: scripts/scan-secrets.mjs has no dependencies beyond Node
# and git, deliberately, so this gate cannot be disabled by a dependency
# resolution problem.
# Run first and on its own: a scanner whose rules have stopped matching
# would otherwise report a clean tree. --self-test scans a synthetic
# sample for every rule and fails if any rule is dead, which is what makes
# the PASS below mean something.
- name: Verify the scanner still detects its own samples
run: node scripts/scan-secrets.mjs --self-test
- name: Scan the working tree for committed secrets
run: node scripts/scan-secrets.mjs --report="${{ runner.temp }}/secret-scan-report.json"
# History is immutable without rewriting every clone, so the known
# historical exposures carry dated records in
# security/secret-scan-allowlist.json. Run weekly rather than per-PR
# because it reads every blob in the repository.
- name: Scan full history for committed secrets
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
run: node scripts/scan-secrets.mjs --history --report="${{ runner.temp }}/secret-scan-history-report.json"
- name: Upload secret scan report
if: always()
uses: actions/upload-artifact@v7
with:
name: secret-scan-report
path: ${{ runner.temp }}/secret-scan*.json
if-no-files-found: ignore
retention-days: 90
snyk:
name: Snyk Vulnerability Scan
runs-on: ubuntu-latest
# Previously skipped on the weekly schedule. That is the run that matters
# most — it catches an advisory published against code that has not changed —
# and with the status job no longer treating a skip as a pass, skipping here
# would report a weekly failure instead.
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: '22'
cache: 'npm'
- run: npm ci --ignore-scripts
- name: Determine whether Snyk is configured
id: snyk-config
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
run: |
if [ -n "$SNYK_TOKEN" ]; then
echo 'configured=true' >> "$GITHUB_OUTPUT"
else
echo 'configured=false' >> "$GITHUB_OUTPUT"
echo '::warning::SNYK_TOKEN is not configured; this job runs the self-contained audit gate instead so its result still reflects a scan that actually ran.'
fi
# No continue-on-error (finding M-18): when Snyk is configured, a high or
# critical finding fails the job.
- name: Run Snyk to check for vulnerabilities
if: steps.snyk-config.outputs.configured == 'true'
uses: snyk/actions/node@v1.0.0
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
# When no token exists the job must still produce a real verdict rather
# than being skipped and then reported as a success. This runs the
# committed gate at the same severity threshold Snyk was configured for,
# over both workspaces, so a green `snyk` status always means "a
# vulnerability scan ran and found nothing at high+".
- name: Fallback scan — install server dependencies
if: steps.snyk-config.outputs.configured != 'true'
run: npm ci --ignore-scripts
working-directory: server
- name: Fallback vulnerability scan (no Snyk token configured)
if: steps.snyk-config.outputs.configured != 'true'
run: |
node scripts/audit-with-exceptions.mjs --severity=high
node scripts/audit-with-exceptions.mjs --scope=server --severity=high
lint:
name: Lint & Static Analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: '22'
cache: 'npm'
- run: npm ci --ignore-scripts
- name: Run ESLint
run: npm run lint
test-security:
name: Security Tests
runs-on: ubuntu-latest
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: '1'
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: '22'
cache: 'npm'
- name: Install system dependencies
run: sudo apt-get update && sudo apt-get install -y python3 make g++
- run: npm ci
- name: Rebuild native modules
run: npm rebuild better-sqlite3-multiple-ciphers
- name: Run cross-org access tests
run: npm run test:security
- name: Run business logic tests
run: npm run test:business
- name: Run compliance validation tests
run: npm run test:compliance
load-test:
name: Performance Load Test
runs-on: ubuntu-latest
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: '1'
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: '22'
cache: 'npm'
- name: Install system dependencies
run: sudo apt-get update && sudo apt-get install -y python3 make g++
- run: npm ci
- name: Rebuild native modules
run: npm rebuild better-sqlite3-multiple-ciphers
- name: Run load tests
run: node scripts/run-test-suites.cjs performance
lockfile-check:
name: Lockfile Integrity
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: '22'
cache: 'npm'
# A missing lockfile means `npm ci` resolves differently on every run,
# which defeats both the audit gate and the SBOM. This used to emit a
# ::warning:: and pass.
- name: Verify lockfiles are committed
run: |
missing=0
for f in package-lock.json server/package-lock.json; do
if [ ! -f "$f" ]; then
echo "::error::$f is not committed — dependency resolution is not reproducible"
missing=1
fi
done
exit $missing
- name: Verify clean install matches lockfile
run: npm ci --ignore-scripts
- name: Verify clean server install matches lockfile
run: npm ci --ignore-scripts
working-directory: server
report-audit-status:
name: Report audit status
runs-on: ubuntu-latest
needs: audit
if: always()
steps:
- name: Set audit commit status
uses: actions/github-script@v9
with:
script: |
const state = '${{ needs.audit.result }}' === 'success' ? 'success' : 'failure';
const sha = context.payload.pull_request
? context.payload.pull_request.head.sha
: context.sha;
await github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha,
state,
context: 'audit',
description: `Dependency audit ${state}`,
target_url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
});
report-snyk-status:
name: Report snyk status
runs-on: ubuntu-latest
needs: snyk
if: always()
steps:
- name: Set snyk commit status
uses: actions/github-script@v9
with:
script: |
// A skipped scan is NOT a passing scan (finding M-18): this used to
// map 'skipped' to 'success', so deleting the job, or any condition
// that stopped it running, silently produced a green scan status.
// The job now runs on every trigger and falls back to the committed
// audit gate when no Snyk token exists, so anything other than
// 'success' is a real failure.
const result = '${{ needs.snyk.result }}';
const state = result === 'success' ? 'success' : 'failure';
const sha = context.payload.pull_request
? context.payload.pull_request.head.sha
: context.sha;
await github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha,
state,
context: 'snyk',
description: `Snyk vulnerability scan ${result}`,
target_url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
});
report-secret-scan-status:
name: Report secret scan status
runs-on: ubuntu-latest
needs: secret-scan
if: always()
steps:
- name: Set secret-scan commit status
uses: actions/github-script@v9
with:
script: |
const result = '${{ needs['secret-scan'].result }}';
const state = result === 'success' ? 'success' : 'failure';
const sha = context.payload.pull_request
? context.payload.pull_request.head.sha
: context.sha;
await github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha,
state,
context: 'secret-scan',
description: `Committed secret scan ${result}`,
target_url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
});