Skip to content
DevSecOps Explained — Shift-Left Security for CI/CD Pipelines

DevSecOps Explained — Shift-Left Security for CI/CD Pipelines

DodaTech Updated Jun 7, 2026 10 min read

DevSecOps is the practice of integrating security controls and testing into every phase of the software development lifecycle — shifting security “left” so vulnerabilities are caught before they reach production.

What You’ll Learn

By the end of this tutorial, you’ll understand the DevSecOps philosophy, implement SAST and DAST scanning in a CI/CD pipeline, automate dependency vulnerability checks, manage secrets securely, and build a pipeline that blocks vulnerable code from reaching production.

Why DevSecOps Matters

Traditional security testing happens at the end of development — a “security gate” before release. This catches issues late when they’re expensive to fix. DevSecOps shifts security left, catching vulnerabilities during coding and building. The result: 50-80% fewer production vulnerabilities and significantly lower remediation costs. At DodaTech, Durga Antivirus Pro uses DevSecOps practices to ensure every signature update is scanned before reaching users.

DevSecOps Learning Path

    flowchart LR
  A[Security Basics] --> B[Network Security]
  B --> C[Web Security]
  C --> D[DevSecOps]
  D --> E{You Are Here}
  E --> F[Secure CI/CD Pipeline]
  style E fill:#f90,color:#fff
  
Prerequisites: Cyber Security basics, familiarity with CI/CD pipelines and version control (Git).

What Is DevSecOps? (The “Why” First)

Think of DevSecOps as installing a metal detector at the entrance of every room instead of only checking bags at the final exit. Traditional security checks everything at the end — deploy to staging, run security tests, fix issues, repeat. DevSecOps checks at every step: when you commit code, when you build, when you test, when you deploy.

The “shift left” concept means moving security activities earlier in the development timeline:

    flowchart LR
  subgraph "Traditional (Security at End)"
    A1[Code] --> B1[Build] --> C1[Test] --> D1[Deploy] --> E1[Security Scan]
  end
  subgraph "DevSecOps (Shift Left)"
    A2[Code + SAST] --> B2[Build + SCA] --> C2[Test + DAST] --> D2[Deploy + Secret Scan]
  end
  

Core DevSecOps Practices

SAST — Static Application Security Testing

SAST scans source code for security vulnerabilities without running the application. It’s a white-box approach — it analyzes the code structure.

What SAST catches:

  • SQL injection patterns
  • Cross-site scripting (XSS) in string concatenation
  • Hardcoded credentials
  • Insecure cryptographic algorithms
  • Buffer overflow risks

Example: Running a SAST scanner locally:

# sast_demo.py — Simulating a SAST scan on Python code
import ast
import re

class SimpleSAST:
    """A minimal SAST scanner that checks for common vulnerabilities."""

    PATTERNS = {
        "sql_injection": [
            r"execute\(f['\"]",
            r"cursor\.execute\(.*\+",
            r"\.execute\(\s*['\"]\s*SELECT.*\{",
        ],
        "hardcoded_password": [
            r"password\s*=\s*['\"][^'\"]+['\"]",
            r"passwd\s*=\s*['\"][^'\"]+['\"]",
            r"secret_key\s*=\s*['\"][^'\"]+['\"]",
        ],
        "eval_usage": [
            r"\beval\s*\(",
            r"\bexec\s*\(",
        ],
        "insecure_hash": [
            r"hashlib\.md5\b",
            r"hashlib\.sha1\b",
        ],
    }

    def scan_file(self, filepath: str) -> list[dict]:
        """Scan a Python file for security issues."""
        findings = []
        try:
            with open(filepath, 'r') as f:
                content = f.read()
                lines = content.split('\n')
        except FileNotFoundError:
            return [{"file": filepath, "error": "File not found"}]

        for vuln_type, patterns in self.PATTERNS.items():
            for pattern in patterns:
                for i, line in enumerate(lines, 1):
                    if re.search(pattern, line, re.IGNORECASE):
                        findings.append({
                            "file": filepath,
                            "line": i,
                            "type": vuln_type,
                            "severity": "HIGH" if vuln_type == "sql_injection" else "MEDIUM",
                            "snippet": line.strip()
                        })

        return findings

