Wire security scans into the CI pipeline
CI / test (push) Successful in 25s
CI / lint (push) Failing after 22s
CI / secrets-scan (push) Failing after 13s
CI / vuln-scan (push) Failing after 9s
CI / sast (push) Successful in 20s

Replace the test-only workflow with a parallel five-job pipeline:
tests, lint+format, gitleaks, Trivy (fs scan + CycloneDX SBOM), and
Semgrep SAST. Security scans are report-only initially so the team
can baseline findings before flipping the gates to blocking. Adds
.gitleaks.toml allowlists for the known dev/test placeholders so the
secret scan starts at zero noise. Future build-image / image-scan /
push-to-harbor stages are sketched in comments at the bottom of
ci.yml.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-04 15:58:56 -07:00
parent 1fafad4e69
commit a314908c7b
3 changed files with 197 additions and 39 deletions
+174
View File
@@ -0,0 +1,174 @@
name: CI
on:
push:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Generate Prisma client
run: pnpm --filter tehriehlbudget-backend exec prisma generate
- name: Backend tests (Jest)
run: pnpm test:backend
- name: Frontend tests (Vitest)
# Supabase's createClient validates the URL at module load, so the
# frontend tests need *some* value to import lib/supabase without
# throwing. Real auth calls are stubbed in the tests, so these are
# purely structural placeholders — never used over the wire.
env:
VITE_SUPABASE_URL: http://localhost:54321
VITE_SUPABASE_ANON_KEY: placeholder-anon-key-for-tests-only
run: pnpm test:frontend
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Generate Prisma client
# Backend ESLint imports types from the generated Prisma client.
run: pnpm --filter tehriehlbudget-backend exec prisma generate
- name: Lint
run: pnpm lint
- name: Format check
run: pnpm format:check
secrets-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install gitleaks
run: |
curl -sSL https://github.com/gitleaks/gitleaks/releases/download/v8.21.2/gitleaks_8.21.2_linux_x64.tar.gz \
-o /tmp/gitleaks.tar.gz
tar -xzf /tmp/gitleaks.tar.gz -C /tmp gitleaks
sudo mv /tmp/gitleaks /usr/local/bin/gitleaks
gitleaks version
- name: Run gitleaks
# TODO: flip to --exit-code 1 once the baseline is clean
run: |
gitleaks detect \
--source . \
--config .gitleaks.toml \
--report-format sarif \
--report-path gitleaks.sarif \
--redact \
--no-git \
--exit-code 0
- uses: actions/upload-artifact@v4
if: always()
with:
name: gitleaks-report
path: gitleaks.sarif
vuln-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Trivy filesystem scan (vuln + misconfig)
# TODO: flip exit-code to '1' once baseline is clean
uses: aquasecurity/trivy-action@0.28.0
with:
scan-type: fs
scan-ref: .
format: sarif
output: trivy-fs.sarif
severity: HIGH,CRITICAL
exit-code: '0'
scanners: vuln,misconfig
- name: Trivy SBOM (CycloneDX)
uses: aquasecurity/trivy-action@0.28.0
with:
scan-type: fs
scan-ref: .
format: cyclonedx
output: sbom.cdx.json
- uses: actions/upload-artifact@v4
if: always()
with:
name: trivy-reports
path: |
trivy-fs.sarif
sbom.cdx.json
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Semgrep
# TODO: drop the trailing "|| true" once the baseline is clean
run: |
docker run --rm -v "$PWD:/src" returntocorp/semgrep:latest \
semgrep scan \
--config p/typescript \
--config p/react \
--config p/nodejsscan \
--config p/owasp-top-ten \
--sarif --output /src/semgrep.sarif \
--error /src || true
- uses: actions/upload-artifact@v4
if: always()
with:
name: semgrep-report
path: semgrep.sarif
# ─────────────────────────────────────────────────────────────────────────────
# Future stages (not yet enabled — Dockerfiles do not exist yet):
#
# build-images:
# needs: [test, lint, secrets-scan, vuln-scan, sast]
# - docker build tehriehlbudget-backend → harbor.<host>/tehriehlbudget/backend:<sha>
# - docker build tehriehlbudget-frontend → harbor.<host>/tehriehlbudget/frontend:<sha>
#
# image-scan:
# needs: build-images
# - trivy image scan on each built image (severity gate ON here from day one)
# - re-run trivy SBOM on the image so Harbor gets an image-level CycloneDX
#
# push:
# needs: image-scan
# - docker login harbor.<self-hosted-domain> (creds via Gitea Actions secrets)
# - docker push backend + frontend tags
# ─────────────────────────────────────────────────────────────────────────────
-39
View File
@@ -1,39 +0,0 @@
name: Tests
on:
push:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Generate Prisma client
run: pnpm --filter tehriehlbudget-backend exec prisma generate
- name: Backend tests (Jest)
run: pnpm test:backend
- name: Frontend tests (Vitest)
# Supabase's createClient validates the URL at module load, so the
# frontend tests need *some* value to import lib/supabase without
# throwing. Real auth calls are stubbed in the tests, so these are
# purely structural placeholders — never used over the wire.
env:
VITE_SUPABASE_URL: http://localhost:54321
VITE_SUPABASE_ANON_KEY: placeholder-anon-key-for-tests-only
run: pnpm test:frontend
+23
View File
@@ -0,0 +1,23 @@
# Gitleaks configuration
# Extends the built-in default ruleset and adds allowlists for known
# non-secret values that would otherwise trip the scan.
[extend]
useDefault = true
[allowlist]
description = "Known dev/test placeholders and generated artifacts"
regexes = [
# Frontend test env value, set in .gitea/workflows/ci.yml
'''placeholder-anon-key-for-tests-only''',
# Local-only Postgres password in docker-compose.yml (dev container)
'''development_password''',
]
paths = [
'''pnpm-lock\.yaml''',
'''.*/coverage/.*''',
'''.*/dist/.*''',
'''.*/node_modules/.*''',
]