~/cloud/building-security-pipeline.md

Building a Security Pipeline — DevSecOps in Practice

6 min readApril 04, 2026by Gorav Singal#dev-sec-ops#security-pipeline#sast
// TL;DR

Shift security left: run SAST (Semgrep) and secret scanning (Gitleaks) on every PR, SCA (Trivy) on every build, DAST (ZAP) in staging, and infra scanning (Checkov) on every Terraform change. Gate on critical findings only — don't block on noise.

Building a Security Pipeline — DevSecOps in Practice

Security tools that nobody runs are security theater. I’ve seen teams buy expensive SAST licenses that sit unused because the tool takes 45 minutes to scan and nobody wants to wait. The secret to a security pipeline that actually works: make it fast, make it automatic, and make it block only what matters.

This article walks through building a complete security pipeline from pre-commit hooks to runtime monitoring.

Why a Security Pipeline?

The economics are simple: fixing a vulnerability in production costs 100x more than fixing it in development. A security pipeline shifts detection left — catching issues when they’re cheapest to fix.

Security Pipeline Stages

But here’s the nuance most teams miss: a security pipeline that blocks every PR on every finding is worse than no pipeline at all. Developers will route around it. The goal is high-signal, low-friction security gates.

The Security Scanning Stack

Every application needs five types of security scanning:

Scan Type What It Finds When to Run Tool
SAST Code-level bugs (SQLi, XSS, hardcoded secrets) Every PR Semgrep
SCA Vulnerable dependencies Every build Trivy, Snyk
Secret Scanning Leaked API keys, passwords, tokens Pre-commit + PR Gitleaks
IaC Scanning Misconfigured infrastructure Every Terraform PR Checkov
DAST Runtime vulnerabilities Staging deploys OWASP ZAP

Security Tool Comparison Matrix

Integrating into CI/CD — The Full Pipeline

Here’s a complete GitHub Actions workflow that implements all five scan types:

# .github/workflows/security-pipeline.yml
name: Security Pipeline

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  # Stage 1: Fast scans on every PR (< 2 min)
  secret-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Gitleaks Secret Scan
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  sast:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Semgrep SAST Scan
        uses: returntocorp/semgrep-action@v1
        with:
          config: >-
            p/security-audit
            p/owasp-top-ten
            p/nodejs
          generateSarif: true

      - name: Upload SARIF
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: semgrep.sarif

  iac-scan:
    runs-on: ubuntu-latest
    if: contains(github.event.pull_request.changed_files, 'terraform/')
    steps:
      - uses: actions/checkout@v4

      - name: Checkov IaC Scan
        uses: bridgecrewio/checkov-action@v12
        with:
          directory: terraform/
          framework: terraform
          soft_fail: false
          skip_check: CKV_AWS_18  # Skip specific checks if needed

  # Stage 2: Build-time scans (< 5 min)
  sca:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Trivy Dependency Scan
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          scan-ref: '.'
          severity: 'CRITICAL,HIGH'
          exit-code: '1'
          format: 'sarif'
          output: 'trivy-results.sarif'

      - name: Upload Trivy SARIF
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: trivy-results.sarif

  image-scan:
    runs-on: ubuntu-latest
    needs: [secret-scan, sast]
    steps:
      - uses: actions/checkout@v4

      - name: Build Docker Image
        run: docker build -t app:${{ github.sha }} .

      - name: Trivy Image Scan
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'app:${{ github.sha }}'
          severity: 'CRITICAL,HIGH'
          exit-code: '1'

  # Stage 3: DAST on staging (post-deploy)
  dast:
    runs-on: ubuntu-latest
    needs: [image-scan]
    if: github.ref == 'refs/heads/main'
    steps:
      - name: Wait for staging deploy
        run: sleep 30  # Wait for deployment to complete

      - name: OWASP ZAP Baseline Scan
        uses: zaproxy/action-[email protected]
        with:
          target: 'https://staging.example.com'
          rules_file_name: '.zap/rules.tsv'
          cmd_options: '-a'

  # Quality Gate — aggregate results
  security-gate:
    runs-on: ubuntu-latest
    needs: [secret-scan, sast, sca, iac-scan]
    if: always()
    steps:
      - name: Check scan results
        run: |
          if [ "${{ needs.secret-scan.result }}" == "failure" ]; then
            echo "❌ Secret scan failed — blocking merge"
            exit 1
          fi
          if [ "${{ needs.sast.result }}" == "failure" ]; then
            echo "⚠️ SAST findings detected — review required"
            # Don't block, but require review
          fi
          if [ "${{ needs.sca.result }}" == "failure" ]; then
            echo "❌ Critical/High vulnerabilities found — blocking merge"
            exit 1
          fi
          echo "✅ Security gate passed"

