Skip to content
Merged
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
60 changes: 40 additions & 20 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened, labeled]
schedule:
# Weekly whole-tree vulnerability sweep (Mondays 06:00 UTC), independent of
# PR/push activity, so a newly-published advisory surfaces within a week even
# when the repo is quiet.
- cron: '0 6 * * 1'
workflow_dispatch:

permissions:
actions: read
contents: read
security-events: write

jobs:
test:
Expand All @@ -32,9 +35,6 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Audit dependencies
run: npm audit --audit-level=moderate

- name: Type check
run: npm run check

Expand Down Expand Up @@ -77,23 +77,43 @@ jobs:
sarif_file: guarddog.sarif
category: guarddog

osv-scanner:
name: OSV Scanner
# google/osv-scanner-action v2.3.8
# NOTE: the repo setting "Require actions pinned to a full-length commit SHA"
# is intentionally OFF. v2.3.8's reusable workflow ships an unpinned
# actions/download-artifact, which GitHub's SHA-pin enforcement rejects
# recursively (breaks this job at startup). Fixed upstream on main
# (osv-scanner-action commit f6fb127, not yet in a release). Re-enable the
# setting once Dependabot bumps this pin past the next osv-scanner-action release.
uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@9a498708959aeaef5ef730655706c5a1df1edbc2
with:
scan-args: |-
--lockfile=package-lock.json
security-audit:
name: Dependency Audit
runs-on: ubuntu-24.04
# Whole-tree CVE gate. Runs on push-to-main, the weekly schedule, and manual
# dispatch — never on pull_request. This is deliberate: a freshly-published
# advisory in (dev/build) tooling that a PR never touched should not red-X
# every open PR. On PRs, vulnerabilities are gated by Dependency Review
# (scoped to the deps the diff introduces) plus GuardDog; Dependabot alerts
# and auto-PRs are the remediation path. The push-to-main run still gates the
# deploy job below, so production protection is unchanged.
if: github.event_name != 'pull_request'
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10

- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
node-version: 20
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Audit dependencies
run: npm audit --audit-level=moderate

sbom:
name: SBOM & Grype Scan
runs-on: ubuntu-24.04
# Off the PR path (same rationale as security-audit). Generates the SBOM
# artifact and runs Grype for Security-tab visibility, but is non-blocking
# (fail-build: false) — npm audit in security-audit is the single whole-tree
# gate. Runs on push-to-main + schedule and feeds the deploy pipeline below.
if: github.event_name != 'pull_request'
permissions:
contents: read
security-events: write
Expand All @@ -114,7 +134,7 @@ jobs:
uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2
with:
sbom: sbom.spdx.json
fail-build: true
fail-build: false
severity-cutoff: medium

- name: Upload Grype SARIF
Expand Down Expand Up @@ -144,7 +164,7 @@ jobs:
deploy:
name: Deploy to Cloudflare Pages
runs-on: ubuntu-24.04
needs: [test, guarddog, osv-scanner, sbom]
needs: [test, guarddog, security-audit, sbom]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: production
permissions:
Expand Down
29 changes: 11 additions & 18 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ You can expect an acknowledgement within 48 hours and a resolution or status upd
| Adversary | Goal | Primary controls |
|---|---|---|
| Malicious paste / shared link | Trigger XSS via crafted SAML or JWT content rendered in the DOM | Svelte's default text escaping, `esc()` in the XML highlighter, CSP (`default-src 'self'`, no `unsafe-inline`, hash-pinned inline scripts) |
| Compromised npm package | Inject malicious code into the build or CI environment | GuardDog, OSV-Scanner, Grype, `npm audit`, SHA-pinned actions, `npm ci` lockfile enforcement |
| Compromised npm package | Inject malicious code into the build or CI environment | GuardDog, `npm audit`, Grype, Dependency Review, SHA-pinned actions, `npm ci` lockfile enforcement |
| Compromised upstream GitHub Action | Substitute malicious CI code via a tampered version tag | All actions pinned to commit SHA, Dependabot rotates pins daily |
| SSRF via OIDC proxy | Use the discovery Worker to reach internal infrastructure | `redirect: 'error'`, 5-second timeout, 100 KB cap, response validation |
| Compromised maintainer account | Push unsigned or unreviewed code to `main` | Required commit signatures, branch protection, CODEOWNERS review, scoped API tokens |
Expand Down Expand Up @@ -137,7 +137,6 @@ Current pins in `.github/workflows/ci.yml`:
- `codecov/codecov-action` — SHA-pinned
- `github/codeql-action/upload-sarif` — SHA-pinned (v3.28.13)
- `actions/dependency-review-action` — SHA-pinned (v5.0.0)
- `google/osv-scanner-action` reusable workflow — SHA-pinned (v2.3.8)
- `anchore/sbom-action` — SHA-pinned (v0.24.0)
- `anchore/scan-action` — SHA-pinned (v7.4.0)
- `actions/attest-build-provenance` — SHA-pinned (v4.1.0)
Expand All @@ -160,13 +159,9 @@ Dependabot is configured to open daily PRs when new versions of these actions ar

