Skip to content
BoringStack
GitHub

Security pipeline

7 min read

Security CI

A fresh fork ships with secret scanning, dep 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.

3

automated layers

Weekly

cron sweep

Zero

silent suppressions

Layer 1

CI gates

Block PRs on leaked secrets, vulnerable dependencies, and code patterns linked to auth bypass.

Layer 2

Agent skills

Trail of Bits and Ghost Security marketplace skills for on-demand deep analysis.

Layer 3

Project review skill

Orchestrates Layer 2 and verifies stack-specific requirements generic tools miss.

All three run on a Monday-morning cron (06:23 UTC). New CVEs against existing dependencies are caught automatically; no manual rescans needed.

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 net. The gitleaks CLI with a versioned .gitleaksignore is the second. Findings upload as SARIF to the repo’s Security tab.

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 dep tree, including transitive deps.
  • The 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.

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

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.

The 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

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

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

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 opinionated 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.

A new gitleaks finding on a PR

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 dep tree

Read the advisory. If patched, bump the dep 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.

Semgrep flags a rule on legitimate code

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

A new CVE was filed against an existing dep, or an ignoreUntil expired. Read the run output, 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

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.