Build:** SCA, container image scanning, binary signing
- Deploy: Runtime policy validation, DAST (staging only)
- Post-Deploy: Runtime monitoring, SBOM verification
Tools should output machine-readable formats (JSON, SARIF) for programmatic consumption.
Step 3: Build a Security Decision Engine
Scanners produce signals; the pipeline must make decisions. A TypeScript-based gate script aggregates results, evaluates against policy, and enforces outcomes (pass, warn, fail). This decouples tool output from pipeline logic.
Step 4: Implement Secret Management and Rotation
Hardcoded credentials in pipelines are a primary attack vector. Use a secrets manager (HashiCorp Vault, AWS Secrets Manager, GitHub Secrets) with dynamic credentials. Rotate pipeline tokens automatically and enforce least-privilege access per job.
Step 5: Establish Feedback Loops and Metrics
Security velocity requires measurement. Track:
- Policy violation rate per repository
- Mean time to remediate (MTTR) by severity
- False positive ratio
- Gate pass rate vs. deployment frequency
Architecture Decisions and Rationale
Why Policy-as-Code over tool-specific rules? Tool configurations are vendor-locked and difficult to audit. PaC centralizes security logic, enables unit testing of policies, and allows consistent enforcement across heterogeneous toolchains.
Why TypeScript for the decision engine? Modern CI/CD ecosystems (GitHub Actions, GitLab CI, custom runners) run on Node.js. TypeScript provides type safety for scan output parsing, enables reusable policy evaluation libraries, and integrates seamlessly with existing pipeline orchestration code.
Why shift-left + shift-right? Shift-left prevents known vulnerabilities from entering the artifact. Shift-right detects runtime anomalies, configuration drift, and zero-day exploits. Both are required for defense-in-depth.
Code Example: Security Gate Engine (TypeScript)
import { readFileSync } from 'fs';
import { execSync } from 'child_process';
interface ScanResult {
tool: string;
severity: 'critical' | 'high' | 'medium' | 'low';
count: number;
details: Array<{ id: string; file: string; line: number }>;
}
interface PolicyThresholds {
maxCritical: number;
maxHigh: number;
allowWarnings: boolean;
}
export class SecurityGate {
constructor(private thresholds: PolicyThresholds) {}
evaluate(scanResults: ScanResult[]): { status: 'PASS' | 'WARN' | 'FAIL'; message: string } {
const criticalCount = scanResults
.filter(r => r.severity === 'critical')
.reduce((sum, r) => sum + r.count, 0);
const highCount = scanResults
.filter(r => r.severity === 'high')
.reduce((sum, r) => sum + r.count, 0);
if (criticalCount > this.thresholds.maxCritical) {
return {
status: 'FAIL',
message: `Policy violation: ${criticalCount} critical findings exceed threshold (${this.thresholds.maxCritical})`
};
}
if (highCount > this.thresholds.maxHigh) {
return {
status: 'FAIL',
message: `Policy violation: ${highCount} high findings exceed threshold (${this.thresholds.maxHigh})`
};
}
if (this.thresholds.allowWarnings && (criticalCount > 0 || highCount > 0)) {
return {
status: 'WARN',
message: `Security warnings: ${criticalCount} critical, ${highCount} high findings detected. Proceeding with manual review.`
};
}
return { status: 'PASS', message: 'All security thresholds met.' };
}
}
// Usage in pipeline
const results: ScanResult[] = JSON.parse(readFileSync('./scan-results.json', 'utf-8'));
const gate = new SecurityGate({ maxCritical: 0, maxHigh: 2, allowWarnings: false });
const decision = gate.evaluate(results);
if (decision.status === 'FAIL') {
console.error(`[SECURITY GATE] ${decision.message}`);
process.exit(1);
} else if (decision.status === 'WARN') {
console.warn(`[SECURITY GATE] ${decision.message}`);
} else {
console.log(`[SECURITY GATE] ${decision.message}`);
}
This engine decouples tool output from pipeline behavior. It enforces deterministic thresholds, supports warning states for non-blocking findings, and integrates with any CI system that can execute Node.js.
Pitfall Guide
-
Treating security as a CI stage, not a design constraint
Adding a scanner to the end of a pipeline does not change developer behavior. Security must be enforced at commit time with immediate feedback. Post-merge gates create batch rework and degrade developer experience.
-
Ignoring false positive triage workflows
Scanners generate noise. Without a structured triage process (suppression rules, baseline files, manual review queues), teams disable gates or ignore output. Implement a false positive SLA and maintain a curated suppression policy.
-
Scanning everything without risk-based prioritization
Equal weighting of all findings causes alert fatigue. Prioritize by attack surface, data classification, and exploitability. Focus SAST/SCA on user-facing services and infrastructure components first.
-
Hardcoding secrets in pipeline configurations
Environment variables, CI secrets, and service accounts are frequently over-provisioned. Use dynamic credential injection, short-lived tokens, and audit logging. Never store secrets in version control or pipeline YAML.
-
Neglecting runtime security (shift-right gap)
Pre-deployment scans cannot catch zero-days, misconfigurations applied post-deploy, or lateral movement. Implement runtime policy enforcement, eBPF-based monitoring, and SBOM verification in production.
-
Security teams operating in isolation from DevOps
Security cannot be a separate ticket queue. Embed security champions in engineering squads, co-own pipeline policies, and measure security velocity alongside deployment metrics.
-
Over-engineering the pipeline with redundant tools
Running three SAST scanners and two SCA tools increases noise without improving coverage. Standardize on a single authoritative source per category. Validate tool selection against attack models, not vendor marketing.
Best Practices from Production:
- Start with critical paths: authentication services, payment processing, data pipelines.
- Use policy-as-code for all enforcement; avoid tool-specific exceptions.
- Implement SLA-based remediation: criticals within 24h, highs within 7 days, medium within 30 days.
- Automate secret rotation and enforce least privilege per pipeline job.
- Measure security velocity: track policy violation rate, MTTR, and gate pass rate.
- Treat security findings as technical debt; backlog and prioritize alongside features.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Startup MVP (1-10 engineers) | Lightweight PaC + SCA + Secret Detection | Fast feedback, low overhead, prevents dependency breaches | Low tooling cost; minimal pipeline latency |
| Regulated Enterprise (Finance/Healthcare) | Full shift-left + IaC scanning + runtime CSPM + audit logging | Compliance requirements, audit trails, strict change control | High initial integration cost; reduces breach liability |
| High-Velocity Platform (SaaS, Microservices) | Policy-as-code gate + SAST/SCA + automated remediation PRs | Maintains deployment frequency while enforcing security baselines | Moderate tooling cost; reduces hotfix and rollback expenses |
Configuration Template
# .github/workflows/security-gate.yml
name: Security Gate
on: [pull_request, push]
jobs:
security-evaluation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Run SAST (Semgrep)
run: semgrep --config=auto --json > sast-results.json
- name: Run SCA (npm audit)
run: npm audit --json > sca-results.json
- name: Run Secret Detection (gitleaks)
run: gitleaks detect --report-format json --report-path secrets-results.json
- name: Aggregate & Evaluate
run: node scripts/security-gate.js
env:
MAX_CRITICAL: 0
MAX_HIGH: 2
ALLOW_WARNINGS: false
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: sast-results.json
// scripts/security-gate.ts
import { SecurityGate } from '../src/security-gate';
import { readFileSync } from 'fs';
const parseToolOutput = (file: string, tool: string): Array<{ severity: string; count: number }> => {
const data = JSON.parse(readFileSync(file, 'utf-8'));
// Normalize tool-specific formats to unified structure
return data.results?.map((r: any) => ({
severity: r.severity || 'low',
count: 1,
id: r.check_id || r.vulnerabilityId || 'unknown',
file: r.file || r.filePath || 'unknown',
line: r.line || r.startLine || 0
})) || [];
};
const results = [
...parseToolOutput('sast-results.json', 'semgrep'),
...parseToolOutput('sca-results.json', 'npm-audit'),
...parseToolOutput('secrets-results.json', 'gitleaks')
];
const gate = new SecurityGate({
maxCritical: parseInt(process.env.MAX_CRITICAL || '0'),
maxHigh: parseInt(process.env.MAX_HIGH || '2'),
allowWarnings: process.env.ALLOW_WARNINGS === 'true'
});
const decision = gate.evaluate(results);
console.log(`[GATE] ${decision.status}: ${decision.message}`);
process.exit(decision.status === 'FAIL' ? 1 : 0);
Quick Start Guide
- Install baseline tools:
npm install -D semgrep @gitleaks/gitleaks (or use GitHub Actions marketplace equivalents)
- Create the security gate script: Copy the TypeScript gate engine into
scripts/security-gate.ts and compile to JS
- Add the workflow: Place the YAML template in
.github/workflows/ and adjust thresholds to match your risk tolerance
- Run locally: Execute
semgrep --config=auto --json > sast-results.json and node scripts/security-gate.js to validate policy evaluation
- Merge and monitor: Open a PR with a known vulnerable dependency or hardcoded secret. Verify the gate fails deterministically and produces actionable output
DevSecOps is not a toolchain; it is a delivery discipline. When policy evaluation is deterministic, feedback is immediate, and security metrics are treated as first-class engineering KPIs, pipelines stop being attack surfaces and start being assurance mechanisms.