
Your pipeline builds, tests and deploys, but an npm dependency contains a critical CVE (a known vulnerability, publicly listed under a unique identifier), a developer committed an AWS token, and your Docker image ships a vulnerable OpenSSL. You will only find out after the incident. GitLab builds five security scanners straight into CI/CD: they analyze your code, your dependencies, your images and your secrets on every pipeline, with a vulnerability report that developers can act on.
Each one covers a family of risks: SAST analyzes the source code without running it, Secret Detection spots committed tokens, API keys and passwords, Dependency Scanning looks for known CVEs in the libraries the project depends on, Container Scanning inspects the Docker images you build, and DAST tests the deployed application the way an external attacker would. Each scanner has its own section below.
Branch pipelines vs merge request pipelines. By default, the scanners run on branch pipelines (on every push). To run them specifically on merge request pipelines and show the vulnerability diff, add the variable SAST_ENABLE_MR_PIPELINES: "true" (and its equivalent for each scanner) or use the latest templates (Jobs/SAST.latest.gitlab-ci.yml).
What you will learn
- Enable each scanner in a GitLab CI pipeline with the official templates
- Read a vulnerability report in a merge request and prioritize the fixes
- Configure Secret Detection to detect committed tokens before the merge
- Scan your Docker images built in the same pipeline
- Configure the governance mechanisms to react to detected vulnerabilities
- Choose between Auto DevOps and manual configuration
In which context?
The GitLab security scanners meet a precise need: detect vulnerabilities as early as possible in the development cycle, ideally at the merge request, before the code reaches the main branch.
Without these scanners, teams discover security problems:
- In production, when an external scanner (Trivy, Snyk) detects a critical CVE on a deployed image
- During an audit, when they find that secrets have been lying in the Git history for months
- After an incident, when an attacker exploits a dependency vulnerability that had been known for a long time
In daily practice, the scanners are used to:
- Catch secrets (AWS tokens, SSH keys, passwords) in commits before they are merged into the main branch
- Check dependencies on every update of
package-lock.json,go.sumorrequirements.txt - Validate Docker images before pushing them to the Container Registry
- Test the attack surface of an application deployed in a review app
- Produce reports for compliance audits (see Audit & compliance)
Prerequisites
- A GitLab project with a working pipeline (see First pipeline)
- Understand templates and includes (the scanners use
include: template:) - For Container Scanning: a job that builds a Docker image (see Registries)
- For DAST: a deployed application that is reachable (review app or staging environment)
The 5 scanners explained
Overview
Source code ──► SAST ──────────► Vulnerabilities in the code │ ├──► Secret Detection ──► Tokens/keys detected │ ├──► Dependency Scanning ► CVEs in the dependencies │ └──► Container Scanning ─► CVEs in the Docker image │Deployed application ──► DAST ─────────► Web vulnerabilities (XSS, injection, etc.)SAST, Static Application Security Testing
SAST analyzes your source code without running it. It detects programming flaws: SQL injections, XSS, use of dangerous functions, poor handling of user input.
GitLab automatically detects the languages and enables the matching analyzers. Depending on the project, several analyzers can run in parallel: Semgrep (Python, JS, Go, Java…), SpotBugs (Java, Kotlin), Sobelow (Elixir), PMD (Apex), Kubesec (Kubernetes manifests) and GitLab Advanced SAST (Ultimate, inter-procedural analysis).
include: - template: Jobs/SAST.gitlab-ci.ymlThat's it. GitLab automatically creates one or more jobs (e.g. semgrep-sast, spotbugs-sast) in the test stage which:
- Detect the project's languages
- Run the appropriate analyzers
- Produce a
gl-sast-report.jsonreport (GitLab Security Report format, not SARIF) - Show the results in the merge request (Ultimate)
Common customization:
include: - template: Jobs/SAST.gitlab-ci.yml
variables: SAST_EXCLUDED_PATHS: "spec,test,tests,tmp,vendor,node_modules" SEARCH_MAX_DEPTH: 4Secret Detection
The Secret Detection scanner goes through the commits to find secrets (tokens, API keys, passwords, certificates) that should not have been committed.
include: - template: Jobs/Secret-Detection.gitlab-ci.ymlAmong other things, it detects:
| Type of secret | Example pattern |
|---|---|
| AWS keys | AKIA[0-9A-Z]{16} |
| GitLab tokens | glpat-[0-9a-zA-Z_-]{20} |
| SSH/RSA private keys | -----BEGIN RSA PRIVATE KEY----- |
| GitHub tokens | ghp_[0-9a-zA-Z]{36} |
| Clear-text passwords | Variables containing password, secret, token |
Initial scan of the history (run only once):
After enabling Secret Detection, run a pipeline manually with the variable SECRET_DETECTION_HISTORIC_SCAN: "true" to scan the whole Git history. It is a one-shot scan, do not leave it in your permanent configuration, because it makes every pipeline considerably longer.
# Single manual pipeline for the historic scan# Build > Pipelines > New pipeline > Add variable:# SECRET_DETECTION_HISTORIC_SCAN = trueStandard (daily) configuration:
include: - template: Jobs/Secret-Detection.gitlab-ci.yml
variables: SECRET_DETECTION_EXCLUDED_PATHS: "tests/"Dependency Scanning
Dependency Scanning analyzes your lock files (package-lock.json, go.sum, Gemfile.lock, requirements.txt) to identify the dependencies with known CVEs (NVD database and GitLab Advisory Database).
include: - template: Jobs/Dependency-Scanning.gitlab-ci.ymlIt produces a report listing each vulnerable dependency with:
- The CVE identifier
- The severity (Critical, High, Medium, Low)
- The fixed version (when there is one)
- The link to the advisory
Customization:
include: - template: Jobs/Dependency-Scanning.gitlab-ci.yml
variables: DS_EXCLUDED_PATHS: "vendor/"Container Scanning
Container Scanning analyzes the Docker image that your pipeline has just built to find vulnerabilities in the system packages (apt, apk, rpm) and the embedded libraries.
include: - template: Jobs/Container-Scanning.gitlab-ci.yml
container_scanning: variables: CS_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHADAST, Dynamic Application Security Testing
DAST tests an application that is deployed and reachable by simulating real attacks: SQL injections, XSS, CSRF, missing security headers, etc.
include: - template: Security/DAST.gitlab-ci.yml
variables: DAST_TARGET_URL: "https://staging.mon-app.com"DAST is the heaviest scanner: it launches a headless browser and explores the application. It is generally run on a staging environment or a review app, not in production.
Customization:
include: - template: Security/DAST.gitlab-ci.yml
variables: DAST_TARGET_URL: "https://$CI_ENVIRONMENT_SLUG.review.mon-app.com" DAST_FULL_SCAN_ENABLED: "false" # Quick scan by default DAST_BROWSER_SCAN: "true" # Browser-based scan (recommended)Complete security pipeline
Here is a pipeline that combines the 5 scanners with the build and test steps:
stages: - build - test - deploy - dast # Stage required by the DAST template
include: - template: Jobs/SAST.gitlab-ci.yml - template: Jobs/Secret-Detection.gitlab-ci.yml - template: Jobs/Dependency-Scanning.gitlab-ci.yml - template: Jobs/Container-Scanning.gitlab-ci.yml - template: Security/DAST.gitlab-ci.yml
variables: CS_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA DAST_TARGET_URL: "https://$CI_ENVIRONMENT_SLUG.staging.mon-app.com"
build-image: stage: build image: docker:27@sha256:aa3df78ecf320f5fafdce71c659f1629e96e9de0968305fe1de670e0ca9176ce services: - docker:27-dind@sha256:aa3df78ecf320f5fafdce71c659f1629e96e9de0968305fe1de670e0ca9176ce variables: DOCKER_TLS_CERTDIR: "/certs" before_script: - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" script: - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" . - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
test: stage: test image: node:22-alpine@sha256:16e22a550f3863206a3f701448c45f7912c6896a62de43add43bb9c86130c3e2 script: - npm ci - npm test
deploy-staging: stage: deploy script: - deploy-to-staging.sh environment: name: staging/$CI_COMMIT_REF_SLUG url: https://$CI_ENVIRONMENT_SLUG.staging.mon-app.com rules: - if: $CI_MERGE_REQUEST_IIDThe build-image job reads the registry password on standard input with --password-stdin, rather than in a -p argument: a command-line argument shows up in the process list (ps) and can end up in the job traces.
Reading the vulnerability reports
In the merge request
When the scanners run on an MR, GitLab shows a security widget summarizing:
- The number of new vulnerabilities introduced by the MR
- The number of vulnerabilities fixed
- The breakdown by severity (Critical, High, Medium, Low)
Each vulnerability is clickable and displays:
- The description of the flaw
- The file and line concerned (SAST) or the dependency (Dependency Scanning)
- The severity and the CVSS score
- The recommended solution (update, workaround)
- The CVE / CWE identifier
In the Security Dashboard
The Security Dashboard (Security & Compliance > Vulnerability Report) centralizes all the vulnerabilities detected on the default branch. It lets you:
- Filter by scanner, severity, status
- Mark a vulnerability as "confirmed", "dismissed" or "resolved"
- Create a GitLab issue directly from a vulnerability
- Track the evolution over time
First governance mechanisms
Two complementary mechanisms let you react to the scanners' results:
1. allow_failure: false, detect scanner failures
By default, the scanners use allow_failure: true: if the scanner crashes (timeout, network error, image unavailable), the pipeline continues. By switching to false, a failure of the scanner itself blocks the pipeline, but the vulnerabilities detected block nothing:
semgrep-sast: allow_failure: false # Blocks if the scanner crashes, NOT if findings exist2. Merge request approval policies, block on findings (Ultimate)
To prevent the merge when critical vulnerabilities are detected, configure a merge request approval policy in a security policy project:
-
Create a Security Policy Project (Security & Compliance > Policies > New policy project)
-
Create a Merge request approval policy with a
scan_findingrule:- Minimum severity:
criticalorhigh - Scanners concerned: SAST, Dependency Scanning, Container Scanning, Secret Detection
- Required approvers: members of the security team
- Minimum severity:
-
Link the policy project to your projects (Settings > Security & Compliance > Security policy project)
Auto DevOps vs manual configuration
GitLab offers Auto DevOps, which automatically enables all the scanners without configuration. It is tempting, but here is when to choose each approach:
| Criterion | Auto DevOps | Manual configuration |
|---|---|---|
| Setup time | Instant | 15 min |
| Control | Limited | Total |
| Scanners enabled | All | Your choice |
| Customization | Via variables | Via full YAML |
| Recommended for | Prototyping, small projects | Production, compliance |
To enable Auto DevOps: Settings > CI/CD > Auto DevOps > Enable.
For most teams in production, manual configuration via include: template: is preferable because it gives explicit control over what runs.
Troubleshooting
| Symptom | Likely cause | Solution |
|---|---|---|
| Scanner not run | Template not included, or template rules that exclude the job | Check include: and the template variables |
Pipeline failed with no job at all, no message | A job (even one from an include) uses a stage missing from stages: | Open the project's CI linter: it names the missing stage |
SAST: 0 findings on a project with code | Language not detected | Add SAST_DEFAULT_ANALYZERS explicitly |
| Container Scanning fails | Image not found | Check CS_IMAGE and that the build job succeeded |
| Secret Detection: false positives | Pattern too broad | Add a .gitlab-secret-detection.toml file to exclude |
| DAST timeout | Application not reachable from the runner | Check the URL and the runner's network |
| Security widget missing in the MR | Insufficient license or missing reports | Check the GitLab tier and the jobs' artifacts |
Dependency Scanning: 0 findings | Lock file missing | Generate package-lock.json or go.sum beforehand |
Hands-on lab
The lab "Turn the scanners on, and read what they say" replays this guide on a
GitLab CE forge set up on a virtual machine, with its runner and its
registry. The project's pipeline only runs the application tests: it is
green, and nobody has ever looked at what the repository holds from a security
point of view. You enable two of the three scanners available on the Free
tier, SAST and Secret Detection, with the same include: template:
lines as above.
Because the lab instance runs Community Edition, it has no Security
Dashboard and no security widget in the merge request: the report is a JSON
file dropped as an artifact (gl-sast-report.json,
gl-secret-detection-report.json), and reading it is part of the job. A
committed token has been sitting in the code for a long time, which calls
for the historic scan described above, run once by hand. You fix the flaw
the report points at, then record in a LECTURE.md file the file and the
line it named.
Grading is taken on what the forge really produced: two successful jobs that
dropped a readable report, a report that names the token, and a fresh scan
on main that no longer names it while the tests still pass. Deleting the line
without running the scanner is therefore not enough. The lab starts with
dsoxlab run gl-21-activer-les-scanners, after dsoxlab provision;
installing DSOXLab is described in the DSOXLab
documentation.
Key takeaways
- GitLab builds in 5 security scanners that you enable via
include: template:, no external tools needed - SAST, Secret Detection and Container Scanning are available on all tiers (including Free, the tier of a CE instance), Dependency Scanning and DAST require Ultimate
- Results appear directly in the merge request (Ultimate widget), on Free, the JSON reports can be downloaded
- Container Scanning analyzes the Docker images built in the same pipeline
- DAST tests the deployed application, it needs a reachable environment and the
daststage allow_failure: falsedetects scanner failures, to block on findings, configure merge request approval policies (Ultimate)- Auto DevOps enables everything automatically, but manual configuration offers more control
- These scanners complement the level 4 security module, which covers protecting the pipelines themselves
Knowledge check
Check that the essentials of this lesson are in place. The questions only cover what has just been explained here.
Knowledge check
Check what you have learned with this interactive quiz
How it works
- The timer starts when you click Start
- Multiple choice, true/false and short answer questions
- You can move between questions
- Detailed results are shown at the end
Starts the quiz and the timer
Review
(0/0)Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Weak areas
No topic below the pass mark.
Recommended next
Hints before you try again?
Continue the course: C3: Industrializing GitLab CI/CD pipelines
A new full quiz with random questions
Work again only on the questions you missed