A vulnerability found while writing code costs a developer minutes to fix. The same vulnerability caught in production costs a sprint. And if an attacker finds it first, it costs millions. This is the core argument behind shift-left security - moving security checks as early as possible in the development lifecycle.
DevSecOps takes this idea and turns it into a practice: security is not a separate phase at the end but a continuous process woven into every stage of development, from the first line of code to the production deployment.
The Cost of Late Security
IBM's Cost of a Data Breach Report has consistently shown that the cost of fixing security issues grows exponentially the later they are caught:
| Stage | Cost to Fix | Time to Fix |
|---|---|---|
| IDE / Local Dev | Minutes | Seconds to minutes |
| Code Review / PR | Hours | Minutes to hours |
| CI/CD Pipeline | Days | Hours to days |
| Staging / QA | Weeks | Days |
| Production | Months | Weeks to months |
| Post-breach | Millions ($) | Months to years |
The takeaway is clear: every stage you push security earlier saves an order of magnitude in cost and time.
The DevSecOps Pipeline
A mature DevSecOps pipeline integrates security checks at every stage:
Let's break down each stage with concrete tools and configuration.
Stage 1: Security in the IDE
The fastest feedback loop. Catch vulnerabilities before you even save the file.
Recommended Tools
- Semgrep: lightweight static analysis with community rules for OWASP vulnerabilities
- Snyk IDE Extension: real-time dependency vulnerability scanning
- GitLens + GitLeaks: detect secrets in your editor
- ESLint Security Plugins:
eslint-plugin-securityfor Node.js,eslint-plugin-no-unsanitizedfor DOM XSS
Example: ESLint Security Configuration
{ "extends": ["eslint:recommended"], "plugins": ["security", "no-unsanitized"], "rules": { "security/detect-object-injection": "warn", "security/detect-non-literal-regexp": "warn", "security/detect-unsafe-regex": "error", "security/detect-buffer-noassert": "error", "security/detect-eval-with-expression": "error", "security/detect-no-csrf-before-method-override": "error", "security/detect-possible-timing-attacks": "warn", "no-unsanitized/method": "error", "no-unsanitized/property": "error" } }
Stage 2: Pre-commit Hooks
The second line of defense. Runs automatically before every commit, blocking dangerous code from entering the repository.
Gitleaks: Catch Secrets Before They Hit Git
The most common security mistake in codebases is committing secrets - API keys, database passwords, tokens. Once a secret hits git history, it is extremely hard to remove completely (even with force pushes, forks and caches may retain it).
# .pre-commit-config.yaml repos: - repo: https://github.com/gitleaks/gitleaks rev: v8.21.0 hooks: - id: gitleaks - repo: https://github.com/semgrep/semgrep rev: v1.90.0 hooks: - id: semgrep args: ['--config', 'auto']
Install and activate:
pip install pre-commit pre-commit install
Now every git commit automatically scans for leaked secrets and common vulnerabilities. If anything is found, the commit is blocked.
Custom Gitleaks Rules
You can add custom patterns for your organization's secrets:
# .gitleaks.toml title = "Custom Gitleaks Config" [[rules]] id = "internal-api-key" description = "Internal API key detected" regex = '''INTERNAL_KEY_[A-Za-z0-9]{32}''' tags = ["key", "internal"]
Stage 3: CI/CD Pipeline Security
This is where the heavy lifting happens. Your CI pipeline should run multiple security scans on every pull request.
SAST (Static Application Security Testing)
SAST tools analyze source code without executing it, looking for patterns that indicate vulnerabilities.
# .github/workflows/security.yml name: Security Scan on: pull_request: branches: [main] jobs: sast: name: Static Analysis runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run Semgrep uses: semgrep/semgrep-action@v1 with: config: >- p/owasp-top-ten p/typescript p/nodejs p/react generateSarif: true - name: Upload SARIF uses: github/codeql-action/upload-sarif@v3 with: sarif_file: semgrep.sarif
Semgrep's p/owasp-top-ten ruleset catches the most common vulnerabilities: SQL injection, XSS, SSRF, path traversal, insecure deserialization, and more.
SCA (Software Composition Analysis)
SCA scans your dependencies for known vulnerabilities. This is critical - over 80% of modern application code comes from open-source dependencies.
dependency-scan: name: Dependency Audit runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run Snyk uses: snyk/actions/node@master env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} with: args: --severity-threshold=high - name: npm audit run: npm audit --audit-level=high
Container Security with Trivy
If you build Docker images, scanning them for vulnerabilities is essential. Trivy is the most popular open-source container scanner.
container-scan: name: Container Security runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build image run: docker build -t my-app:${{ github.sha }} . - name: Run Trivy uses: aquasecurity/trivy-action@master with: image-ref: my-app:${{ github.sha }} format: 'sarif' output: 'trivy-results.sarif' severity: 'CRITICAL,HIGH' exit-code: '1' - name: Upload Trivy SARIF uses: github/codeql-action/upload-sarif@v3 with: sarif_file: trivy-results.sarif
SBOM Generation
A Software Bill of Materials (SBOM) is a complete inventory of every component in your application. Increasingly required by compliance frameworks and government regulations (the US Executive Order on Cybersecurity mandates SBOM for federal software).
sbom: name: Generate SBOM runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Generate SBOM with Syft uses: anchore/sbom-action@v0 with: format: spdx-json output-file: sbom.spdx.json - name: Upload SBOM uses: actions/upload-artifact@v4 with: name: sbom path: sbom.spdx.json
Stage 4: Secure Docker Images
A production Docker image should follow the principle of least privilege. This is what a hardened Dockerfile looks like:
# Build stage FROM node:22-alpine AS builder WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci COPY . . RUN npm run build # Production stage FROM node:22-alpine AS runner WORKDIR /app # Install dumb-init before dropping root RUN apk add --no-cache dumb-init # Don't run as root RUN addgroup -S app && adduser -S app -G app # Copy only what's needed COPY /app/dist ./dist COPY /app/node_modules ./node_modules COPY /app/package.json ./ # Drop to non-root user USER app ENTRYPOINT ["dumb-init", "--"] # Health check HEALTHCHECK \ CMD wget -qO- http://localhost:3000/health || exit 1 EXPOSE 3000 CMD ["node", "dist/server.js"]
Key practices:
- Use multi-stage builds: the builder stage has dev dependencies; the runner stage has only production code
- Don't run as root: create a non-root user and switch to it
- Use Alpine images: smaller attack surface (fewer packages installed by default)
- Pin image versions:
node:22-alpineinstead ofnode:latestto avoid supply chain attacks - Use
npm ci: deterministic installs from lock file, notnpm install
Stage 5: Secret Management
Hard-coded secrets are the number one cause of breaches in developer-caused incidents.
What NOT to Do
// NEVER do this const API_KEY = "sk-1234567890abcdef"; const DB_PASSWORD = "supersecret123"; const client = new Client({ connectionString: `postgres://admin:${DB_PASSWORD}@db.example.com/prod` });
What to Do Instead
// Use environment variables const client = new Client({ connectionString: process.env.DATABASE_URL }); // Or use a secret manager import { SecretManagerServiceClient } from '@google-cloud/secret-manager'; const client = new SecretManagerServiceClient(); const [version] = await client.accessSecretVersion({ name: 'projects/my-project/secrets/db-password/versions/latest', }); const dbPassword = version.payload?.data?.toString();
Secret Management Hierarchy
The OWASP Top 10: A Quick Reference
Every developer should know the OWASP Top 10. Condensed version:
| # | Vulnerability | What It Is | Prevention |
|---|---|---|---|
| 1 | Broken Access Control | Users accessing resources they shouldn't | Deny by default, validate on server side |
| 2 | Cryptographic Failures | Weak encryption, plaintext data | Use strong algorithms (AES-256, bcrypt), TLS everywhere |
| 3 | Injection | SQL, NoSQL, OS command injection | Parameterized queries, input validation |
| 4 | Insecure Design | Flawed architecture | Threat modeling, secure design patterns |
| 5 | Security Misconfiguration | Default credentials, open cloud buckets | Hardened defaults, automated config audits |
| 6 | Vulnerable Components | Known CVEs in dependencies | SCA scanning, regular updates |
| 7 | Auth Failures | Weak passwords, broken sessions | MFA, rate limiting, secure session management |
| 8 | Data Integrity Failures | Unsigned updates, untrusted CI/CD | Code signing, SBOM, pipeline integrity |
| 9 | Logging Failures | No audit trail | Structured logging, alerting on anomalies |
| 10 | SSRF | Server-side request forgery | Allowlist outbound URLs, validate inputs |
Complete GitHub Actions Security Workflow
A complete, production-ready workflow that combines everything above:
# .github/workflows/security.yml name: Security Pipeline on: pull_request: branches: [main] push: branches: [main] permissions: contents: read security-events: write jobs: secrets-scan: name: Secret Detection runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: gitleaks/gitleaks-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} sast: name: Static Analysis (SAST) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: semgrep/semgrep-action@v1 with: config: p/owasp-top-ten p/typescript p/nodejs dependency-audit: name: Dependency Scan (SCA) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - run: npm ci - run: npm audit --audit-level=high - uses: snyk/actions/node@master env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} with: args: --severity-threshold=high continue-on-error: true container-scan: name: Container Scan runs-on: ubuntu-latest needs: [sast, dependency-audit] steps: - uses: actions/checkout@v4 - run: docker build -t app:${{ github.sha }} . - uses: aquasecurity/trivy-action@master with: image-ref: app:${{ github.sha }} severity: CRITICAL,HIGH exit-code: '1' sbom: name: SBOM Generation runs-on: ubuntu-latest needs: [container-scan] steps: - uses: actions/checkout@v4 - uses: anchore/sbom-action@v0 with: format: spdx-json output-file: sbom.spdx.json
Metrics to Track
How do you know your DevSecOps program is working? Track these metrics:
- Mean time to remediate (MTTR): how fast you fix vulnerabilities after detection
- Vulnerability escape rate: percentage of vulnerabilities that reach production
- False positive rate: too many false positives lead to alert fatigue and ignored warnings
- Dependency freshness: average age of your dependencies (older = more likely to have known CVEs)
- SBOM coverage: percentage of projects with up-to-date SBOMs
Getting Started: A Practical Roadmap
Don't try to implement everything at once. A phased approach works better:
Conclusion
DevSecOps is not about adding more tools to your pipeline - it is about making security a natural part of how you build software. The goal is not to block every PR with security warnings but to give developers fast feedback so they can fix issues while the code is still fresh in their minds.
Start with the basics: pre-commit hooks for secrets, dependency scanning in CI, and container scanning for Docker images. Then iterate based on what your team needs.
Security is not a feature you ship once. It is a practice you build into every commit.
DevSecOps Starter Checklist:
- Gitleaks pre-commit hooks installed
- .env and secret files in .gitignore
- Semgrep SAST in CI pipeline
- Snyk or npm audit for dependency scanning
- Trivy for container image scanning
- Non-root user in Dockerfiles
- Secrets in environment variables or secret manager
- SBOM generation on every release
- OWASP Top 10 awareness across the team