Tool Deep Dives

Semgrep — SAST

Semgrep is my go-to for SAST because it’s fast (seconds, not minutes) and the rules are readable:

# .semgrep/custom-rules.yml
rules:
  - id: no-hardcoded-aws-keys
    patterns:
      - pattern-regex: "AKIA[0-9A-Z]{16}"
    message: "Hardcoded AWS access key detected"
    languages: [generic]
    severity: ERROR

  - id: no-eval-user-input
    patterns:
      - pattern: eval($X)
      - pattern-not: eval("...")
    message: "eval() with dynamic input — potential code injection"
    languages: [javascript, typescript]
    severity: ERROR

  - id: sql-injection-risk
    patterns:
      - pattern: |
          $QUERY = "..." + $INPUT + "..."
      - metavariable-regex:
          metavariable: $QUERY
          regex: ".*(SELECT|INSERT|UPDATE|DELETE).*"
    message: "SQL string concatenation — use parameterized queries"
    languages: [javascript, python]
    severity: ERROR

Gitleaks — Secret Scanning

# .gitleaks.toml
title = "Gitleaks Configuration"

[allowlist]
  paths = [
    '''\.test\.''',
    '''_test\.go''',
    '''testdata/''',
  ]

[[rules]]
  id = "aws-access-key"
  description = "AWS Access Key"
  regex = '''AKIA[0-9A-Z]{16}'''
  tags = ["aws", "credentials"]

[[rules]]
  id = "generic-api-key"
  description = "Generic API Key"
  regex = '''(?i)(api[_-]?key|apikey)\s*[:=]\s*['"][a-zA-Z0-9]{32,}['"]'''
  tags = ["api", "credentials"]

[[rules]]
  id = "private-key"
  description = "Private Key"
  regex = '''-----BEGIN (RSA |EC |DSA )?PRIVATE KEY-----'''
  tags = ["private-key"]

Checkov — Infrastructure Scanning

# Scan Terraform files
checkov -d terraform/ --framework terraform

# Scan with custom policy
checkov -d terraform/ \
  --external-checks-dir custom_policies/ \
  --check CKV_AWS_18,CKV_AWS_19,CKV_AWS_145

# Scan Kubernetes manifests
checkov -d k8s/ --framework kubernetes

# Scan Dockerfiles
checkov -f Dockerfile --framework dockerfile

Gating Strategy

This is where most teams go wrong. Here’s my approach:

Hard Gates (Block the PR)

  • Secrets detected — always block, no exceptions
  • Critical CVEs with known exploits — EPSS > 0.5
  • SQL injection / command injection — high-confidence SAST findings
  • Public S3 buckets in Terraform — infrastructure misconfigs

Soft Gates (Warn, Require Review)

  • High-severity CVEs without exploits — might be a false positive
  • Medium SAST findings — context-dependent
  • Deprecated dependency versions — track but don’t block

Skip (Don’t Even Report)

  • Info-level findings — noise that erodes trust
  • Findings in test files — different risk profile
  • Known false positives — maintain a suppression list
# .security/gate-config.yml
gates:
  hard_block:
    - secrets: all
    - sca:
        severity: [CRITICAL]
        epss_threshold: 0.5
    - sast:
        rules: [sql-injection, command-injection, ssrf]
        confidence: high
    - iac:
        checks: [CKV_AWS_18, CKV_AWS_19, CKV_AWS_20]

  soft_warn:
    - sca:
        severity: [HIGH]
    - sast:
        severity: [WARNING]

  suppress:
    - paths: ['**/test/**', '**/testdata/**']
    - ids: ['CVE-2023-XXXX']  # Known FP

Reducing False Positives

