Layer 1
CI gates
Block PRs on leaked secrets, vulnerable dependencies, and code patterns linked to auth bypass.
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
Block PRs on leaked secrets, vulnerable dependencies, and code patterns linked to auth bypass.
Layer 2
Trail of Bits and Ghost Security marketplace skills for on-demand deep analysis.
Layer 3
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.
security-secrets (gitleaks)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)Two passes:
osv-scanner reads the lockfile and queries the OSV database for known CVEs across the dep tree, including transitive deps.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.
security-sast (Semgrep)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:
main branch protection: signed commits required, linear history, no force-push, no deletion, all status checks blocking, conversations must resolveAll 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:
ignoreUntil allowlist entry expiringTwo 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:
| Skill | Use it when |
|---|---|
/differential-review | Reviewing a diff for security regressions |
/sharp-edges <path> | Asking “what could bite me in this file?” |
/supply-chain-risk-auditor | Adding a new dep |
/insecure-defaults | Reviewing config and env handling |
/static-analysis | Running ad-hoc CodeQL/Semgrep on a branch |
/fp-check | Getting a second opinion on a finding |
Ghost Security adds two AI-driven scanners:
| Skill | Use it when |
|---|---|
/ghost-scan-code | Want a SAST sweep over a diff |
/ghost-validate | Probing 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:
stripe_event_id dedup)accountId scoping on every route handler/auth/login, /auth/forgot-password, /auth/resend-verification)For apps/ui:
fetch; only @/lib/api/client.ts calls the APIdangerouslySetInnerHTMLimport.meta.env outside src/lib/env/Invoke either way:
/security-reviewEvery 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 runthe 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-evalconst spec: unknown = new Function("return " + precompiledCode)();This pipeline is opinionated for the BoringStack template surface. It doesn’t replace:
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.
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.
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.
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.
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.