Why Shift-Left Security Matters

Finding a vulnerability in production costs roughly 30x more to fix than catching it in development. The DevSecOps principle — shift security left into the development pipeline — is an economic argument as much as a technical one.

At Dell, I worked on embedding security tooling into GitLab CI/CD pipelines for internal microservices. This post covers what we learned, what failed, and the config patterns that actually stuck.

The Pipeline Before and After

Before: build → test → deploy. After: build → sast → dependency-scan → container-scan → dast → deploy.

SAST — Semgrep and Bandit

We used Semgrep for multi-language static analysis and Bandit for Python-specific checks. The critical config decision: fail the pipeline on HIGH severity findings only. Failing on MEDIUM immediately created alert fatigue — developers started ignoring pipeline failures entirely, which is worse than no SAST at all.

sast:
  stage: sast
  script:
    - semgrep --config=auto --severity=ERROR --error .
    - bandit -r . -ll  # only HIGH severity
  allow_failure: false

Lesson: a pipeline that always fails teaches developers to ignore failures. Tune your severity thresholds before rolling out to teams.

Dependency Scanning — Trivy

We evaluated Dependabot, Snyk, and Trivy. Trivy won for our use case — open source, fast, and scans container images as well as package manifests in a single pass. The gotcha: Trivy's CVE database updates daily. We added --ignore-unfixed to only block on CVEs that have a fix available — otherwise you're blocking deploys for vulnerabilities with no remediation path.

Container Scanning

Base image choice matters enormously. Switching from python:3.11 to python:3.11-slim reduced our critical CVE count from 47 to 3 in a single change. Most CVEs live in packages you never use. Use minimal base images.

DAST — The Hard Part

Dynamic Application Security Testing requires a running instance of your app. We used OWASP ZAP in API scan mode against a staging environment. ZAP's active scan takes 20+ minutes on large APIs — run it on schedule, not on every merge request. False positive rates on internal APIs are high — maintain a suppression list.

What Didn't Work

  • Mandatory MR blocking on all findings — developers bypassed by adding allow_failure: true
  • Too many tools — we ran 6 scanners initially; 3 covered 90% of findings with 10% of the noise

The Setup That Stuck

After six months of iteration: Semgrep on every MR (blocking on CRITICAL only), Trivy on every container push, ZAP on a nightly schedule against staging. Security findings go into a shared dashboard rather than blocking MRs by default — with escalation rules for critical severity.

Takeaway

DevSecOps is a cultural change as much as a technical one. Start with high-signal, low-noise tooling, don't block deploys until you've tuned thresholds, and measure false positive rates obsessively.