### Minimal token permissions

All workflows use explicit `permissions` blocks rather than relying on GitHub's write-all default. The CI workflow sets the following at the workflow level, which serves as the baseline for all jobs:
All workflows use explicit `permissions` blocks rather than relying on GitHub's write-all default. The CI workflow sets `contents: read` at the workflow level as the baseline for all jobs.

- `actions: read` — required for the OSV-Scanner reusable workflow (GitHub constrains reusable workflow callers: the calling workflow must include any permissions granted to the called workflow)
- `contents: read`
- `security-events: write` — required for SARIF upload to GitHub Code Scanning across multiple jobs; also needed at workflow level for the OSV-Scanner reusable workflow call

Individual jobs override this baseline to the minimum they require. The deploy job additionally requests `deployments: write` (Cloudflare Pages action), `id-token: write` (OIDC for sigstore attestation), and `attestations: write` (GitHub attestation API). No job can write to the repository, create issues, or make API calls beyond what is explicitly declared.
Individual jobs add only what they require — the `GuardDog Supply Chain Scan` and `SBOM & Grype Scan` jobs each request `security-events: write` for their own SARIF upload. The deploy job additionally requests `deployments: write` (Cloudflare Pages action), `id-token: write` (OIDC for sigstore attestation), and `attestations: write` (GitHub attestation API). No job can write to the repository, create issues, or make API calls beyond what is explicitly declared.

### No secrets in PR workflows

Expand All @@ -180,7 +175,9 @@ The deploy job targets a GitHub Environment named `production`. This provides a

### Dependency auditing

The CI workflow runs `npm audit --audit-level=moderate` on every push and pull request. The build fails if any moderate, high, or critical vulnerabilities are present in the transitive dependency tree. Dependencies are installed with `npm ci` (not `npm install`), which installs exactly what is recorded in `package-lock.json` and fails if there is any discrepancy — preventing lockfile drift and ensuring reproducible installs.
`npm audit --audit-level=moderate` runs in a dedicated `Dependency Audit` job that fails the build if any moderate, high, or critical vulnerability is present anywhere in the transitive dependency tree. This job runs on push to `main`, on a weekly schedule (Mondays 06:00 UTC), and on manual dispatch — but **not** on pull requests. The rationale: a freshly-published advisory in build/test tooling that a PR never touched should not block every open pull request at once. Pull requests are vulnerability-gated by Dependency Review (scoped to the dependencies the diff introduces) and GuardDog; Dependabot alerts and auto-PRs drive remediation; and the push-to-`main` run still gates the production deploy, so deploy protection is unchanged.

Dependencies are installed with `npm ci` (not `npm install`), which installs exactly what is recorded in `package-lock.json` and fails if there is any discrepancy — preventing lockfile drift and ensuring reproducible installs.

### Secret scanning

Expand Down Expand Up @@ -208,25 +205,21 @@ A CodeQL workflow runs on every push and pull request to `main`, and weekly on M

GuardDog runs entirely within the CI runner — no data is sent to any external service. Findings are reported as SARIF and uploaded to the GitHub code scanning dashboard. The job must pass before deploy is allowed.

### OSV-Scanner

