Security has long been the afterthought bolted on at the end of the software development lifecycle — the dreaded "security review" that happens right before production and inevitably delays releases. That era is over.
In 2026, DevSecOps isn't a buzzword. It's the baseline expectation. Organizations that treat security as a shared responsibility — embedded directly into development workflows — ship faster, safer, and with far fewer costly breaches.
This guide walks you through three pillars of a modern secure pipeline: SAST, DAST, and SBOMs. Whether you're just getting started or looking to harden an existing pipeline, you'll leave with actionable patterns, real-world examples, and working configurations.
What Is "Shifting Security Left"?
"Shifting left" means moving security checks earlier in the software development lifecycle — ideally, as early as a developer's local machine or the first CI pipeline stage.
The logic is simple: the earlier you find a vulnerability, the cheaper it is to fix.
A bug caught during code review costs a few minutes. The same bug caught post-deployment can cost millions in incident response, regulatory fines, and lost user trust.
Traditional: Code → Build → Test → Deploy → [Security Audit] → Prod
Shift Left: [Security] → Code → [Security] → Build → [Security] → Deploy → Prod
Security becomes a continuous activity, not a checkpoint.
The Three Pillars: SAST, DAST, and SBOMs
🔍 SAST — Static Application Security Testing
SAST tools analyze your source code, bytecode, or binaries without executing the application. Think of it as a security-aware linter that understands vulnerability patterns like SQL injection, hardcoded secrets, insecure deserialization, and more.
When it runs: During development and on every pull request or commit.
What it catches:
- Injection vulnerabilities (SQL, command, LDAP)
- Hardcoded credentials and API keys
- Insecure use of cryptographic functions
- Path traversal and unsafe file operations
- Cross-site scripting (XSS) in server-rendered templates
Popular SAST tools:
| Tool | Language Support | Notes |
|---|---|---|
| Semgrep | 30+ languages | Fast, highly customizable rules |
| CodeQL | 10+ languages | Deep semantic analysis by GitHub |
| SonarQube | 30+ languages | Full platform with dashboards |
| Bandit | Python | Lightweight, fast |
| Checkmarx | Enterprise | Comprehensive with IDE plugins |
SAST in a GitHub Actions Pipeline
Here's a practical example using Semgrep in a GitHub Actions workflow:
# .github/workflows/sast.yml
name: SAST - Semgrep Scan
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
semgrep:
name: Static Security Scan
runs-on: ubuntu-latest
container:
image: semgrep/semgrep
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run Semgrep
run: |
semgrep ci \
--config=auto \
--sarif \
--output=semgrep-results.sarif
env:
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
- name: Upload SARIF results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: semgrep-results.sarif
if: always()
This uploads findings directly to GitHub's Security tab in SARIF format, so your team sees vulnerabilities inline with pull requests.
🌐 DAST — Dynamic Application Security Testing
DAST tools test your running application by sending real HTTP requests and analyzing responses. Unlike SAST, DAST doesn't care about your source code — it attacks the application the same way a malicious user would.
When it runs: Against a staging or ephemeral environment after deployment, but before production.
What it catches:
- Authentication and session management flaws
- Broken access control
- Server misconfigurations
- Runtime injection vulnerabilities
- Security headers missing or misconfigured
Popular DAST tools:
| Tool | Type | Notes |
|---|---|---|
| OWASP ZAP | Open source | Industry standard, CI-friendly |
| Burp Suite | Commercial | Best-in-class for manual + automated |
| Nuclei | Open source | Template-based, blazing fast |
| StackHawk | SaaS | Developer-friendly, CI-native |
DAST with OWASP ZAP in CI/CD
# .github/workflows/dast.yml
name: DAST - OWASP ZAP Scan
on:
push:
branches: [main]
jobs:
zap-scan:
name: Dynamic Security Scan
runs-on: ubuntu-latest
services:
app:
image: your-org/your-app:latest
ports:
- 3000:3000
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Wait for app to be ready
run: |
timeout 60 bash -c 'until curl -s http://localhost:3000/health; do sleep 2; done'
- name: Run ZAP Baseline Scan
uses: zaproxy/action-baseline@v0.12.0
with:
target: "http://localhost:3000"
rules_file_name: ".zap/rules.tsv"
cmd_options: "-a"
fail_action: true
- name: Upload ZAP Report
uses: actions/upload-artifact@v4
with:
name: zap-report
path: report_html.html
if: always()
Tip: Use a
.zap/rules.tsvfile to tune which alerts fail the build versus warn. Not every finding should be a pipeline blocker — context matters.
📦 SBOMs — Software Bill of Materials
An SBOM is a formal, machine-readable inventory of every component in your software: open-source libraries, their versions, licenses, and known vulnerabilities.
Think of it like a nutritional label for your software. You need to know what's in the package — especially when a new CVE drops and you have 10 minutes to answer "are we affected?"
In 2025, SBOMs became a regulatory requirement for software sold to the U.S. federal government (via Executive Order 14028) and are increasingly mandated in the EU under the Cyber Resilience Act.
Common SBOM formats:
- SPDX — Linux Foundation standard, widely adopted
- CycloneDX — OWASP-led, richer security metadata, gaining ground fast
Popular SBOM tools:
| Tool | Format | Notes |
|---|---|---|
| Syft | SPDX, CycloneDX | Multi-ecosystem, fast |
| cdxgen | CycloneDX | Deep language support |
| Trivy | SPDX, CycloneDX | Vulnerability scanning + SBOM |
| grype | — | Vuln scanning from Syft SBOMs |
Generating and Scanning an SBOM in CI
# .github/workflows/sbom.yml
name: SBOM Generation and Vulnerability Scan
on:
push:
branches: [main]
release:
types: [published]
jobs:
sbom:
name: Generate SBOM & Scan
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Syft
uses: anchore/sbom-action/download-syft@v0
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
format: cyclonedx-json
output-file: sbom.cyclonedx.json
artifact-name: sbom-cyclonedx
- name: Scan SBOM for Vulnerabilities with Grype
uses: anchore/scan-action@v3
with:
sbom: sbom.cyclonedx.json
fail-build: true
severity-cutoff: high
- name: Attach SBOM to Release
uses: softprops/action-gh-release@v2
if: github.event_name == 'release'
with:
files: sbom.cyclonedx.json
This workflow generates a CycloneDX SBOM, scans it for known CVEs using Grype, fails the build on HIGH or CRITICAL findings, and attaches the SBOM to GitHub releases automatically.
Putting It All Together: A Complete DevSecOps Pipeline
Here's how all three layers fit into a unified pipeline:
┌─────────────────────────────────────────────────────────────────┐
│ CI/CD SECURE PIPELINE │
├────────────┬──────────────┬─────────────────┬───────────────────┤
│ PRE-COMMIT│ BUILD │ STAGING/TEST │ RELEASE │
│ │ │ │ │
│ • Secrets │ • SAST scan │ • DAST scan │ • SBOM attached │
│ scanning │ (Semgrep, │ (ZAP, Nuclei) │ to artifact │
│ (gitleaks│ CodeQL) │ │ │
│ trufflehg│ • Dependency │ • API security │ • Sign artifact │
│ ) │ audit │ testing │ (Sigstore/ │
│ │ • SBOM gen │ │ cosign) │
│ • Lint & │ (Syft) │ • Container │ │
│ SAST │ │ scanning │ • Publish SBOM │
│ (fast) │ • License │ │ to registry │
│ │ check │ │ │
└────────────┴──────────────┴─────────────────┴───────────────────┘
Secrets Scanning: The Pre-Commit Layer
Before SAST even runs, stop secrets from entering your codebase with gitleaks or trufflehog:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.1
hooks:
- id: gitleaks
name: Detect hardcoded secrets
Install it locally with:
pip install pre-commit
pre-commit install
Now every git commit runs the secret scan before it even reaches your remote.
Real-World Example: Securing a Node.js API Pipeline
Let's say you have a Node.js REST API. Here's a complete, annotated pipeline covering all layers:
# .github/workflows/secure-pipeline.yml
name: Secure CI Pipeline
on:
pull_request:
branches: [main]
push:
branches: [main]
env:
NODE_VERSION: "22"
IMAGE_NAME: my-api
jobs:
# ── Layer 1: SAST ────────────────────────────────
sast:
name: Static Analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
- name: Install dependencies
run: npm ci
- name: npm audit (dependency vulnerabilities)
run: npm audit --audit-level=high
- name: Semgrep SAST scan
uses: semgrep/semgrep-action@v1
with:
config: >-
p/nodejs
p/secrets
p/owasp-top-ten
env:
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
# ── Layer 2: Build & SBOM ────────────────────────
build:
name: Build & Generate SBOM
runs-on: ubuntu-latest
needs: sast
outputs:
image-digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v4
- name: Build Docker image
id: build
uses: docker/build-push-action@v6
with:
context: .
push: false
tags: ${{ env.IMAGE_NAME }}:${{ github.sha }}
outputs: type=docker,dest=/tmp/image.tar
- name: Generate container SBOM
uses: anchore/sbom-action@v0
with:
image: ${{ env.IMAGE_NAME }}:${{ github.sha }}
format: cyclonedx-json
output-file: sbom.json
- name: Scan SBOM for CVEs
uses: anchore/scan-action@v3
with:
sbom: sbom.json
fail-build: true
severity-cutoff: high
- name: Upload SBOM artifact
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.json
# ── Layer 3: DAST ────────────────────────────────
dast:
name: Dynamic Analysis
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Deploy to ephemeral staging
run: |
# Pull and run the built image (or deploy to staging env)
docker load < /tmp/image.tar
docker run -d -p 3000:3000 \
--name staging-app \
${{ env.IMAGE_NAME }}:${{ github.sha }}
sleep 5 # Wait for startup
- name: Run ZAP API scan
uses: zaproxy/action-api-scan@v0.7.0
with:
target: "http://localhost:3000"
format: openapi
api_scan_file: openapi.yaml
fail_action: true
cmd_options: "-l WARN"
- name: Cleanup
run: docker stop staging-app && docker rm staging-app
if: always()
Best Practices
✅ Do These
- Fail fast, fail loud. Block PRs on
CRITICALandHIGHfindings. Warn onMEDIUM. LogLOW. - Tune your tools. A scanner that generates 500 false positives will be ignored. Invest time in baseline rules.
- Treat security findings like bugs. Create tickets, assign owners, set SLAs. Don't let findings rot.
- Sign your artifacts. Use Sigstore/cosign to cryptographically sign container images and SBOMs.
- Store SBOMs with every release. They're useless if you generate them but don't keep them.
- Use ephemeral environments for DAST. Never run active scanners against production.
- Rotate secrets regularly and use a secrets manager (Vault, AWS Secrets Manager, 1Password Secrets Automation) — never environment variables in code.
❌ Common Mistakes
- Running DAST against production. Scanners generate real traffic. They can trigger alerts, corrupt data, or — in worst cases — cause outages. Always target a staging environment.
- Ignoring license compliance. SBOMs reveal GPL-licensed dependencies you may not be allowed to ship commercially. Check licenses automatically with tools like FOSSA or license-checker.
- Only scanning on main. By the time code reaches
main, it's already been reviewed. Scan on every PR and every commit. - Not updating scanner rule sets. SAST tools are only as good as their rules. Keep them updated — new vulnerability patterns emerge constantly.
- Treating DevSecOps as a tooling problem. Tools are 30% of the solution. The other 70% is culture: developers owning security, security teams enabling developers, and leadership measuring security outcomes.
- Skipping developer education. A scanner that flags a SQL injection is useless if the developer doesn't understand why it's dangerous or how to fix it properly.
🚀 Pro Tips
💡 Use policy-as-code for consistent enforcement. Tools like Open Policy Agent (OPA) or Kyverno let you define security policies declaratively and enforce them consistently across pipelines and Kubernetes clusters.
💡 Correlate SAST + DAST findings. A vulnerability flagged by both SAST and DAST is almost certainly real. Build a process to prioritize correlated findings — they're your highest-confidence, highest-priority items.
💡 Implement a "break glass" process. Sometimes you need to deploy despite a finding (zero-day patch, critical business need). Implement a documented override process with required approvals and automatic ticket creation — so bypasses are visible, not invisible.
💡 Generate SBOMs at build time, not post-hoc.
Generating SBOMs from source is more accurate than scanning a deployed artifact. Use tools like cdxgen with your build system for language-native dependency resolution.
💡 Leverage GitHub Advanced Security or GitLab Ultimate. Both platforms have native SAST, secret scanning, and dependency review built in. If you're already on these platforms, enable them first before adding external tools.
💡 Track your Mean Time to Remediate (MTTR) for security findings. Security is measurable. Tracking MTTR across severity levels tells you whether your DevSecOps program is actually improving outcomes — or just generating reports.
📌 Key Takeaways
-
Shifting left means embedding security at every stage of the SDLC, starting from the developer's machine — not just before release.
-
SAST analyzes your source code without running it, catching issues like injection flaws, insecure crypto, and hardcoded secrets early in development.
-
DAST tests your running application the way an attacker would, finding runtime vulnerabilities that static analysis can't see.
-
SBOMs give you a complete, auditable inventory of your software's components — essential for vulnerability response, license compliance, and regulatory requirements.
-
All three are complementary. SAST + DAST + SBOM together provide defense-in-depth across the pipeline. No single tool catches everything.
-
Culture beats tooling. DevSecOps succeeds when security becomes a shared responsibility, not a gated function owned by a separate team.
-
Start small, iterate fast. Don't try to implement everything at once. Add SAST to PRs first, then SBOMs to builds, then DAST to staging. Incrementally raise the bar.
Conclusion
The shift from "security at the end" to "security everywhere" isn't just a technical change — it's a cultural one. But the technical foundation matters enormously. SAST gives you early feedback in the IDE and on every commit. DAST gives you real-world attack simulation against running code. SBOMs give you transparency and traceability across your entire dependency graph.
Together, they form a layered security posture that catches different classes of vulnerabilities at different stages — while keeping your pipeline fast enough that developers actually embrace it rather than route around it.
The best security program is one developers trust, understand, and contribute to. Start there.