Skip to content
BoringStack
Star

Security pipeline

7 min read

A fresh fork ships with secret scanning, dependency auditing, SAST, signed commits, branch protection, and agent skills already wired. Three automated layers block PRs on leaked secrets, vulnerable dependencies, and code patterns linked to auth bypass. All three run on a Monday-morning cron (06:23 UTC). New CVEs against existing dependencies are caught automatically without manual rescans.

The three layers:

  1. CI gates: block PRs on leaked secrets, vulnerable dependencies, and code patterns linked to auth bypass.
  2. Agent skills: Trail of Bits and Ghost Security marketplace skills for on-demand deep analysis.
  3. Project review skill: orchestrates Layer 2 and verifies stack-specific requirements generic tools miss.

Every push to main and every pull request runs three blocking workflows.

Catches API keys, tokens, private keys, and other high-entropy strings before they reach main. GitHub’s secret push protection is the first layer. The gitleaks CLI with a versioned .gitleaksignore is the second. Findings upload as SARIF to the repo’s Security tab.

Verify: push a test secret to a feature branch. The workflow should block it before the PR can merge.

security-deps (osv-scanner + native audit)

Section titled “security-deps (osv-scanner + native audit)”

Two passes:

  • osv-scanner reads the lockfile and queries the OSV database for known CVEs across the dependency tree, including transitive dependencies.
  • Native audit (bun audit for apps/api, bun run audit for apps/ui, Trivy config mode for the OpenTofu repo) catches things the OSV cross-reference misses.

Both honor osv-scanner.toml for accepted-risk allowlisting. Every ignored CVE carries a reason and an ignoreUntil date. When the date passes, the suppression dies and CI fails. No silent suppressions, no infinite snoozing.

Verify: run osv-scanner --lockfile=package-lock.json locally. It should match the workflow output.

Runs OWASP and JavaScript rule packs plus repo-specific rules from .semgrep/. Findings upload as SARIF to GitHub Code Scanning. Custom rules catch BoringStack-specific footguns: new Function-style template eval, logger payloads that include PII, raw SQL string concatenation.

Verify: run semgrep --config=p/owasp-top-ten --config=.semgrep src/ locally. It should match the workflow output.

The monorepo root ./scripts/audit-repo-settings.sh diffs the live GitHub configuration against .github/desired-repo-settings.json. Drift prints copy-pasteable gh api commands. Nothing auto-applies.

Desired state on every repo:

  • Secret scanning and push protection: enabled
  • Dependabot security updates: enabled
  • Merge style: squash-only, auto-delete branch
  • main branch protection: signed commits required, linear history, no force-push, no deletion, all status checks blocking, conversations must resolve

Verify: run ./scripts/audit-repo-settings.sh locally. It should report zero drift.

All security workflows fire on 0 6 * * 1 (Monday morning UTC, staggered by minute to avoid the GitHub Actions cron pileup at :00). Even if nobody pushes for a month, CI catches:

  • A new CVE filed against a dependency you’re already using
  • An ignoreUntil allowlist entry expiring
  • A new rule release from Semgrep or osv

The security specification runs behavioral cases for the 22 tracked API findings against required Postgres and Valkey services. Named-case reconciliation and positive controls prevent a missing fixture or skipped test from masquerading as evidence. The security spec (review findings) workflow reports for PRs targeting main and runs the expensive cases when relevant paths change.

This complements secret, dependency and static analysis. Read the upgrade runbook for refresh-lineage migration, OAuth writer cutover and changed entitlement defaults; passing scans alone does not cover those operational requirements.

Two marketplaces are declared in .claude/settings.json. When you trust the folder, Claude Code prompts to install them.

Trail of Bits ships six specialist skills the same firm uses on paid engagements:

SkillUse it when
/differential-reviewReviewing a diff for security regressions
/sharp-edges <path>Asking “what could bite me in this file?”
/supply-chain-risk-auditorAdding a new dep
/insecure-defaultsReviewing config and env handling
/static-analysisRunning ad-hoc CodeQL/Semgrep on a branch
/fp-checkGetting a second opinion on a finding