[OSV-Scanner](https://google.github.io/osv-scanner/) (Google, Apache 2.0) runs on every push and pull request via Google's official reusable workflow. It queries the [Open Source Vulnerabilities](https://osv.dev) database, which aggregates CVE, GitHub Security Advisories (GHSA), and OSV records — a broader set of sources than npm's advisory feed alone. Findings are automatically uploaded to the GitHub code scanning dashboard as SARIF. The job must pass before deploy is allowed.

### SBOM and Grype

On every push and pull request:
Alongside the `Dependency Audit` job (push, weekly schedule, and manual dispatch — not pull requests):

- **Syft** ([anchore/sbom-action](https://github.com/anchore/sbom-action), Apache 2.0) generates a Software Bill of Materials in SPDX-JSON format and uploads it as a workflow artifact. This provides an auditable inventory of every package in the build and supports compliance requirements that expect a machine-readable SBOM.

- **Grype** ([anchore/scan-action](https://github.com/anchore/scan-action), Apache 2.0) scans the Syft-generated SBOM for known vulnerabilities at medium severity or higher. Findings are uploaded to the GitHub code scanning dashboard as SARIF. The job must pass before deploy is allowed.
- **Grype** ([anchore/scan-action](https://github.com/anchore/scan-action), Apache 2.0) scans the Syft-generated SBOM for known vulnerabilities at medium severity or higher and uploads findings to the GitHub code scanning dashboard as SARIF. Grype runs in non-blocking mode (`fail-build: false`): `npm audit` in the `Dependency Audit` job is the single blocking CVE gate, while Grype contributes a second, independent vulnerability database to the Security tab.

### Dependency Review

The [dependency-review-action](https://github.com/actions/dependency-review-action) runs on pull requests only. It compares the dependency diff introduced by the PR against GitHub's vulnerability database and fails the check if any newly added package carries a moderate or higher CVE. This catches vulnerable dependencies at PR time, before they land in `main`, complementing the full-tree scans that run on push.
The [dependency-review-action](https://github.com/actions/dependency-review-action) runs on pull requests only. It compares the dependency diff introduced by the PR against GitHub's vulnerability database and fails the check if any newly added package carries a moderate or higher CVE. This is the pull-request-time vulnerability gate: it catches vulnerable dependencies a PR would *introduce*, before they land in `main`, while the full-tree `npm audit` and Grype scans run on push and the weekly schedule. A PR is therefore never blocked by a pre-existing, tree-wide advisory in a dependency the PR did not touch.

### GitHub Code Scanning

All SARIF-producing tools (CodeQL, GuardDog, OSV-Scanner, Grype, OSSF Scorecard) upload their findings to GitHub's code scanning dashboard (Security → Code scanning). This provides a single triage surface across all scanners, with per-file, per-line annotation on pull requests. Each tool registers under its own `category` so findings are de-duplicated and attributable to their source.
All SARIF-producing tools (CodeQL, GuardDog, Grype, OSSF Scorecard) upload their findings to GitHub's code scanning dashboard (Security → Code scanning). This provides a single triage surface across all scanners, with per-file, per-line annotation on pull requests. Each tool registers under its own `category` so findings are de-duplicated and attributable to their source.

### OSSF Scorecard

Expand All @@ -242,7 +235,7 @@ This means the provenance of every deployed bundle is verifiable: given the arti

The `main` branch is protected with the following rules enforced for all contributors:

- **Required status checks** — `Build & Test`, `GuardDog Supply Chain Scan`, `OSV Scanner / osv-scan`, `SBOM & Grype Scan`, and `Dependency Review` must all pass before any merge is allowed; the branch must be up to date with `main` before merging (strict mode)
- **Required status checks** — `Build & Test`, `GuardDog Supply Chain Scan`, `Dependency Review`, and `CodeQL` must all pass before any merge is allowed; the branch must be up to date with `main` before merging (strict mode). The whole-tree scans (`Dependency Audit`, `SBOM & Grype Scan`) run on push and the weekly schedule rather than on pull requests, so they are not pull-request status checks — they gate the production deploy instead
- **Required signatures** — every commit merged to `main` must carry a verified cryptographic signature
- **Required pull request review** — at least one approval is required; stale approvals are dismissed on new pushes; code owner review is required
- **No force pushes** — force-pushing to `main` is blocked
Expand Down
Loading