# Example scan
scanner = SimpleSAST()
results = scanner.scan_file("sample_app.py")

print("=== SAST Scan Results ===")
for r in results:
    print(f"[{r['severity']}] {r['type']} at {r['file']}:{r['line']}")
    print(f"  Code: {r['snippet']}")

if not results:
    print("No vulnerabilities found.")

DAST — Dynamic Application Security Testing

DAST scans a running application from the outside — it’s a black-box approach that doesn’t need source code access.

What DAST catches:

  • Authentication bypasses
  • Session management flaws
  • Exposed sensitive endpoints
  • Misconfigured CORS headers
  • TLS/SSL weaknesses
# dast_demo.py — Simulating a DAST scan on a web endpoint
import requests
import json

class SimpleDAST:
    """A minimal DAST scanner that tests common web vulnerabilities."""

    def __init__(self, base_url: str):
        self.base_url = base_url.rstrip('/')

    def test_sql_injection(self, endpoint: str, param: str) -> dict:
        """Test for SQL injection in a parameter."""
        payloads = ["'", "\"", "' OR '1'='1", "'; DROP TABLE users--"]
        results = []

        for payload in payloads:
            try:
                url = f"{self.base_url}{endpoint}?{param}={payload}"
                response = requests.get(url, timeout=5)

                # Check for error messages indicating SQL injection
                if any(indicator in response.text.lower() for indicator in
                       ["sql", "mysql", "syntax error", "unclosed quotation"]):
                    results.append({
                        "payload": payload,
                        "status": response.status_code,
                        "indicator": "SQL error in response",
                        "finding": "POSSIBLE SQL INJECTION"
                    })

            except requests.RequestException as e:
                results.append({
                    "payload": payload,
                    "error": str(e)
                })

        return {
            "endpoint": endpoint,
            "parameter": param,
            "results": results
        }

    def test_xss(self, endpoint: str, param: str) -> dict:
        """Test for XSS in a parameter."""
        payload = "<script>alert(1)</script>"
        results = []

        try:
            url = f"{self.base_url}{endpoint}?{param}={payload}"
            response = requests.get(url, timeout=5)

            # Check if payload is reflected unescaped
            if payload in response.text:
                results.append({
                    "payload": payload,
                    "finding": "REFLECTED XSS — payload reflected without escaping"
                })
        except requests.RequestException as e:
            results.append({"error": str(e)})

        return {"endpoint": endpoint, "results": results}

# Example usage (replace URL with your target)
# scanner = SimpleDAST("https://example.com")
# results = scanner.test_sql_injection("/search", "q")
# results = scanner.test_xss("/search", "q")

SCA — Software Composition Analysis

SCA scans third-party dependencies for known vulnerabilities using databases like the National Vulnerability Database (NVD) and GitHub Advisory Database.

# Using OWASP Dependency-Check (SCA tool)
# Scans dependencies for known CVEs

docker run --rm \
  -v $(pwd):/src \
  owasp/dependency-check \
  --scan /src \
  --format HTML \
  --out /src/reports

# Check for known vulnerabilities in npm packages
npm audit

# Check for known vulnerabilities in Python packages
pip-audit
# or
safety check

Example output of npm audit:

=== npm audit security report ===
# Run  npm install express@4.18.2  to resolve 1 vulnerability

Low            Prototype Pollution
Package        qs
Dependency of  express
Path           express > qs
More info      https://github.com/advisories/GHSA-xxx

Secrets Management

Never hardcode secrets in source code. Use secrets management tools:

# BAD — hardcoded in code
API_KEY = "sk-abc123def456"

# GOOD — use environment variables
import os
API_KEY = os.environ.get("API_KEY")

# BETTER — use a secrets manager
# AWS Secrets Manager, HashiCorp Vault, or GCP Secret Manager

# Using HashiCorp Vault from Python
import hvac

client = hvac.Client(url='https://vault.dodatech.io')
client.token = os.environ['VAULT_TOKEN']

