The Problem
Your team knows security scanning should happen on every pull request, but setting it up is a maze of tool configuration, noisy false positives, and unclear remediation steps. You install a SAST tool, it finds 300 issues on the first run — 280 of which are informational or false positives — and developers start ignoring the results entirely. The scanner that was supposed to catch vulnerabilities becomes the tool that cried wolf.
Meanwhile, actual vulnerabilities slip through because nobody trusts the output. A 25-person engineering org ends up doing manual security reviews before releases, which creates a bottleneck and still misses dependency vulnerabilities. Last quarter, a penetration test found an SQL injection that had been in production for eight months. It was in the scanner's default ruleset the entire time — buried under 280 other findings that everyone learned to scroll past.
The failed scanner attempt has a second-order effect: the team becomes resistant to trying again. "We already tried security scanning and it didn't work" becomes conventional wisdom, when what actually happened was that the tooling was misconfigured and the triage was missing.
The Solution
Using the security-audit skill to configure and tune scanning tools, the cicd-pipeline skill to integrate scans into the PR workflow, and the coding-agent to auto-triage findings and generate fix suggestions, the pipeline catches real vulnerabilities while keeping noise low enough that developers actually pay attention. The key insight: a scanner that surfaces 8 real findings is infinitely more valuable than one that surfaces 300 findings nobody reads.
Step-by-Step Walkthrough
Step 1: Assess the Current Security Posture
Before adding scanners, understand what's missing. A gap analysis prevents the "install everything and see what happens" approach that leads to the 300-finding wall:
Audit our Node.js monorepo for security scanning gaps. We have a GitHub Actions
CI pipeline but no security scanning. The repo has 3 services: api/ (Express),
worker/ (Bull queue), and web/ (Next.js). Show me what we're missing.
Step 2: Identify What's Missing
The gap analysis comes back with a clear inventory of blind spots:
Currently missing:
- Dependency vulnerability scanning (npm audit / Snyk) — 847 transitive dependencies with no visibility into known CVEs
- Static analysis for code vulnerabilities (semgrep / CodeQL) — no automated detection of injection, XSS, or auth bypass patterns
- Secret detection (no pre-commit hooks or CI checks) — leaked credentials would reach the repository unnoticed
- Container image scanning (Dockerfiles present but unscanned) — base images could contain known vulnerabilities
- License compliance checking — no visibility into GPL or other restrictive licenses in dependencies
Risk indicators that need immediate attention:
api/package-lock.jsonhas 847 dependencies, last audit unknown- 3 Dockerfiles using
node:18base image — not pinned to a digest, so a compromised or vulnerable image could be pulled silently on any build - No
.gitignoreentry for.envfiles inworker/— one accidentalgit add .away from leaking database credentials to the repository
Step 3: Generate CI Pipeline Configuration
The scanning workflow covers three dimensions — dependencies, secrets, and static analysis — each running as a parallel job so scans don't add to PR wait time:
# .github/workflows/security-scan.yml
name: Security Scan
on: [pull_request]
jobs:
dependency-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm audit --audit-level=high --workspace=api
- run: npm audit --audit-level=high --workspace=worker
- run: npm audit --audit-level=high --workspace=web
secret-detection:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for detecting secrets in past commits
- uses: gitleaks/gitleaks-action@v2
static-analysis:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: returntocorp/semgrep-action@v1
with:
config: p/nodejs p/jwt p/sql-injection
Three jobs, running in parallel on every PR. Dependency check catches known CVEs in the 847 transitive dependencies. Secret detection scans the full git history — not just the current diff — for leaked credentials, API keys, and tokens. Static analysis looks for code-level vulnerabilities with rulesets specific to the Node.js ecosystem: injection patterns, insecure JWT handling, and SQL construction.
The three jobs run in parallel, so the total scan time is the duration of the slowest job (usually static analysis at 2-3 minutes), not the sum of all three.
Step 4: Configure Triage Rules to Cut the Noise
This is the step that makes or breaks the entire setup. Without triage, the first scan surfaces 300 findings and developers tune out permanently. The previous team that tried this made exactly that mistake.
The triage configuration (security-triage.json) maps severity and location to action:
| Finding | Location | Action |
|---|---|---|
| CRITICAL or HIGH | src/ | Block PR, require fix before merge |
| MEDIUM | src/ | Warn, add PR comment, don't block |
| Any severity | test/ or __mocks__/ | Suppress entirely |
| Dependency vuln with no fix available | Any | Warn only, don't block |
A .semgrep-ignore file excludes 12 rule patterns for test files and generated code. Test files that deliberately contain injection patterns (for testing input validation, for example) don't trigger alerts. Generated code from ORMs and protobuf compilers gets excluded because developers can't fix those findings anyway.
Expected noise reduction: roughly 80% fewer false positive comments on PRs. The scanner goes from "300 findings, ignore everything" to "8 findings, all worth reading."
Step 5: Generate Fixes for Real Vulnerabilities
The initial scan of the current codebase surfaces 3 actionable vulnerabilities:
1. SQL injection in api/src/routes/search.js:42 (HIGH)
// Before — string interpolation builds the query from user input
db.query(`SELECT * FROM products WHERE name LIKE '%${term}%'`)
// After — parameterized query keeps user input out of the SQL
db.query('SELECT * FROM products WHERE name LIKE $1', [`%${term}%`])
This is the exact vulnerability the penetration test found. It's been in production for eight months, hidden under 280 other findings that the team stopped reading.
2. Prototype pollution via lodash.merge in worker/ (HIGH)
npm install lodash@4.17.21 --workspace=worker
A one-line fix — the patched version of lodash closes the prototype pollution vector. The vulnerable version has been in the lockfile since the worker service was created.
3. Missing rate limiting on /api/auth/login (MEDIUM)
A generated patch adds express-rate-limit middleware — 5 attempts per minute per IP, with a clear 429 response for the client. Without rate limiting, the login endpoint is vulnerable to credential stuffing attacks.
Three findings, three fixes, all applied in one sprint. The contrast with the previous experience is stark: instead of 300 findings that paralyze the team, there are 3 that each have a clear fix. The same SQL injection that a $20,000 penetration test found would have been caught on the developer's first PR if this pipeline had existed eight months ago.
Going forward, every new PR gets scanned automatically. A developer introducing a string-interpolated SQL query gets blocked at the PR level with a specific fix suggestion — not a generic "vulnerability detected" message, but the exact parameterized query they should use instead.
Real-World Example
Tomas leads backend engineering at a 25-person SaaS startup. They passed a customer security questionnaire by promising automated scanning, but never actually implemented it. Then a penetration test found an SQL injection that had been in production for eight months — the kind of finding that shakes customer confidence and requires a post-incident disclosure.
He asks the agent to set up comprehensive scanning for the monorepo. The GitHub Actions workflows go in for dependency, SAST, and secret scanning. Triage rules ensure developers only see actionable findings — the first PR scan surfaces 8 real issues instead of 300 noisy alerts. The agent generates fix patches for 5 of the 8, which developers apply in the same sprint. The remaining 3 (two dependency vulnerabilities with no upstream fix, one medium-severity finding requiring architectural changes) get tracked as issues with clear remediation plans.
Within a month, every PR gets scanned automatically, the team trusts the results because false positives are rare, and the next penetration test comes back clean. The security questionnaire answer changes from a promise to a fact — with CI logs to prove it.