Security Scanning
SAST
DAST
Dependency Scanning
Secret Detection
Container Scanning
Learn how to integrate GitLab's security scanners into your CI/CD pipelines. From static analysis to container scanning, build a DevSecOps workflow that catches vulnerabilities before they reach production.
SAST
DAST
Dependency
Secret
Container
Why Security Scanning in CI/CD?
Security vulnerabilities found in production are expensive. They require emergency patches, incident response, and sometimes public disclosure. The longer a vulnerability exists in your codebase, the higher the chance it gets exploited. Security scanning in CI/CD shifts this detection left — catching issues during development, in merge requests, before code is merged and deployed. This is the core idea of DevSecOps: make security an integral part of the development workflow rather than a separate gate at the end.
GitLab provides a suite of security scanners that integrate directly into your pipelines. Each scanner targets a different layer of the application: SAST analyzes source code for vulnerabilities like SQL injection, DAST tests the running application for runtime issues, Dependency Scanning checks your libraries for known CVEs, Secret Detection finds accidentally committed credentials, and Container Scanning inspects Docker images for vulnerable OS packages [citation:1][citation:15]. Together, they provide defense in depth across the software supply chain.
These scanners are enabled by including GitLab's official CI/CD templates. A single include line activates a scanner, which then runs automatically on every pipeline (subject to rules). Results appear in merge requests, the security dashboard, and the vulnerability report, making findings actionable without leaving GitLab. For teams on Ultimate, findings can also block merges via security approvals, creating a true security gate [citation:1].
Core Principle: Each scanner covers a different attack surface. SAST finds code flaws, DAST finds runtime flaws, dependency scanning finds supply chain flaws, secret detection finds leaked credentials, and container scanning finds infrastructure flaws. You need all of them.
SAST
Static analysis of source code. Finds SQL injection, XSS, hardcoded secrets in code. Runs during build/test stage.
DAST
Dynamic analysis of running application. Crawls and attacks staging environment. Finds runtime vulnerabilities.
Dependency Scanning
Checks lockfiles (package-lock.json, go.sum, etc.) for known CVEs in dependencies. Runs on every commit.
Secret Detection
Scans commits and history for API keys, tokens, passwords. Prevents credential leaks before they become breaches.
Container Scanning
Scans Docker image layers for vulnerable OS packages. Runs after image is built and pushed.
Security Dashboard
Aggregates findings across projects. Tracks vulnerability trends and remediation progress. Ultimate tier.
SAST: Static Application Security Testing
SAST analyzes your source code without running it, looking for patterns that indicate security vulnerabilities. GitLab's SAST is powered by multiple analyzers — Semgrep for pattern-based checks across many languages, NodeJsScan for Node.js-specific issues, and KubeSec for Kubernetes manifests [citation:11]. The scanner automatically detects your project's languages and runs the appropriate analyzers, producing a JSON report that GitLab parses and displays in merge requests.
Enabling SAST is a single include line. The template creates jobs that run in the test stage, scan the code, and upload the results. By default, SAST jobs have allow_failure: true, meaning findings won't block the pipeline. For production pipelines, you can set allow_failure: false or configure security approvals to require review before merging when high-severity findings exist [citation:1].
SAST results appear directly in merge requests, showing exactly which lines of code triggered a finding. For Ultimate users, GitLab can even suggest fixes — a "Resolve with AI" or "Resolve with merge request" button generates a patch that addresses the vulnerability [citation:1]. This closes the loop from detection to remediation without leaving the MR.
# Enable SAST
include:
- template: Security/SAST.gitlab-ci.yml
stages:
- build
- test
- deploy
# SAST runs automatically in test stage
# No additional configuration required for basic scanning
# Customize SAST (exclude paths, disable analyzers)
sast:
variables:
SAST_EXCLUDED_PATHS: "spec, test, tests, tmp"
SAST_EXCLUDED_ANALYZERS: "nodejs-scan"
SCAN_KUBERNETES_MANIFESTS: "true"
# Custom ruleset (Ultimate)
sast:
variables:
SAST_RULESET_PATH: ".gitlab/sast/ruleset.toml"
# Fail pipeline on findings (use with caution)
sast:
allow_failure: false
# Available SAST variables:
# SAST_EXCLUDED_PATHS - Paths to exclude from scanning
# SAST_EXCLUDED_ANALYZERS - Analyzers to skip
# SCAN_KUBERNETES_MANIFESTS - Enable KubeSec scanning
# SAST_RULESET_PATH - Custom ruleset file
# Results:
# - gl-sast-report.json artifact
# - Findings in MR security widget
# - Vulnerability report in Security > Vulnerability Report
# - Security dashboard (Ultimate)
SAST Best Practices:
- Exclude test directories and generated code to reduce noise
- Run SAST on every commit and merge request
- Use security approvals for critical findings (Ultimate)
- Review SAST findings in MRs before merging
- Customize rulesets to match your organization's policies
DAST: Dynamic Application Security Testing
DAST tests your application while it's running, simulating attacks against a deployed environment. Unlike SAST, which analyzes code statically, DAST interacts with the live application — crawling pages, submitting forms, and probing for vulnerabilities like cross-site scripting, SQL injection, and misconfigured headers. This catches issues that only manifest at runtime, such as authentication bypasses or server misconfigurations [citation:2].
GitLab's DAST uses a browser-based crawler (Chromium) to discover pages and test them. You must provide a target URL via DAST_TARGET_URL or an environment_url.txt file. If your application requires login, you configure authentication with DAST_AUTH_USERNAME, DAST_AUTH_PASSWORD, and field selectors. Without authentication, DAST can only scan publicly accessible pages, missing the majority of your application [citation:2][citation:17].
DAST is most effective against a staging environment that mirrors production. Running it against production is risky (active scanning could cause issues) and running it against localhost requires the runner to have network access. The typical pattern is: deploy to staging, run DAST against staging, then deploy to production after DAST passes. Results appear in the vulnerability report and can be configured to block deployments or require approvals [citation:7].
# Enable DAST
include:
- template: Security/DAST.gitlab-ci.yml
stages:
- build
- test
- deploy
- dast
# Basic DAST configuration
dast:
stage: dast
variables:
DAST_TARGET_URL: "https://staging.example.com"
DAST_FULL_SCAN: "false" # Quick scan for MRs
rules:
- if: $CI_COMMIT_BRANCH == "main"
# DAST with authentication
dast:
stage: dast
variables:
DAST_TARGET_URL: "https://staging.example.com"
DAST_AUTH_URL: "https://staging.example.com/login"
DAST_AUTH_USERNAME: "testuser"
DAST_AUTH_PASSWORD: $DAST_PASSWORD # From CI/CD variables
DAST_AUTH_USERNAME_FIELD: "css:input[name=username]"
DAST_AUTH_PASSWORD_FIELD: "css:input[name=password]"
DAST_AUTH_SUBMIT_FIELD: "css:button[type=submit]"
# Full scan on schedule, quick scan on MRs
dast:
stage: dast
variables:
DAST_TARGET_URL: "https://staging.example.com"
DAST_FULL_SCAN: "true"
DAST_CRAWL_GRAPH: "true" # Generate crawl graph
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
- if: $CI_COMMIT_BRANCH == "main"
variables:
DAST_FULL_SCAN: "false"
# DAST with environment_url.txt (dynamic environments)
# In deploy job:
deploy_staging:
stage: deploy
script:
- ./deploy.sh staging
- echo "https://staging.example.com" > environment_url.txt
artifacts:
paths:
- environment_url.txt
# Available DAST variables:
# DAST_TARGET_URL - Target URL (required)
# DAST_FULL_SCAN - true/false (full vs quick scan)
# DAST_AUTH_URL - Login page URL
# DAST_AUTH_USERNAME/PASSWORD - Credentials
# DAST_AUTH_USERNAME_FIELD/PASSWORD_FIELD - Form selectors
# DAST_CRAWL_GRAPH - Generate crawl graph (for debugging)
# DAST_SCOPE_EXCLUDE_ELEMENTS - Elements to exclude from crawl
DAST Caveats:
- Runner network access: The runner must be able to reach the target URL
- Active scanning: DAST sends real attacks — never run against production without caution
- Authentication required: Without auth, only public pages are scanned [citation:2]
- Scan time: Full scans can take 15+ minutes; use quick scans for MRs [citation:7]
Dependency Scanning
Modern applications are built on hundreds of open-source dependencies. Each dependency is a potential vulnerability vector — a CVE in a logging library or a compromised package can affect thousands of applications. Dependency Scanning automatically checks your lockfiles (package-lock.json, go.sum, pom.xml, requirements.txt, etc.) against vulnerability databases and reports any dependencies with known CVEs [citation:3][citation:8].
GitLab's Dependency Scanning uses the Gemnasium analyzer, which maintains its own advisory database. The scanner supports all major package managers: npm, yarn, pnpm, Maven, Gradle, pip, Poetry, Go modules, Composer, and more. For each vulnerable dependency, it reports the CVE ID, severity, affected versions, and the fixed version, making remediation straightforward. Results appear in merge requests, showing newly introduced vulnerabilities alongside existing ones [citation:18].
The scanner is enabled with a single include. You can exclude paths (like test directories) and specific analyzers using variables. For projects with many dependencies, the scan runs quickly because it analyzes lockfiles rather than downloading packages. This makes it practical to run on every commit and merge request, catching vulnerable dependencies before they're merged [citation:8].
# Enable Dependency Scanning
include:
- template: Security/Dependency-Scanning.gitlab-ci.yml
stages:
- test
# Customize dependency scanning
dependency_scanning:
variables:
DS_EXCLUDED_PATHS: "spec, test, tests, tmp"
DS_EXCLUDED_ANALYZERS: "gemnasium-python" # Skip if not using Python
DS_INCLUDE_DEV_DEPENDENCIES: "false" # Exclude dev dependencies
# Fail on critical vulnerabilities
dependency_scanning:
allow_failure: false
# Supported lockfiles:
# JavaScript: package-lock.json, yarn.lock, pnpm-lock.yaml
# Python: requirements.txt, Pipfile.lock, poetry.lock
# Java: pom.xml, build.gradle, build.gradle.kts
# Go: go.sum, go.mod
# Ruby: Gemfile.lock
# PHP: composer.lock
# .NET: packages.lock.json
# Results:
# - gl-dependency-scanning-report.json
# - Findings in MR security widget
# - Vulnerability report
# For Go projects: generate go.graph for better accuracy
build:
stage: build
image: golang:latest
script:
- go mod tidy
- go build ./...
- go mod graph > go.graph
artifacts:
paths:
- go.graph
# For Gradle: generate dependency lockfile
# Use gradle-dependency-lock-plugin or HtmlDependencyReportTask
# See GitLab docs for details
Dependency Scanning vs Container Scanning: Dependency scanning checks application libraries (npm, pip, Maven). Container scanning checks OS packages inside Docker images (apk, dpkg, rpm). Both are needed — they cover different layers [citation:15].
Secret Detection
Accidentally committed secrets are one of the most common and dangerous security incidents. An API key pushed to a public repository can be exploited within minutes. Even in private repositories, secrets in git history persist forever unless the history is rewritten. Secret Detection scans your commits for patterns that match API keys, tokens, passwords, private keys, and other sensitive values, reporting them as vulnerabilities before they can be exploited [citation:4].
By default, Secret Detection scans the current state of the repository and new commits. This catches secrets as they're introduced. But secrets already in your history — from months or years ago — won't be found unless you run a historic scan. Setting SECRET_DETECTION_HISTORIC_SCAN: "true" scans every branch and commit in the repository, uncovering secrets that were deleted but remain in git history [citation:4]. This is essential for the first scan on an existing repository.
GitLab also offers push protection, which blocks commits containing secrets before they reach the remote. This is a client-side or server-side check that prevents the leak entirely rather than detecting it after the fact. For organizations serious about secret hygiene, combining push protection with pipeline scanning provides defense in depth [citation:4].
# Enable Secret Detection
include:
- template: Jobs/Secret-Detection.gitlab-ci.yml
# Historic scan (scan all branches and commits)
secret_detection:
variables:
SECRET_DETECTION_HISTORIC_SCAN: "true"
# Custom scan range (scan specific commits)
secret_detection:
variables:
SECRET_DETECTION_LOG_OPTIONS: "origin..HEAD"
# Exclude paths (e.g., test fixtures with dummy secrets)
secret_detection:
variables:
SECRET_DETECTION_EXCLUDED_PATHS: "spec, test, tests, fixtures"
# Fail pipeline on detected secrets
secret_detection:
allow_failure: false
# Custom ruleset (override severity, disable rules)
# Create .gitlab/secret-detection-ruleset.toml
# [[secrets.ruleset]]
# [secrets.ruleset.identifier]
# type = "gitleaks_rule_id"
# value = "RSA private key"
# [secrets.ruleset.override]
# severity = "Critical"
# Available variables:
# SECRET_DETECTION_HISTORIC_SCAN - Scan full history
# SECRET_DETECTION_LOG_OPTIONS - Custom commit range
# SECRET_DETECTION_EXCLUDED_PATHS - Paths to exclude
# SECRET_DETECTION_IMAGE_SUFFIX - Use FIPS images
# Results:
# - gl-secret-detection-report.json
# - Findings in MR security widget
# - Vulnerability report
# Push protection (separate from pipeline scanning)
# Configured in Settings > Security > Secret push protection
# Blocks pushes containing secrets before they reach remote
Secret Detection Best Practices:
- Run a historic scan on existing repositories immediately
- Enable push protection to block secrets before they're committed
- Exclude test fixtures that contain fake secrets
- Rotate any secret that was detected, even if it was deleted
- Use masked CI/CD variables instead of hardcoded secrets
Container Scanning
Container images bundle your application with its OS libraries, language runtime, and system packages. Every one of those layers can contain known vulnerabilities. Container Scanning inspects the image's OS packages (Alpine apk, Debian dpkg, RHEL rpm) and reports CVEs, severity, and fixed versions. GitLab uses Trivy as the default scanner, which is fast and has a comprehensive vulnerability database [citation:15][citation:20].
The scanner runs after your image is built and pushed to the registry. This ensures you're scanning exactly what will be deployed, not a local build that might differ. You configure it by setting CS_IMAGE to the image reference, and optionally CS_SEVERITY_THRESHOLD to filter findings. By default, the threshold is set to medium starting in GitLab 18.0, meaning low and unknown severity findings are not reported [citation:10].
Container scanning results appear in merge requests, showing newly introduced vulnerabilities compared to the base branch. This is particularly useful for base image updates: when you bump node:18-alpine to a newer patch, the scan shows exactly what changed. For production pipelines, you can set allow_failure: false to block deployment if critical vulnerabilities exist [citation:20].
# Enable Container Scanning
include:
- template: Security/Container-Scanning.gitlab-ci.yml
stages:
- build
- test
- security
- deploy
# Build image first
build:
stage: build
image: docker:24
services:
- docker:24-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
# Container scanning job
container_scanning:
stage: security
variables:
CS_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
CS_SEVERITY_THRESHOLD: "HIGH" # Only report HIGH and CRITICAL
CS_DOCKERFILE_PATH: "Dockerfile" # For remediation advice
needs:
- build
allow_failure: false # Block pipeline on findings
# Scan multi-arch image (specific platform)
container_scanning:
variables:
CS_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
TRIVY_PLATFORM: "linux/arm64"
# Available variables:
# CS_IMAGE - Image to scan (required)
# CS_SEVERITY_THRESHOLD - Minimum severity (LOW, MEDIUM, HIGH, CRITICAL)
# CS_DOCKERFILE_PATH - Dockerfile path for remediation
# CS_DEFAULT_BRANCH_IMAGE - Default branch image name (for MR comparison)
# TRIVY_PLATFORM - Platform for multi-arch images
# Results:
# - gl-container-scanning-report.json
# - Findings in MR security widget
# - Vulnerability report
# - Security dashboard (Ultimate)
# Reduce vulnerabilities:
# - Use minimal base images (alpine, distroless)
# - Update base images regularly
# - Use multi-stage builds
# - Remove unnecessary packages
Container Scanning Severity Threshold: Starting in GitLab 18.0, the default CS_SEVERITY_THRESHOLD is medium, meaning low-severity findings are not reported by default. This reduces noise but may hide issues. Adjust the threshold based on your risk tolerance [citation:10].
Complete Security Scanning Pipeline
Here's a complete pipeline that integrates all five scanners with appropriate stages and dependencies.
# Complete security scanning pipeline
stages:
- build
- test
- security
- dast
- deploy
# Include all security templates
include:
- template: Security/SAST.gitlab-ci.yml
- template: Jobs/Secret-Detection.gitlab-ci.yml
- template: Security/Dependency-Scanning.gitlab-ci.yml
- template: Security/Container-Scanning.gitlab-ci.yml
- template: Security/DAST.gitlab-ci.yml
# Build image (needed for container scanning)
build:
stage: build
image: docker:24
services:
- docker:24-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
# SAST runs automatically in test stage
sast:
stage: test
variables:
SAST_EXCLUDED_PATHS: "spec, test, tests, tmp"
# Secret detection runs automatically in test stage
secret_detection:
stage: test
variables:
SECRET_DETECTION_HISTORIC_SCAN: "true"
# Dependency scanning runs automatically in test stage
dependency_scanning:
stage: test
variables:
DS_EXCLUDED_PATHS: "spec, test, tests, tmp"
# Container scanning runs in security stage after build
container_scanning:
stage: security
variables:
CS_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
CS_SEVERITY_THRESHOLD: "HIGH"
needs:
- build
# DAST runs against staging after deploy
# (staging deploy job not shown for brevity)
dast:
stage: dast
variables:
DAST_TARGET_URL: "https://staging.example.com"
DAST_FULL_SCAN: "false"
DAST_AUTH_USERNAME: "testuser"
DAST_AUTH_PASSWORD: $DAST_PASSWORD
needs:
- deploy_staging
rules:
- if: $CI_COMMIT_BRANCH == "main"
# Deploy (after all security checks pass)
deploy_production:
stage: deploy
script:
- ./deploy.sh production
environment:
name: production
needs:
- container_scanning
- dast
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
Troubleshooting Security Scanning
# SAST job not running
# - Check include template path is correct
# - Check project has supported language files
# - Check SAST_DISABLED is not set to true
# SAST false positives
# - Use SAST_EXCLUDED_PATHS to exclude test/generated code
# - Use custom ruleset to adjust severity or disable rules
# - Review findings in context before dismissing
# DAST cannot connect to target
# - Check runner network access (can it reach the URL?)
# - Check target URL is correct (include port if non-standard)
# - Check DAST_TARGET_URL is set
# - For local services, use services: in job definition
# DAST missing pages
# - Enable DAST_CRAWL_GRAPH to visualize crawled pages
# - Adjust DAST_SCOPE_EXCLUDE_ELEMENTS to exclude navigation items
# - Ensure authentication is configured if app requires login
# Dependency scanning not finding lockfile
# - Check lockfile is committed to repository
# - Check DS_EXCLUDED_PATHS doesn't exclude the lockfile
# - For Go, generate go.graph for better accuracy
# Secret detection not finding known secret
# - Run historic scan (SECRET_DETECTION_HISTORIC_SCAN: true)
# - Check secret pattern matches a known rule
# - Check file path isn't excluded
# Container scanning job fails
# - Check image exists in registry
# - Check registry credentials (CI_REGISTRY_USER, CI_REGISTRY_PASSWORD)
# - Check CS_IMAGE is set correctly
# - Check runner can pull image
# Container scanning finds no vulnerabilities (but expected some)
# - Check CS_SEVERITY_THRESHOLD (default medium in 18.0)
# - Lower threshold to "low" to see all findings
# - Check scanner is using latest vulnerability database
# Too many findings (noise)
# - Set allow_failure: false only for critical/high
# - Use severity thresholds to filter
# - Create allowlist for accepted risks
# - Exclude test/dev dependencies
# Debug: check report artifacts
# Download gl-*-report.json artifacts from job
# Inspect JSON for details on findings
Frequently Asked Questions
Which scanners do I need for a complete security posture?
All five: SAST (code), DAST (runtime), Dependency Scanning (libraries), Secret Detection (credentials), and Container Scanning (images). Each covers a different attack surface [citation:1][citation:15].
Do security scanners slow down pipelines?
Yes, but typically by 30-60 seconds per scanner. Container scanning and DAST are the slowest. You can run scanners in parallel and use quick scans (DAST) for merge requests.
Should security scanners block merges?
For critical and high findings, yes. Use allow_failure: false or security approvals (Ultimate). For lower severities, warn but don't block to avoid alert fatigue.
What's the difference between SAST and DAST?
SAST analyzes source code statically (no running app). DAST tests the running application dynamically (crawls and attacks). SAST finds code flaws, DAST finds runtime flaws. Both are complementary.
How do I scan git history for secrets?
Set SECRET_DETECTION_HISTORIC_SCAN: "true" in the secret_detection job. This scans all branches and commits, finding secrets that were deleted but remain in history [citation:4].
Why is container scanning not finding OS vulnerabilities?
Check CS_SEVERITY_THRESHOLD. Since GitLab 18.0, the default is medium, so low-severity CVEs are hidden. Lower the threshold to low to see all findings [citation:10].
Can I use security scanners on GitLab Free?
Yes. SAST, Secret Detection, and Container Scanning are available on Free. DAST and Dependency Scanning require Ultimate for full features. Free users get basic scanning but not the security dashboard [citation:11].
How do I handle false positives?
Use severity thresholds, exclude paths, or create custom rulesets to disable specific rules. For container scanning, use vulnerability-allowlist.yml to allowlist specific CVEs [citation:5].
Security scanning is not a gate at the end — it's a continuous practice woven into every pipeline.