secret = client.secrets.kv.read_secret_version(
    path='api-keys/production'
)
API_KEY = secret['data']['data']['api_key']

Building a DevSecOps Pipeline

Here’s a complete GitHub Actions workflow that bakes in security at every step:

# .github/workflows/devsecops.yml
name: DevSecOps Pipeline

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

jobs:
  security-checks:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4

    # 1. SAST — Static code analysis
    - name: SAST — Bandit (Python)
      run: |
        pip install bandit
        bandit -r . -f json -o bandit-report.json
    - name: Upload SAST Report
      uses: actions/upload-artifact@v4
      with:
        name: bandit-report
        path: bandit-report.json

    # 2. SCA — Dependency scanning
    - name: SCA — pip-audit
      run: |
        pip install pip-audit
        pip-audit --desc on -o pip-audit-report.json
    - name: Upload SCA Report
      uses: actions/upload-artifact@v4
      with:
        name: pip-audit-report
        path: pip-audit-report.json

    # 3. Secrets scanning
    - name: Secrets — truffleHog
      run: |
        docker run --rm -v "$(pwd):/repo" trufflesecurity/trufflehog:latest \
          filesystem /repo --json > trufflehog-report.json
    - name: Upload Secrets Report
      uses: actions/upload-artifact@v4
      with:
        name: trufflehog-report
        path: trufflehog-report.json

  build:
    needs: security-checks
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: Build
      run: docker build -t app:latest .

    # 4. Container scanning
    - name: Container Scan — Trivy
      run: |
        docker run --rm aquasec/trivy image \
          --severity CRITICAL,HIGH \
          --exit-code 1 \
          app:latest
    - name: Upload Container Scan
      uses: actions/upload-artifact@v4
      with:
        name: trivy-report
        path: trivy-report.json

  deploy-staging:
    needs: build
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: Deploy to Staging
      run: echo "Deploying to staging..."
    # 5. DAST — Dynamic scan against staging
    - name: DAST — OWASP ZAP
      run: |
        docker run --rm owasp/zap2docker-stable \
          zap-baseline.py -t https://staging.example.com -r zap-report.html
    - name: Upload ZAP Report
      uses: actions/upload-artifact@v4
      with:
        name: zap-report
        path: zap-report.html

DevSecOps Tools Reference

CategoryToolWhat It Checks
SASTBandit, Semgrep, SonarQube, CheckmarxSource code vulnerabilities
DASTOWASP ZAP, Burp Suite, AcunetixRunning app vulnerabilities
SCASnyk, Dependabot, OWASP DCDependency CVEs
SecretstruffleHog, GitLeaks, ggshieldHardcoded credentials
ContainerTrivy, Clair, AnchoreImage vulnerabilities
IaCCheckov, tfsec, TerrascanInfrastructure-as-Code misconfigs

Common DevSecOps Mistakes

1. Treating Security as a Gate, Not a Practice

If security only blocks the release, teams will resent it. Security should be a practice embedded in every sprint, not a wall at the end.

2. Too Many False Positives

SAST tools generate noise. Tune your rules and suppress known false positives. If developers see too many irrelevant alerts, they’ll ignore all of them.

3. Scanning Only at Build Time

Scan during development too. Pre-commit hooks catch issues before they reach the repo:

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: detect-private-key
  - repo: https://github.com/PyCQA/bandit
    rev: 1.7.8
    hooks:
      - id: bandit
        args: ["-r", "."]

4. Not Scanning Infrastructure as Code

Your Terraform and Kubernetes YAML files can have security misconfigurations too. Use Checkov or tfsec.

5. Ignoring Container Image Layers

A base image from 2022 may have CVEs. Always scan base images and rebuild regularly. Use minimal base images like alpine or distroless.

6. Secrets in Build Logs

Environment variables or build arguments can leak secrets into CI/CD logs. Mask secrets in log output.

7. No SBOM (Software Bill of Materials)

Without an SBOM, you don’t know what’s in your software. Generate one during build:

# Generate SPDX SBOM for a container image
docker run --rm anchore/syft alpine:latest -o spdx-json > sbom.json

