-
-
Notifications
You must be signed in to change notification settings - Fork 0
358 lines (342 loc) · 15.6 KB
/
Copy pathsecurity.yml
File metadata and controls
358 lines (342 loc) · 15.6 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
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
name: Security — Full App Scan
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: "0 2 * * *"
workflow_dispatch:
permissions:
contents: read
security-events: write
actions: read
pull-requests: read
jobs:
# Stage 1: Dependency scanning (npm audit + Snyk)
deps:
name: Stage 1 — Dependencies
runs-on: ubuntu-latest
env:
# JOB-level so the Snyk step's `if: env.SNYK_TOKEN != ''` can actually see it.
# A step-level env is applied AFTER the step's own `if` is evaluated, so the
# gate was always false and Snyk SILENTLY never ran (false "deps scanned").
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
cache: "npm"
- run: npm ci
- name: npm audit (high+)
# Matches pre-push hook policy (.kit.toml). Moderates tracked separately;
# postcss/qs remaining moderates only fixable via next 9.3.3 downgrade (breaking).
run: npm audit --audit-level=high
- name: Snyk scan
if: env.SNYK_TOKEN != ''
uses: snyk/actions/node@9adf32b1121593767fc3c057af55b55db032dc04 # v1.0.0
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high --all-projects
# Stage 1b: Supply-chain (bumblebee — known compromises, not CVEs)
supply-chain:
name: Stage 1b — Supply-chain (bumblebee)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
cache: "npm"
- run: npm ci
- name: Build CLI
run: npm run build
- name: Bumblebee deep scan on repo
env:
KIT_BUMBLEBEE_PROFILE: deep
KIT_BUMBLEBEE_ROOTS: "."
# Fail CLOSED: scanner-unavailable / timeout must fail the gate, not
# warn-and-pass an unscanned tree as clean.
KIT_BUMBLEBEE_REQUIRED: "1"
# Run checkSecurity and filter to supply-chain category. Avoids the full
# `kit ci` pipeline which expects gh auth / 1password / etc.
run: node scripts/run-supply-chain-check.mjs
# Stage 1c: Triage new deps on PRs — handled by separate triage-deps.yml workflow
# Stage 1d: GuardDog nightly deep sweep (#205) — behavioral-malware heuristics
# on the direct deps, uncached and with a generous timeout. Too slow
# (~25s/package) for the local check budget, so the continuous deep scan lives
# here; local `kit check` uses the clean-verdict cache.
guarddog-nightly:
name: Stage 1d — GuardDog malware sweep (nightly)
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
cache: "npm"
- run: npm ci
- name: Build CLI
run: npm run build
- name: Install guarddog + semgrep
run: pip install --user pipx && pipx install guarddog && pipx install semgrep
- name: GuardDog verify (direct deps, no cache, long budget)
env:
KIT_GUARDDOG: "1"
# 45 min — the sweep is the slow path by design; fail closed on timeout.
KIT_GUARDDOG_TIMEOUT_MS: "2700000"
# Fresh cache path so the sweep never reads a stale verdict — it always
# performs the real scan.
KIT_GUARDDOG_CACHE: "/tmp/guarddog-nightly-cache.json"
run: node scripts/run-guarddog-check.mjs
timeout-minutes: 50
# Stage 2: SAST (ESLint security + semgrep + SonarCloud)
sast:
name: Stage 2 — SAST
runs-on: ubuntu-latest
env:
# JOB-level so the SonarCloud step's `if: env.SONAR_TOKEN != ''` can see it
# (a step-level env is evaluated too late — same trap as Snyk).
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
cache: "npm"
- run: npm ci
- name: Semgrep (application code only)
# Excludes set in .semgrepignore — terraform/k8s handled by Stage 5
# Checkov/tfsec so they're skipped here to avoid duplicate findings.
uses: semgrep/semgrep-action@713efdd345f3035192eaa63f56867b88e63e4e5d # v1
with:
config: >-
p/security-audit
p/owasp-top-ten
p/typescript
p/nodejs
- name: SonarCloud
if: env.SONAR_TOKEN != ''
uses: SonarSource/sonarcloud-github-action@ffc3010689be73b8e5ae0c57ce35968afd7909e8 # v5.0.0
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Note SonarCloud skip (visible, not a silent green)
if: env.SONAR_TOKEN == ''
run: echo "::warning title=SonarCloud skipped::SONAR_TOKEN not set — SonarCloud did NOT run; SAST this stage is semgrep-only"
# Stage 3: DAST — N/A for this repo.
# kit is a CLI + MCP (stdio) tool — no HTTP surface to scan.
# ZAP baseline can't run without a reachable target. The marketplace
# frontend has its own DAST in its deploy pipeline. If a future HTTP
# endpoint lands here, restore the ZAP job at this anchor.
dast:
name: Stage 3 — DAST (skipped — no HTTP surface)
if: false
runs-on: ubuntu-latest
steps:
- run: echo "skipped"
# Stage 4: Container scanning (Trivy)
container:
name: Stage 4 — Container (Trivy)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Build image
run: docker build -t kit:${{ github.sha }} .
- name: Trivy scan (HIGH+CRITICAL — gating)
# scanners: vuln — only CVE scan. Secret scanning handled by gitleaks.
# Image is clean of HIGH/CRITICAL after runtime apk upgrades + npm removal.
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: kit:${{ github.sha }}
format: sarif
output: trivy-results.sarif
severity: "CRITICAL,HIGH"
exit-code: "1"
ignore-unfixed: true
scanners: vuln
limit-severities-for-sarif: true
- name: Upload SARIF
# SARIF upload requires Code Scanning enabled in repo settings.
# Don't fail the job if Code Scanning is off — scan result is the gate.
if: always()
continue-on-error: true # kit-self-audit: allow-continue-on-error
uses: github/codeql-action/upload-sarif@c35d1b164463ee62a100735382aaaa525c5d3496 # codeql-bundle-v2.25.6
with:
sarif_file: trivy-results.sarif
# Stage 5: Infrastructure (Checkov + tfsec + kubesec)
infra:
name: Stage 5 — Infrastructure
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Checkov (terraform + kubernetes)
# Baseline of pre-existing findings in .checkov.yaml skip-check.
# Gating on net-new findings only — see .checkov.yaml header for context.
uses: bridgecrewio/checkov-action@59b9d7edfcad5b87fbe3f473a9a134a721ad03f8 # v12.3119.0
with:
directory: .
config_file: .checkov.yaml
output_format: sarif
output_file_path: checkov.sarif
soft_fail: false
- name: tfsec
if: hashFiles('terraform/**/*.tf') != ''
continue-on-error: true # kit-self-audit: allow-continue-on-error
uses: aquasecurity/tfsec-action@b466648d6e39e7c75324f25d83891162a721f2d6 # v1.0.3
with:
working_directory: terraform
soft_fail: true
- name: Upload Checkov SARIF
# SARIF upload requires Code Scanning enabled in repo settings.
if: always()
continue-on-error: true # kit-self-audit: allow-continue-on-error
uses: github/codeql-action/upload-sarif@c35d1b164463ee62a100735382aaaa525c5d3496 # codeql-bundle-v2.25.6
with:
sarif_file: checkov.sarif
# Secret scanning (gitleaks)
secrets:
name: Secret scan (gitleaks)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@e0c47f4f8be36e29cdc102c57e68cb5cbf0e8d1e # v3.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
# GDPR + headers + cert
# Scopes: only enforce for user-data-handling sub-packages.
# kit CLI is a dev tool, no user data — runs informational only.
compliance:
name: GDPR + security headers
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: GDPR compliance check (user-data packages only)
run: |
set -e
USER_DATA_DIRS=""
if [ -z "$USER_DATA_DIRS" ]; then
echo "No user-data-handling packages detected — GDPR check N/A for dev CLI"
exit 0
fi
if [ ! -f PRIVACY.md ]; then
echo "::warning::PRIVACY.md missing — required when shipping user-data packages"
fi
for dir in $USER_DATA_DIRS; do
grep -rq "right_to_be_forgotten\|deleteUserData" "$dir" || echo "::warning::$dir: data deletion fn not found"
grep -rq "consent\|recordConsent" "$dir" || echo "::warning::$dir: consent mgmt not found"
grep -rq "exportUserData\|dataExport" "$dir" || echo "::warning::$dir: data export not found"
done
echo "GDPR scan complete (warnings only)"
- name: Security headers in code
# This step could not fail. It read `if grep -rE "…" src/ packages/ | head -1; then`,
# and `head` exits 0 on empty input, so the condition was ALWAYS true: "Security
# headers configured" printed unconditionally and the ::warning:: branch was
# unreachable. Same class as the publish.yml tag-signature gate fixed in 6.6.4 (a
# condition decided by the pipe's last command); self-audit R1-fail-open-ci now flags
# it, and found this one.
#
# Made honest rather than merely correct: kit is a CLI + MCP (stdio) tool, the same
# "no HTTP surface" that skips Stage 3 DAST above, so a missing CSP is not a finding
# here — a permanently-expected warning would just train people to ignore warnings.
# The check is therefore gated on a surface actually existing, and fires the day one
# lands. Measured when written: every one of these server patterns matches 0 lines in
# src/ and packages/.
run: |
set -o pipefail
if ! grep -rqE "createServer|express\(|fastify\(|Bun\.serve|\.listen\(" src/ packages/ 2>/dev/null; then
echo "No HTTP surface in src/ or packages/ — header check N/A (same reason Stage 3 DAST is skipped)"
exit 0
fi
if grep -rqE "Content-Security-Policy|Strict-Transport-Security|X-Frame-Options" src/ packages/; then
echo "Security headers configured"
else
echo "::warning::HTTP surface found in src/ or packages/ but no security headers — required for web-facing code"
fi
# kit self-check (uses kit check-security)
self-check:
name: kit check-security
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
cache: "npm"
- run: npm ci && npm run build
- name: Run checkSecurity directly
# Avoid `kit check` which also runs services/secrets/skills checks
# that need gh auth, 1password, etc. — unavailable in CI.
run: node scripts/run-security-check.mjs
# kit self-audit (uses kit self-audit — checks kit's own source for the
# audit's bug-classes + asserts CI-referenced scripts exist).
self-audit:
name: kit self-audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
cache: "npm"
- run: npm ci && npm run build
- name: Run kit self-audit
# No --fail-on-warning. GATES this job (fail -> blocks the gate via needs):
# R11 CI references a missing/renamed script
# R3 sensitive file written world-readable (mode not owner-only)
# R6 unguarded dynamic import of a variable
# R7 unescaped variable in CI/XML/summary output
# R9 raw write to the chained audit log (chain break)
# R1 unannotated `|| true` in a workflow (fail-open)
# R14 docs claim a command / flag / [section] kit does not have
# WARNS only (does not gate without --fail-on-warning):
# R1 continue-on-error: true; R1b/R2/R4/R8 heuristics
# ADVISORIES (info, never gate, reported aggregated): R5, R10.
run: node dist/cli.js self-audit --format=github
# kit dogfood — provision the scanners declared in .kit.toml [tools] via
# `kit install` (mise), then run kit's own security scan with them present.
# Proves kit's provisioning + scan path works end-to-end. INFORMATIONAL ONLY:
# not in the `gate` needs, and each step is continue-on-error, so a finding
# never blocks — the gating trivy/gitleaks live in the dedicated stages above.
dogfood-scan:
name: kit dogfood (provision + scan)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
cache: "npm"
- run: npm ci && npm run build
- uses: jdx/mise-action@3c2e0cf82a5b2e5249f0d3635a4d83d0ae861518 # v4.2.5
- name: Provision declared tools via kit install (triage-gated)
run: node dist/cli.js install
continue-on-error: true # kit-self-audit: allow-continue-on-error
- name: Run kit's security scan with trivy + trufflehog present
run: node scripts/run-security-check.mjs
continue-on-error: true # kit-self-audit: allow-continue-on-error
# Aggregated gate — all stages must pass
gate:
name: Security gate
needs: [deps, supply-chain, sast, container, infra, secrets, compliance, self-check, self-audit]
runs-on: ubuntu-latest
if: always()
steps:
- name: Verify all jobs passed
run: |
# Fail CLOSED: a required scan that was cancelled or skipped is NOT a pass.
# Checking only 'failure' let a cancelled/skipped stage read as green — a
# single green "Security gate" that hid an un-run scan.
if [ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped') }}" = "true" ]; then
echo "Security gate FAILED — a required scan did not succeed (failed / cancelled / skipped)"
exit 1
fi
echo "Security gate PASSED — all required scans succeeded"