False positives are the #1 reason security pipelines fail. Developers stop trusting the tools and start ignoring findings.

Yes

No

Yes

No

Yes

No

Finding Reported

Is it real?

Is it exploitable?

Add to Suppress List

In production path?

Reduce Severity

Hard Block

Soft Warn

Reduce noise over time

Practical tips:

  1. Start in audit mode — report but don’t block for the first 2 weeks
  2. Tune aggressively — suppress false positives immediately with documented reasons
  3. Use SARIF — standardized format so all tools report to the same dashboard
  4. Track signal-to-noise ratio — if more than 20% of findings are false positives, tune more

Developer Experience

A security pipeline that annoys developers will be circumvented. Design for DX:

  • Fast — secret scanning and SAST should complete in under 2 minutes
  • Actionable — findings should include fix suggestions, not just vulnerability IDs
  • Inline — show findings as PR comments, not in a separate dashboard
  • Suppressible — let developers mark false positives with a comment and a reason
# Pre-commit hook for instant feedback
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.0
    hooks:
      - id: gitleaks

  - repo: https://github.com/returntocorp/semgrep
    rev: v1.50.0
    hooks:
      - id: semgrep
        args: ['--config', 'p/security-audit', '--error']

Key Takeaways

  1. Five scan types, one pipeline — SAST, SCA, secrets, IaC, and DAST cover your bases
  2. Gate wisely — hard block on secrets and critical exploitable CVEs; warn on everything else
  3. Speed matters — if your security scan takes 10+ minutes, developers will hate it
  4. False positives kill pipelines — tune aggressively, maintain suppression lists
  5. Start in audit mode — collect data before blocking
  6. Automate the boring stuff — secret scanning and SCA should be fully automated

A well-tuned security pipeline catches real issues without slowing down development. It’s the foundation that makes all the other security practices in this course sustainable.

$ subscribe --weekly

One deep dive a week, in your inbox

System design, AI engineering, trading systems and interview prep. Tested code, real numbers. Free, and you can unsubscribe any time.

or read it on gyanbyte.substack.com

Share

Related Posts

Dependency Vulnerability Detection at Scale

Dependency Vulnerability Detection at Scale

The average application has over 200 transitive dependencies. Each one is code…

Supply Chain Security — Protecting Your Software Pipeline

Supply Chain Security — Protecting Your Software Pipeline

In 2024, a single malicious contributor nearly compromised every Linux system on…

Security Ticketing and Incident Response

Security Ticketing and Incident Response

The worst time to figure out your incident response process is during an…

Security Mindset for Engineers — Think Like an Attacker

Security Mindset for Engineers — Think Like an Attacker

Most engineers think about security the way they think about flossing — they…

Secrets Management — Vault, SSM, and Secrets Manager

Secrets Management — Vault, SSM, and Secrets Manager

I’ve watched a production database get wiped because someone committed a root…

Penetration Testing Basics for Developers

Penetration Testing Basics for Developers

Most developers think of penetration testing as something a separate security…

Latest Posts

How to Stay Valuable as a Software Engineer in the AI Era: A Practical Playbook

How to Stay Valuable as a Software Engineer in the AI Era: A Practical Playbook

In the previous article I argued that AI is not killing software engineering…

How to Review AI-Generated Code: A Checklist for Catching What the Model Missed

How to Review AI-Generated Code: A Checklist for Catching What the Model Missed

A few months back I approved a PR that was almost entirely AI-generated. It…

How to Debug AI-Generated Code: Understand First, Fix Last

How to Debug AI-Generated Code: Understand First, Fix Last

In the last article I wrote about reviewing AI code before it goes in. This one…

The Future of Software Engineering: What AI Changes, What Stays, and Where the Jobs Go

The Future of Software Engineering: What AI Changes, What Stays, and Where the Jobs Go

Every few weeks somebody sends me a scary headline — “AI will replace all…

The Transactional Outbox Pattern: Never Lose an Event Again

The Transactional Outbox Pattern: Never Lose an Event Again

This is one of those patterns that looks like nothing until you get burnt once…

Event Sourcing, Explained: Store What Happened, Not Just the Final State

Event Sourcing, Explained: Store What Happened, Not Just the Final State

Here is a question I like to ask in interviews: your bank account shows a…