Practice Questions

1. What does “shift left” mean in DevSecOps?

Moving security testing earlier in the development lifecycle — from a final gate to continuous checks during coding, building, and testing.

2. What’s the difference between SAST and DAST?

SAST (Static) scans source code without running it — catches issues early. DAST (Dynamic) scans a running application — catches runtime issues. They complement each other.

3. What does SCA scan for?

Software Composition Analysis scans third-party dependencies for known vulnerabilities (CVEs) using public vulnerability databases.

4. Why should secrets never be hardcoded in source code?

Hardcoded secrets in Git history are exposed to anyone with repo access. Even if removed later, they remain in the commit history. Use environment variables or a secrets manager.

5. Challenge: Set up a pre-commit hook that prevents committing code with print() statements (a common security risk for logging sensitive data).

Use a pre-commit hook with a regex check for print( or console.log( patterns. Reject the commit if found.

Mini Project: DevSecOps Dashboard

# devsecops_dashboard.py
# Aggregate scan results from multiple tools into one report
import json
import glob
from datetime import datetime

class DevSecOpsDashboard:
    """Aggregate security scan results into a summary."""

    def __init__(self):
        self.findings = {
            "sast": [],
            "dast": [],
            "sca": [],
            "secrets": [],
            "container": []
        }

    def load_bandit_report(self, path: str):
        """Load Bandit SAST results."""
        try:
            with open(path) as f:
                data = json.load(f)
                self.findings["sast"] = data.get("results", [])
        except (FileNotFoundError, json.JSONDecodeError):
            pass

    def summary(self) -> dict:
        """Generate a summary of all findings."""
        total = sum(len(v) for v in self.findings.values())
        by_severity = {"HIGH": 0, "MEDIUM": 0, "LOW": 0}

        for category, items in self.findings.items():
            for item in items:
                sev = item.get("issue_severity", item.get("severity", "LOW")).upper()
                if sev in by_severity:
                    by_severity[sev] += 1

        return {
            "timestamp": datetime.now().isoformat(),
            "total_findings": total,
            "by_category": {k: len(v) for k, v in self.findings.items()},
            "by_severity": by_severity,
            "pass": total == 0
        }

# Usage
dashboard = DevSecOpsDashboard()
dashboard.load_bandit_report("bandit-report.json")
print(json.dumps(dashboard.summary(), indent=2))

FAQ

Do I need a dedicated security team for DevSecOps?
No. DevSecOps is about automating security so developers can own it. A security champion in each team helps, but the tools do the heavy lifting.
What’s the minimum DevSecOps setup for a small team?
Pre-commit hooks (secrets, SAST), SCA on dependencies, container scanning in CI/CD. Start small and add tools as you scale.
How do I handle false positives?
Tune your rules, maintain an allowlist of known false positives, and have a process to mark findings as “reviewed/accepted.” Don’t let perfect be the enemy of good.
What is a Software Bill of Materials (SBOM)?
A machine-readable inventory of all components in your software — dependencies, versions, licenses. Generated during build, it helps track which components are affected when a new CVE is announced.
Can DevSecOps replace penetration testing?
No. DevSecOps catches common vulnerabilities automatically, but a skilled penetration tester finds logic flaws, business logic abuse, and zero-day patterns that automated tools miss. Use both.

Try It Yourself

Create a GitHub repository with a simple Flask app and add:

  1. A .pre-commit-config.yaml with Bandit and secrets checks
  2. A GitHub Actions workflow that runs SCA on push
  3. A Trivy scan in Docker build

Push a commit that introduces a SQL injection vulnerability and watch the pipeline catch it before it reaches production. This is the same DevSecOps pipeline used at DodaTech for Durga Antivirus Pro signature validation.

What’s Next

What’s Next

Congratulations on completing this DevSecOps tutorial! Here’s where to go from here:

  • Practice daily — Consistency is more important than long study sessions
  • Build a project — Apply what you learned by building something real
  • Explore related topics — Check out other tutorials in the same category
  • Join the community — Discuss with other learners and share your progress

Remember: every expert was once a beginner. Keep coding!

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro