Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -227,12 +227,21 @@ jobs:
- run:
name: Install Gitleaks
command: |
set -euo pipefail
GITLEAKS_VERSION="8.21.2"
curl -sSfL \
"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
| tar -xz gitleaks
sudo mv gitleaks /usr/local/bin/gitleaks
gitleaks version
GITLEAKS_ARCHIVE="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
GITLEAKS_SHA256="5bc41815076e6ed6ef8fbecc9d9b75bcae31f39029ceb55da08086315316e3ba"
GITLEAKS_TEMP="$(mktemp -d)"
GITLEAKS_PATH="${GITLEAKS_TEMP}/${GITLEAKS_ARCHIVE}"
trap 'rm -rf -- "${GITLEAKS_TEMP}"' EXIT

curl --fail --location --silent --show-error --retry 3 \
"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/${GITLEAKS_ARCHIVE}" \
--output "${GITLEAKS_PATH}"
printf '%s %s\n' "${GITLEAKS_SHA256}" "${GITLEAKS_PATH}" | sha256sum --check --strict
tar -xzf "${GITLEAKS_PATH}" -C "${GITLEAKS_TEMP}" gitleaks
sudo install -m 0755 "${GITLEAKS_TEMP}/gitleaks" /usr/local/bin/gitleaks
test "$(gitleaks version)" = "${GITLEAKS_VERSION}"
- run:
name: Run Gitleaks — detect committed secrets
command: |
Expand Down
15 changes: 7 additions & 8 deletions .github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,17 @@ See also: [product surfaces](https://github.com/szl-holdings/platform/tree/main/

| Workflow | Trigger | Required Check | Purpose |
|----------|---------|----------------|---------|
| `ci.yml` | PR + push to `main`/`master` | `CI Gate` | Aggregate gate: lint, typecheck, test, build, integration tests, secret scan, readiness smoke, proof-chain, route security |
| `ci.yml` | PR + push to `main` | Individual checks | Clean-clone validation on Linux and Windows, lint, and TypeScript typecheck |
| `ci.yml` | PR + push to `main`/`master` | `Readiness Gate (smoke:product-mode)` | Product-mode API smoke test surfaced separately for fast PR visibility |
| `e2e.yml` | PR + push to `main`/`master` | `E2E Gate` | Full Playwright matrix across all artifact surfaces + axe-core a11y |
| `dependency-review.yml` | PR only | `dependency-review` | OSS vulnerability scan on changed dependencies |
| `codeql.yml` | PR + push + weekly schedule | `analyze` | GitHub CodeQL static analysis (JavaScript/TypeScript) |

### Security (advisory / scheduled)
### Security

| Workflow | Trigger | Purpose |
|----------|---------|---------|
| `security.yml` | PR + push | Dependency vulnerability scan + SBOM generation |
| `secret-scan-scheduled.yml` | Daily 06:17 UTC + `.gitleaks.toml` changes | Full-history Gitleaks scan against `main`; uploads SARIF to Security tab and opens triage issue on findings |
| `secret-scan.yml` | PR only | PR-diff Gitleaks scan using `.gitleaks.toml` |
| `security.yml` | PR to `main` + push to `main` + manual + Mondays 03:00 UTC | Dependency/SBOM checks, Gitleaks and project-specific secret scans, lockfile integrity, license report, and the fan-in `Security Gate (blocking)` job |

### Build & Quality (advisory)

Expand Down Expand Up @@ -131,10 +129,11 @@ All Dependabot PRs must pass the same required CI checks as any other PR.

## Secret Scanning

Two complementary layers:
Three complementary layers:

1. **PR-time gate** (`ci.yml` → `secret-scan` job + `secret-scan.yml`): Gitleaks scans the PR diff using `.gitleaks.toml`. Blocks merge on any finding.
2. **Scheduled sweep** (`secret-scan-scheduled.yml`): Full-history Gitleaks scan of `main` every day at 06:17 UTC. Uploads SARIF to the Security tab; opens a triage issue on findings.
1. **GitHub-native scanning and push protection:** provider-known patterns are checked by GitHub, including before accepted pushes when push protection matches.
2. **PR-time scan** (`security.yml` → `secret-scan`): Gitleaks scans the PR's base-to-head commit range, then the project-specific scanner checks the current tree. A finding fails the `Security Gate (blocking)` fan-in job.
3. **Default-branch and scheduled scan** (`security.yml` → `secret-scan`): pushes to `main`, manual dispatches, and the Monday 03:00 UTC schedule scan reachable repository history with Gitleaks and check the current tree with the project-specific scanner.

Config lives in `.gitleaks.toml`. If you need to add an allowlist entry, document the reason inline and keep patterns as narrow as possible.

Expand Down
21 changes: 14 additions & 7 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,21 +72,28 @@ jobs:

- name: Install Gitleaks
run: |
set -euo pipefail
GITLEAKS_VERSION="8.21.2"
curl -sSfL \
"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
| tar -xz gitleaks
sudo mv gitleaks /usr/local/bin/gitleaks
gitleaks version
GITLEAKS_ARCHIVE="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
GITLEAKS_SHA256="5bc41815076e6ed6ef8fbecc9d9b75bcae31f39029ceb55da08086315316e3ba"
GITLEAKS_PATH="${RUNNER_TEMP}/${GITLEAKS_ARCHIVE}"

curl --fail --location --silent --show-error --retry 3 \
"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/${GITLEAKS_ARCHIVE}" \
--output "${GITLEAKS_PATH}"
printf '%s %s\n' "${GITLEAKS_SHA256}" "${GITLEAKS_PATH}" | sha256sum --check --strict
tar -xzf "${GITLEAKS_PATH}" -C "${RUNNER_TEMP}" gitleaks
sudo install -m 0755 "${RUNNER_TEMP}/gitleaks" /usr/local/bin/gitleaks
test "$(gitleaks version)" = "${GITLEAKS_VERSION}"

- name: Run Gitleaks — detect committed secrets

env:
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
# On a pull_request event scan only the commits in the PR; on push/schedule
# scan the entire current HEAD tree.
# On a pull_request event scan only the commits in the PR. Push, scheduled,
# and manual runs scan the repository history reachable from the checkout.
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
gitleaks detect \
--source . \
Expand Down
24 changes: 12 additions & 12 deletions ops/github/secret-scanning-runbook.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# Runbook — Secret Scan Hit Triage

Last updated: 2026-04-19
Last updated: 2026-08-13
Owner: SZL Holdings (repo owner)

This runbook covers what to do when **any** of the following fires on the
public `szl-holdings/szl-holdings-platform` repo:
public `szl-holdings/platform` repo:

- The PR-time Gitleaks gate (`Security Audit & SBOM / Secret Scan (Gitleaks)` in `.github/workflows/security.yml`)
- The daily scheduled Gitleaks sweep (`Secret Scan (Scheduled — main)` in `.github/workflows/secret-scan-scheduled.yml`)
- The scheduled or manual Gitleaks sweep (`Security Audit & SBOM` in `.github/workflows/security.yml`)
- GitHub-native secret scanning alert (Security → Secret scanning)
- Push protection bounced a `git push` with a "Secret detected" message

Expand All @@ -17,14 +17,14 @@ public `szl-holdings/szl-holdings-platform` repo:
|---|---|---|---|
| GitHub native secret scanning | Repo Settings → Code security → "Secret scanning" | Partner-known token patterns (AWS, GCP, GitHub, Stripe, Slack, …) | On every push to any branch, continuously over history |
| GitHub push protection | Repo Settings → Code security → "Push protection" | Same partner-known patterns, **before** the push lands | At `git push` time (blocking) |
| Gitleaks PR gate | `.github/workflows/security.yml` → `secret-scan` job | Full Gitleaks ruleset + repo overrides in `.gitleaks.toml` | Every PR (blocking — required check) |
| Gitleaks scheduled sweep | `.github/workflows/secret-scan-scheduled.yml` | Full Gitleaks ruleset over full `main` history; SARIF → Security tab | Daily 06:17 UTC + manual dispatch |
| Project-specific scanner | `scripts/qa/scan-secrets.js` | Internal naming patterns (belt-and-suspenders) | Inside the PR gate |
| Gitleaks PR gate | `.github/workflows/security.yml` → `secret-scan` job | Full Gitleaks ruleset + repo overrides in `.gitleaks.toml` over the PR base-to-head range | Every PR; a finding fails the fan-in `Security Gate (blocking)` job |
| Gitleaks history sweep | `.github/workflows/security.yml` → `secret-scan` job | Full Gitleaks ruleset over history reachable from the checked-out `main` ref | Pushes to `main`, Mondays 03:00 UTC, and manual dispatch |
| Project-specific scanner | `scripts/qa/scan-secrets.js` | Internal naming patterns (belt-and-suspenders) in the current tree | Every `security.yml` PR, push, scheduled, and manual run |

## One-time GitHub setup (owner-only, click-path)

Both of the following must be **enabled** on
`szl-holdings/szl-holdings-platform`. They are free on public repos.
`szl-holdings/platform`. They are free on public repos.

1. **Settings → Code security**
2. Under **Secret scanning** click **Enable**.
Expand All @@ -46,7 +46,7 @@ scanning: Enabled** and **Push protection: Enabled** with a green check.

### Step 1 — Confirm it's a real secret (not a fixture)

1. Open the alert / SARIF finding / failed run and read the matched line **in the redacted form GitHub shows you**.
1. Open the native alert or failed workflow run and read the matched line **only in the redacted form GitHub shows you**.
2. Check the file path against the known-safe list:
- `tests/**`, `**/__fixtures__/**`, `**/*.test.*`, `**/*.spec.*`
- `packages/demo-seed/**`, `packages/simulation/**`, `packages/replay-core/**`
Expand All @@ -67,22 +67,22 @@ The credential is already public — rotation is the only mitigation.
- Production runtime: follow `infra/runbooks/RUNBOOK_SECRETS.md`.
3. **Verify rotation** by triggering a workflow run (or `workflow_dispatch` of `Security Audit & SBOM`) that uses the secret and confirming it succeeds.
4. **Record the rotation** as a row in the table at the bottom of this file.
5. **Close the GitHub alert** with the resolution **"Revoked"** (Security → Secret scanning → click the alert → "Close as → Revoked"). For a Gitleaks-triggered triage issue, close it with a comment linking the rotation row.
5. **Close the GitHub alert** with the resolution **"Revoked"** (Security → Secret scanning → click the alert → "Close as → Revoked").
6. **Sweep history** with `gitleaks detect --log-opts="--all"` locally to confirm no other instance of the same value exists elsewhere in history; if it does, rotate any other affected credential too.

### Step 3 — Prevent the recurrence

For every confirmed leak, add a follow-up in the same PR / issue:

- If a developer workflow allowed the leak (e.g., copy-pasting an env value into source), document the safer pattern in `infra/runbooks/RUNBOOK_SECRETS.md`.
- If Gitleaks did not catch the pattern but should have, add a `[[rules]]` entry to `.gitleaks.toml` and re-run the scheduled workflow via **Actions → Secret Scan (Scheduled — main) → Run workflow**.
- If Gitleaks did not catch the pattern but should have, add a `[[rules]]` entry to `.gitleaks.toml` and re-run **Actions → Security Audit & SBOM → Run workflow**.

## Manual on-demand scan

Anyone with `read` on the repo can trigger the scheduled scan ad-hoc:
An authorized maintainer can trigger the scan ad-hoc:

```text
Actions → Secret Scan (Scheduled — main) → Run workflow → main → Run workflow
Actions → Security Audit & SBOM → Run workflow → main → Run workflow
```

Locally:
Expand Down
24 changes: 16 additions & 8 deletions services/customer-portal/.github/workflows/gitleaks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,21 @@ jobs:
fetch-depth: 0
- name: Install gitleaks
run: |
VER="8.21.2"
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${VER}/gitleaks_${VER}_linux_x64.tar.gz" -o /tmp/gitleaks.tgz
tar -xzf /tmp/gitleaks.tgz -C /tmp gitleaks
sudo install /tmp/gitleaks /usr/local/bin/gitleaks
gitleaks version
set -euo pipefail
GITLEAKS_VERSION="8.21.2"
GITLEAKS_ARCHIVE="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
GITLEAKS_SHA256="5bc41815076e6ed6ef8fbecc9d9b75bcae31f39029ceb55da08086315316e3ba"
GITLEAKS_PATH="${RUNNER_TEMP}/${GITLEAKS_ARCHIVE}"

curl --fail --location --silent --show-error --retry 3 \
"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/${GITLEAKS_ARCHIVE}" \
--output "${GITLEAKS_PATH}"
printf '%s %s\n' "${GITLEAKS_SHA256}" "${GITLEAKS_PATH}" | sha256sum --check --strict
tar -xzf "${GITLEAKS_PATH}" -C "${RUNNER_TEMP}" gitleaks
sudo install -m 0755 "${RUNNER_TEMP}/gitleaks" /usr/local/bin/gitleaks
test "$(gitleaks version)" = "${GITLEAKS_VERSION}"
- name: Gitleaks scan
run: |
CFG=""
[ -f .gitleaks.toml ] && CFG="-c .gitleaks.toml"
gitleaks detect --no-banner --redact ${CFG} --exit-code 1
GITLEAKS_CONFIG=()
[ -f .gitleaks.toml ] && GITLEAKS_CONFIG=(-c .gitleaks.toml)
gitleaks detect --no-banner --redact "${GITLEAKS_CONFIG[@]}" --exit-code 1
Loading