Ghost Security adds two AI-driven scanners:

SkillUse it when
/ghost-scan-codeWant a SAST sweep over a diff
/ghost-validateProbing a running service for live vulnerabilities (DAST)

Install once. After that, humans and agents can both invoke /sharp-edges src/auth/oauth.service.ts and get a deep pass without leaving the editor.

.claude/skills/security-review.md in each template orchestrates Layer 2 and adds checks the generic tools can’t make. For apps/api:

  • ACL coverage on every account-scoped table
  • Stripe webhook idempotency (stripe_event_id dedup)
  • Multi-tenant accountId scoping on every route handler
  • Rate limits on credential routes (/auth/login, /auth/forgot-password, /auth/resend-verification)
  • Audit-log on every mutation
  • BullMQ jobs idempotent under retry

For apps/ui:

  • No raw fetch; only @/lib/api/client.ts calls the API
  • No dangerouslySetInnerHTML
  • No import.meta.env outside src/lib/env/
  • No localStorage token storage
  • CSRF and content-type validation on user-upload flows

Invoke either way:

/security-review

Every accepted-risk suppression has a date and a reason. The format is consistent across the three layers.

osv-scanner.toml holds accepted CVEs:

[[IgnoredVulns]]
id = "GHSA-67mh-4wv8-2f99"
ignoreUntil = "2026-11-18T00:00:00Z"
reason = """
esbuild dev-server RCE. Production builds (Dockerfile.prod) do not run
the esbuild dev server; the bundled artifact has no exposed surface.
Awaiting upstream patch via vite transitive deps.
"""

.gitleaksignore holds known false positives (test fixtures, public keys):

<commit-sha>:<file>:<rule-id>:<line>

// nosemgrep: <rule-id> is the inline Semgrep suppression. Each one needs a sibling block comment explaining why:

/*
* `precompiledCode` is the JSON output of Handlebars.precompile() over
* template files we own. Never user input, never network-reachable.
*/
// nosemgrep: semgrep.no-eval
const spec: unknown = new Function("return " + precompiledCode)();

This pipeline is designed specifically for the BoringStack template surface. It doesn’t replace:

  • Penetration testing before a production launch
  • Threat modeling for novel surface area you add on top
  • Compliance audits (SOC 2, ISO 27001), which need an auditor, not a CI workflow
  • Manual review of cryptography, secrets storage, or session handling you write yourself

The CI gates block known-bad patterns. The agent skills surface “you forgot to think about X.” Neither substitutes for thinking.

First check whether it’s a real secret. If yes, rotate it immediately (the secret is already in git history) and amend the commit. If false positive (test fixture, public key), add a fingerprint line to .gitleaksignore and re-push.

osv-scanner flags a new CVE in your dependency tree

Section titled “osv-scanner flags a new CVE in your dependency tree”

Read the advisory. If patched, bump the dependency and re-run. If unpatched but not reachable from your code path, add an [[IgnoredVulns]] block to osv-scanner.toml with a written reason and an ignoreUntil date one quarter out, giving upstream time to ship a patch.

Add // nosemgrep: <rule-id> directly above the line, plus a block comment explaining why the pattern is safe in context. If the rule fires this way often, propose tightening the rule in .semgrep/ instead.

Monday cron fails when nobody changed anything

Section titled “Monday cron fails when nobody changed anything”

A new CVE was filed against an existing dependency, or an ignoreUntil expired. Read the run output and triage as above. The cron exists for this. It surfaces drift in your dependency surface even when you’re not actively pushing.

scripts/audit-repo-settings.sh reports drift

Section titled “scripts/audit-repo-settings.sh reports drift”

Someone (or you) clicked a setting in the GitHub UI. Paste the suggested gh api commands and re-run the audit. If the desired state is wrong, update .github/desired-repo-settings.json first.