Skip to content
Back to blog

Shift-Left Security: Integrating SAST, DAST, and SCA in GitLab CI/CD

Security used to live at the end of our pipeline. A dedicated security review gate, scheduled quarterly, staffed by a team of two. The result was predictable: mountains of deferred findings, developers who had context-switched away three sprints ago, and fixes that introduced regressions because no one remembered why the original code was written that way.

We changed that. Over six months, we embedded SAST, DAST, and SCA scanning directly into every merge request. Here is what we learned.

Why “Shift Left” Is More Than a Slogan

The earlier a vulnerability is caught, the cheaper it is to fix. IBM’s System Sciences Institute puts the cost multiplier at 100× between development and post-production. That number feels abstract until you spend two weeks backporting a patch to three production environments while a customer’s SOC is watching your logs.

Shifting left means developers own security findings in the same workflow where they own test failures. No separate ticket queue. No waiting for a security team bottleneck. Just a red pipeline that tells you what is broken and why.

The Three Layers of Pipeline Security

1. SAST — Static Analysis in Every MR

Static Application Security Testing analyses source code without executing it. We use GitLab’s built-in SAST (powered by Semgrep under the hood) for our Python, Go, and TypeScript services.

.gitlab-ci.yml
include:
- template: Security/SAST.gitlab-ci.yml
sast:
variables:
SAST_EXCLUDED_PATHS: "tests/, docs/"
SAST_SEVERITY_THRESHOLD: "medium"
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

The key configuration decision is SAST_SEVERITY_THRESHOLD. We block MRs on critical and high findings, but surface medium as warnings. Blocking on medium immediately triggers alert fatigue — engineers start ignoring the scanner.

For infrastructure-as-code, we layer in Checkov for Terraform files:

checkov-stage.yml
checkov:
stage: sast
image: bridgecrew/checkov:latest
script:
- checkov -d infrastructure/ --framework terraform --output cli --soft-fail-on MEDIUM
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
changes:
- infrastructure/**/*

2. SCA — Dependency Vulnerability Scanning

Software Composition Analysis catches vulnerabilities in your third-party dependencies. This is where the volume is — the average Node.js application has 1,000+ transitive dependencies.

include:
- template: Security/Dependency-Scanning.gitlab-ci.yml
dependency_scanning:
variables:
DS_EXCLUDED_ANALYZERS: "bundler-audit"
DS_MAX_DEPTH: 5

We complement this with Trivy for container image scanning at build time:

trivy-scan.yml
trivy-scan:
stage: security
image: aquasec/trivy:latest
script:
- trivy image --exit-code 1 --severity HIGH,CRITICAL
--ignore-unfixed
$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
allow_failure: false

--ignore-unfixed is critical here. Without it, you’ll fail builds on CVEs where no patched base image exists yet — which means every build fails until upstream publishes a fix that may be months away.

3. DAST — Dynamic Testing Against a Live Environment

Dynamic Application Security Testing runs against a running instance of your application. It catches things SAST misses: authentication bypass, injections in runtime-evaluated code, misconfigurations in response headers.

We deploy to a review environment on every MR (Cloudflare Pages preview URLs) and then point DAST at it:

include:
- template: DAST.gitlab-ci.yml
dast:
stage: dast
variables:
DAST_WEBSITE: $REVIEW_APP_URL
DAST_FULL_SCAN_ENABLED: "false" # baseline scan on MRs
DAST_ZAP_USE_AJAX_SPIDER: "true"
environment:
name: review/$CI_COMMIT_REF_SLUG
url: $REVIEW_APP_URL
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"

We use baseline scans (passive only) on MRs and full active scans nightly against staging. Full scans against a running app can create noise (or actual damage if the app is destructive by nature), so limit active scanning to environments built for it.

Handling False Positives Without Breaking Morale

The number one reason security scanning fails in practice is false positive fatigue. Engineers who see 40 findings on a UI library import — none of them exploitable in their context — will start adding # nosec comments as muscle memory.

Our approach:

  1. Triage weekly — a 30-minute weekly review of new findings in GitLab’s vulnerability dashboard. Anything confirmed false positive gets dismissed with a comment explaining why.
  2. Baseline exceptions — for accepted risks (e.g., a CVE in a test-only dependency), use GitLab’s vulnerability management to mark it accepted with a rationale and expiry date.
  3. Keep the block list narrow — only critical and high exploitable findings block merge. Everything else informs but does not stop.

Results After Six Months

MetricBeforeAfter
Mean time to detect (MTTD)73 days4 hours
Critical vulns reaching staging18/quarter2/quarter
Security review meetings4/year0
Developer security tickets (open)14023

The security review meetings going to zero is the outcome I’m most proud of. Not because security became unimportant, but because it became routine — part of the same feedback loop as a failing unit test.

What’s Next

We’re currently rolling out gitleaks for secrets detection (pre-receive hooks + CI stage) and evaluating IAST agents for our Python services. I’ll cover both in follow-up posts.

If you’re just getting started, the minimum viable pipeline security stack is:

  • Semgrep SAST (free, open-source rules)
  • Trivy for dependencies + container images
  • OWASP ZAP baseline scan for DAST

All three are free, all three integrate cleanly with GitLab CI. Ship that, tune the thresholds for your context, and you’ll be ahead of 90% of teams your size.


Share LinkedIn X

Related posts