Shift-Left Security in CI/CD Pipelines: A Complete Implementation Guide

Shift-left security integrates security testing early in the software development lifecycle, catching vulnerabilities before they reach production. By embedding security into CI/CD pipelines, teams can identify and fix issues when they’re cheapest to remediate.

Shift-Left Security Stages

  • Pre-commit: Secrets scanning, linting
  • Build: SAST, dependency scanning
  • Test: DAST, container scanning
  • Deploy: IaC scanning, compliance checks

Pre-commit Hooks

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.0
    hooks:
      - id: gitleaks

  - repo: https://github.com/hadolint/hadolint
    rev: v2.12.0
    hooks:
      - id: hadolint-docker

  - repo: https://github.com/bridgecrewio/checkov
    rev: 3.0.0
    hooks:
      - id: checkov
        args: ['--framework', 'terraform']

  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
      - id: detect-private-key
      - id: check-added-large-files

GitHub Actions Security Pipeline

name: Security Pipeline
on: [push, pull_request]

jobs:
  secrets-scan:
    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:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Semgrep
        uses: returntocorp/semgrep-action@v1
        with:
          config: p/default

  dependency-scan:
    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

  container-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build image
        run: docker build -t app:${{ github.sha }} .
      - name: Run Trivy
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: app:${{ github.sha }}
          severity: CRITICAL,HIGH
          exit-code: 1

  iac-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Checkov
        uses: bridgecrewio/checkov-action@master
        with:
          directory: terraform/
          soft_fail: false

GitLab CI Security

# .gitlab-ci.yml
include:
  - template: Security/SAST.gitlab-ci.yml
  - template: Security/Dependency-Scanning.gitlab-ci.yml
  - template: Security/Container-Scanning.gitlab-ci.yml
  - template: Security/Secret-Detection.gitlab-ci.yml

stages:
  - build
  - test
  - security
  - deploy

sast:
  stage: security
  variables:
    SAST_EXCLUDED_PATHS: "spec, test, tests"

container_scanning:
  stage: security
  variables:
    CS_SEVERITY_THRESHOLD: HIGH

dependency_scanning:
  stage: security
  variables:
    DS_EXCLUDED_ANALYZERS: "gemnasium-python"

Quality Gates

# SonarQube quality gate check
name: Quality Gate
on: [push]

jobs:
  sonarqube:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
          
      - name: SonarQube Scan
        uses: sonarsource/sonarqube-scan-action@master
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
          SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
          
      - name: Quality Gate Check
        uses: sonarsource/sonarqube-quality-gate-action@master
        timeout-minutes: 5
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

Security Findings Dashboard

# DefectDojo integration
import requests

def upload_scan_results(scan_type, file_path, engagement_id):
    url = "https://defectdojo.company.com/api/v2/import-scan/"
    headers = {"Authorization": f"Token {DEFECTDOJO_TOKEN}"}
    
    with open(file_path, 'rb') as f:
        data = {
            'engagement': engagement_id,
            'scan_type': scan_type,
            'active': True,
            'verified': False
        }
        files = {'file': f}
        response = requests.post(url, headers=headers, data=data, files=files)
        return response.json()

Conclusion

Shift-left security transforms security from a gate at the end of development to a continuous process throughout the SDLC. By automating security testing in CI/CD pipelines, teams catch vulnerabilities early when they’re easiest and cheapest to fix.