Lesson 2: Setting up Security Scanners and Policies#
In this lesson you will configure all of GitLab’s built-in security scanners, enable Secret Push Protection, integrate a third-party scanner via SARIF, enable security training, and create the three types of security policies that prevent insecure code from being merged.
This lesson covers:
- What security scans GitLab offers
- Adding security scans to the pipeline
- Configuring scanners with security configuration profiles
- Configuring security scanners
- Using DAST scan and site profiles
- Enabling Secret Push Protection
- Integrating a third-party scanner using SARIF
- Enabling security training
- Understanding security policies
- Creating a merge request approval policy (vulnerabilities)
- Creating a scan execution policy (IaC scanning)
- Injecting custom jobs using pipeline execution policies
What security scans GitLab offers#
GitLab provides a comprehensive set of built-in security scanners so you can detect vulnerabilities across your entire application stack — from source code, to dependencies, to running services — without integrating a collection of third-party tools. Detection rules are maintained by GitLab’s Security Research team and are updated continuously.
Software dependency advisories live in the GitLab Advisory Database, which combines the public advisory database with GitLab-curated entries.
GitLab provides the following security scanners:
| Scanner | What it does |
|---|---|
| Static Application Security Testing (SAST) | Analyzes source code for known vulnerability patterns. GitLab Advanced SAST extends this with cross-file, cross-function dataflow (“taint”) analysis and produces a code flow trace from source to sink. |
| Dynamic Application Security Testing (DAST) | Scans your running application by sending requests and analyzing responses for vulnerabilities (requires a deployed application). |
| API Security Testing | Performs dynamic scans of your APIs using an OpenAPI definition, HAR file, or Postman collection. |
| Container Scanning | Scans container images for known vulnerabilities in OS packages and language libraries. Also runs as Continuous Container Scanning against the registry so newly published CVEs surface without a code change. |
| Dependency Scanning | Scans project dependencies for known vulnerabilities and detects each dependency’s software license. Supports Static Reachability to prioritize vulnerabilities that are actually called from your code. |
| Secret Detection | Scans the repository for accidentally committed secrets (API keys, tokens, passwords). Secret Push Protection blocks secrets before they land on the server. |
| Infrastructure as Code (IaC) Scanning | Scans Terraform, Ansible, CloudFormation, Kubernetes, and Helm files for misconfigurations and known vulnerabilities. |
When a scanner detects a vulnerability, it provides the following information:
- Description of the vulnerability
- When it was detected
- Current status (Detected, Confirmed, Dismissed, Resolved)
- Severity with risk assessment data (CVSS score, EPSS probability, KEV status, reachability)
- Location including the file path and line number
- Scanner type that found it
- Evidence supporting the finding
- Relevant links including educational resources, training, and solutions
- Identifiers such as CVE and CWE references
- Request/Response data (for dynamic scans only)
Adding security scans to the pipeline#
Security scanners can be added to your pipeline in three different ways. The most common method is including GitLab-provided CI templates in your .gitlab-ci.yml, which gives you full control over scanner configuration using the GitLab CI/CD job syntax.
This project already has security scanners configured using CI templates, as seen in the .gitlab-ci.yml:
include:
- template: Jobs/Container-Scanning.gitlab-ci.yml
- template: Jobs/Dependency-Scanning.v2.gitlab-ci.yml
- template: Jobs/SAST.gitlab-ci.yml
- template: Jobs/Secret-Detection.gitlab-ci.yml
- template: Security/DAST.gitlab-ci.ymlThe
Dependency-Scanning.v2template is the latest version and includes both vulnerability detection and license scanning in a single scanner.
The three methods for adding scanners are:
CI Templates (recommended) — Add GitLab-maintained CI templates to your
.gitlab-ci.yml. This is the most flexible approach and is what this project uses. See the documentation for details.Security configuration profiles — From a group’s Secure > Security inventory, apply reusable, centrally-managed profiles that turn scanners on across many projects from the UI — no per-project
.gitlab-ci.ymledits. See the documentation for details.Scan Execution Policies — Enforce a scanner from a security policy project so developers cannot disable it. We will set one of these up later in this lesson.
Configuring scanners with security configuration profiles#
Editing .gitlab-ci.yml works well for one project, but it does not scale to dozens or hundreds of repositories. Security configuration profiles solve this: they are centralized, reusable settings that define how and when scanners run, applied in bulk from the UI without editing CI/CD configuration in each project. This is the recommended way to roll out scanners consistently across a group.
Security configuration profiles require GitLab Ultimate and are managed from a group’s Security inventory. To view the inventory you need the Security Manager, Developer, Maintainer, or Owner role on the group. We cover the Security inventory itself in more depth in Lesson 4.
GitLab currently ships three default profiles, each with its own scan triggers:
| Profile | Scan triggers |
|---|---|
| Secret detection | Push protection (blocks pushes containing detected secrets), merge request pipelines, and default-branch pipelines |
| SAST | Merge request pipelines and default-branch pipelines |
| Dependency scanning | Merge request pipelines and default-branch pipelines |
Review test coverage#
On the top bar, select Search or go to and find your group
In the left sidebar, select Secure > Security inventory
Review the Test Coverage column for each project. Each scanner shows one of four statuses:
- Enabled — configured and completed successfully
- Not enabled — not configured
- Failed — ran but did not complete successfully
- Stale — previously enabled but has not run in the last three consecutive pipelines
Apply a profile to projects#
To enable a scanner on a single project:
From Secure > Security inventory, locate the project
Select the vertical ellipsis next to it and choose Manage tool coverage
Turn individual scanners on or off
To apply a default profile across many projects at once:
From Secure > Security inventory, select multiple projects or an entire subgroup
Select the Bulk action dropdown and choose Manage security scanners
Choose Apply default profile to all
The scanners run on the next pipeline for each selected project — no .gitlab-ci.yml edits required.
Profiles are applied to the projects in a group or subgroup, but they are not attached to the group itself, and there is no inheritance between subgroups. If you need a scanner enforced so developers cannot remove it — regardless of the project’s
.gitlab-ci.yml— use a Scan Execution Policy instead, which we cover later in this lesson.
Configuring security scanners#
Each scanner can be configured using CI/CD environment variables. Depending on the scanner, different configuration options are available. You can also use any GitLab CI/CD job keyword to further customize scanner behavior (e.g., changing which stage a scanner runs in, setting rules for when it runs, or adding dependencies).
See the application security documentation for the full list of configuration variables for each scanner.
Example: Configuring GitLab Advanced SAST and Static Reachability#
GitLab Advanced SAST adds cross-file, cross-function dataflow (“taint”) analysis on top of the default SAST analyzers, so you get a code flow view that traces tainted data from the source to the vulnerable sink. Static Reachability does something similar for dependencies — it marks a dependency vulnerability as reachable only if your code actually calls into the vulnerable function.
Both are enabled in this project via a few variables at the top of .gitlab-ci.yml:
variables:
GITLAB_ADVANCED_SAST_ENABLED: true # enable cross-file taint analysis
DS_STATIC_REACHABILITY_ENABLED: true # flag dependency findings as reachable
DS_MAX_DEPTH: -1 # follow the entire call graphWhen triaging, prioritize findings that are reachable — those are the ones an attacker can actually exercise.
Example: Configuring DAST#
Below is an example showing how to customize the DAST scanner. This configuration runs a passive (non-intrusive) scan on the default branch and a full active scan on all other branches:
include:
- template: Security/DAST.gitlab-ci.yml
dast:
stage: dast
rules:
- if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH
variables:
DAST_FULL_SCAN: "false"
- if: $CI_COMMIT_REF_NAME != $CI_DEFAULT_BRANCH
variables:
DAST_FULL_SCAN: "true"
needs:
- deploy-kubernetesUsing DAST scan and site profiles#
DAST is the one scanner whose configuration commonly changes between runs — you want to test the same staging URL with different scan intensities, or you want to scan a different environment with the same scan settings. GitLab models this with two reusable profiles:
- Site profiles — define what to scan: target URL, authentication credentials, allowed/excluded paths, request headers
- Scan profiles — define how to scan: passive vs active, crawl timeout, max scan duration, AJAX spidering, browser-based vs proxy-based
A DAST job picks one of each. Swap the site profile to point at a different environment; swap the scan profile to go from a 5-minute passive scan to a full active scan.
Create a site profile#
Navigate to Secure > Security configuration
Find the DAST row and click Manage profiles
Click New site profile
Fill in:
- Profile name:
tanuki-shop-staging - Target URL: the URL of your deployed Tanuki Shop (from Operate > Environments) — or any HTTPS URL you control if you skipped deployment
- Site type:
Website - Excluded URLs: any paths you don’t want scanned (admin panels, destructive endpoints)
- Click Save profile
Create a scan profile#
Back on the DAST profiles page, click the Scan profiles tab
Click New scan profile
Fill in:
- Profile name:
passive-quick - Spider timeout:
5minutes - Target timeout:
60seconds - Scan mode:
Passive(Activeis intrusive — only run it on test environments) - AJAX spider: enabled (Tanuki Shop is a single-page Angular app, so the AJAX spider catches routes the standard spider misses)
- Click Save profile
Use the profiles from a DAST job#
You can run a DAST scan directly from the profiles page, or reference the profiles from a CI job:
dast:
dast_configuration:
site_profile: "tanuki-shop-staging"
scan_profile: "passive-quick"Now any developer who needs to run DAST against staging can do so without learning the underlying DAST variables — the security team owns the profile, the developer just consumes it. This is the same separation-of-duties pattern we’ll see later with security policies.
Authenticating the DAST scan#
Most of Tanuki Shop lives behind a login, so an unauthenticated scan only reaches a handful of public pages. To scan the authenticated surface, DAST drives the login form in a real browser once at the start of the scan and reuses the resulting session for every subsequent request.
Tanuki Shop ships with seeded accounts, so you don’t need to create one. Use the built-in jim customer account:
| Setting | Value |
|---|---|
DAST_AUTH_USERNAME | jim@tanuki.local |
DAST_AUTH_PASSWORD | ncc-1701 |
Seeded user emails are
<name>@<application.domain>. This deployment runs withNODE_ENV=gitlab(seechart/values.yaml), which loadsconfig/gitlab.ymland sets the domain totanuki.local— hencejim@tanuki.local. If you deploy with a different config (the default isjuice-sh.op), match the username to that domain. The passwordncc-1701is a literal indata/static/users.ymland never changes.jimis a low-privilegecustomer, which is the right level for an authenticated scan; avoid using anadminaccount. These are training credentials for the intentionally insecure demo app — never reuse this pattern for real accounts.
Store both values as masked CI/CD variables (Settings > CI/CD > Variables) rather than committing them, then declare only the non-secret login-form selectors in the dast job:
dast:
variables:
DAST_AUTH_URL: "$DAST_WEBSITE#/login" # login page (built from the deployed target URL)
DAST_AUTH_USERNAME_FIELD: "css:#email" # username/email input
DAST_AUTH_PASSWORD_FIELD: "css:#password" # password input
DAST_AUTH_SUBMIT_FIELD: "css:#loginButton" # submit button
DAST_AUTH_SUCCESS_IF_NO_LOGIN_FORM: "true" # success = the login form is gone
DAST_AUTH_REPORT: "true" # emit gl-dast-debug-auth-report.html to debug loginsDAST reads DAST_AUTH_USERNAME / DAST_AUTH_PASSWORD automatically and types them into the fields identified by DAST_AUTH_USERNAME_FIELD / DAST_AUTH_PASSWORD_FIELD. Because DAST_AUTH_REPORT is enabled, the job publishes gl-dast-debug-auth-report.html as an artifact — open it to confirm the session was established, or to see where a failed login broke.
When configuring a site profile in the UI instead, attach the same credentials under its Authentication section (Username, Password, and the field selectors above) — the site profile is where authentication belongs, since it describes what to scan.
Enabling Secret Push Protection#
Secret Push Protection catches accidentally committed secrets at git push time — before they reach the remote and become part of repository history. It is the cheapest possible place to catch a leak: nothing to revoke, nothing to scrub from history.
Navigate to Secure > Security configuration in the left sidebar
Locate the Secret Push Protection row
Toggle it on
Now try it locally — create a branch, commit a “secret,” and push:
git checkout -b test-secret-push-protection
echo 'AWS_KEY="AKIAIOSFODNN7EXAMPLE"' >> .env.example
git add .env.example
git commit -m "test secret push protection"
git push origin test-secret-push-protectionThe push is rejected and GitLab prints the offending file, line number, and detector that fired. Clean up:
git reset --hard HEAD~1
git checkout gitlab
git branch -D test-secret-push-protectionIf a real secret needs to ship (a placeholder, a test fixture, a known-public token), users with the right role can skip the check by adding
[skip secret push protection]to the commit message or by configuring an exclusion.
Integrating a third-party scanner using SARIF#
GitLab’s built-in scanners cover most languages and frameworks, but you may already use a specialized scanner (Semgrep, Snyk Open Source, Bandit, ESLint security plugins, Trivy, Checkov, etc.) and want its findings to show up in the same vulnerability report. The easiest way is SARIF — the industry-standard output format for static analysis. GitLab consumes SARIF 2.1.0 directly via the artifacts:reports:sast artifact.
We will use Semgrep as the example because it is free, has a strong rule library, and emits SARIF natively.
Add the Semgrep job to your pipeline#
In your project, open
.gitlab-ci.ymlAdd the following job at the bottom of the file:
semgrep-sast:
stage: test
image: returntocorp/semgrep:latest
variables:
# Use the bundled "auto" config; swap for "p/owasp-top-ten", "p/ci",
# or a path to your own rules to customize coverage
SEMGREP_RULES: "p/default"
script:
- semgrep ci --config "$SEMGREP_RULES" --sarif --output gl-sast-semgrep.sarif || true
artifacts:
when: always
reports:
sast: gl-sast-semgrep.sarif
paths:
- gl-sast-semgrep.sarif
expire_in: 1 week
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
allow_failure: true- Commit the change and push it
A few details that make this work end-to-end:
artifacts:reports:sast— this is the key. GitLab inspects every SAST-class artifact and accepts SARIF 2.1.0 in addition to its native JSON format. No conversion step is needed.|| true+allow_failure: true— Semgrep exits non-zero when it finds something. We want findings to surface in GitLab, not fail the build. Your security policies (next section) decide whether the merge request is blocked.paths:artifact — keeps the raw SARIF available for download and for downstream tools (SIEM, dashboards).rules:— runs on merge requests and the default branch, matching how GitLab’s own scanners are scheduled.
Verify the findings appear in GitLab#
Open the merge request (or pipeline) created by your commit
Wait for
semgrep-sastto completeIn the merge request, expand Security scanning. Semgrep findings appear in the same list as the built-in SAST findings, grouped under their own scanner name.
After the pipeline finishes on the default branch, the same findings appear in Secure > Vulnerability report.
Policies you set up later in this lesson treat Semgrep findings exactly like findings from any other scanner — no additional configuration required.
The same pattern works for any tool that emits SARIF — point
artifacts:reports:sastat the SARIF file. For tools that emit GitLab’s native JSON schema directly (Trivy with--format gitlabis a common one), use the sameartifacts:reports:field that matches the scan type (container_scanning,dependency_scanning, etc.).
SARIF ingestion is currently supported under
artifacts:reports:sast. Findings from other categories (DAST, dependency scanning, container scanning) should be emitted in GitLab’s native JSON schema or converted using a small wrapper script. See Security report schemas for the schemas.
Enabling security training#
GitLab can link detected vulnerabilities to remediation training from third-party providers, giving developers a fast path from “what is this finding” to “how do I fix it for good.”
Navigate to Secure > Security configuration in the left sidebar
Click the Vulnerability Management tab
Enable all of the Security training providers:
- Kontra — Interactive developer security education built around real-world examples
- Secure Code Warrior — Bite-sized secure coding lessons that map to specific vulnerabilities
- SecureFlag — Hands-on labs in a virtualized environment
Once enabled, every vulnerability page shows a Training link tailored to the specific CWE.
Understanding security policies#
Security policies let you enforce security requirements as code. Policies live in a dedicated security policy project — a separate repository that GitLab creates automatically when you create your first policy. This separation means security teams can manage policies independently of developers, which is the foundation for separation of duties.
There are four main types of policies:
- Merge Request Approval Policies — require extra approvals when conditions are met (e.g., new high-severity vulnerabilities)
- Scan Execution Policies — enforce specific scans on designated branches/schedules so they cannot be removed by a
.gitlab-ci.ymlchange - Pipeline Execution Policies — inject custom CI/CD jobs (compliance checks, custom audits) that developers cannot remove
- Vulnerability Management Policies — automate status transitions on the vulnerability report (e.g., auto-resolve vulnerabilities older than 90 days when no longer detected)
In the following sections we will set up the first three.
Creating a merge request approval policy (vulnerabilities)#
Merge request approvals are the gate before code merges. GitLab can require additional approvals when scanners detect new vulnerabilities, so insecure code cannot ship without a security review.
Navigate to Secure > Policies in the left sidebar
Click the New policy button
Click the Select policy button under Merge request approval policy
Fill out the following:
- Name:
require_maintainer_approval_for_detected_vulnerabilities - Description (optional): Requires approval from maintainers before vulnerable code can be merged
Ensure the Enabled radio button is selected under Policy status
Note that the Policy scope is set to the current project by default
Policies can also be created at the group level, scoped to multiple projects at once. This is the usual approach for enforcing consistent security requirements across an entire organization.
- Under the Rules section, create a rule with the following specifications:
When
Security ScanfromAll scannersruns against thedefault branchwithNo exceptionsand findsAnyvulnerability matching all of the following criteria:
Severity is:
All severity levels
Status is:
New,All vulnerability states
- Under the Actions section, create an action with the following criteria:
Require
1approval fromRoles:Maintainer
Under Override project approval settings, unselect all default options so the policy does not override existing project settings.
Under Fallback behavior and edge case settings, select Fail closed. This blocks the merge request when the scanner cannot run for any reason, instead of silently letting it through.
Click Configure with a merge request. You will be taken to a merge request in the security policy project that GitLab created to store your policies.
Click Merge to activate the policy.
Creating a scan execution policy (IaC scanning)#
A scan execution policy enforces that a scanner runs — even if the project’s .gitlab-ci.yml does not include it. This is how security teams guarantee coverage across many projects without depending on each project’s pipeline configuration.
We will enforce IaC scanning, which is not yet in this project’s pipeline.
Navigate to Secure > Policies in the left sidebar
Click the New policy button
Click Select policy under Scan execution policy
Fill out the following:
- Name:
enforce_iac_scanning_on_all_branches - Description (optional): Ensures IaC scanning runs on every pipeline, regardless of project CI configuration
Ensure the Enabled radio button is selected
Under the Actions section, configure:
Run
IaC Scanningscan withDefaultsettings
- Under the Conditions section, configure:
Triggers:
Triggered by pipeline event
For branches:
All branches
- Click Configure with a merge request and merge the resulting MR
The IaC scanner now runs in every pipeline. Developers cannot disable it by editing .gitlab-ci.yml — it is injected from the security policy project.
Injecting custom jobs using pipeline execution policies#
Now let’s create a Pipeline Execution Policy that injects a custom CI/CD job into the pipeline. This is the most common way to enforce compliance checks, audit logging, or additional testing that developers cannot bypass.
Pipeline execution policies can inject any custom CI/CD configuration (including security scans). If you only need to enforce that specific GitLab security scans run, prefer a Scan Execution Policy — they are simpler to author.
Importing the custom job project#
The custom job lives in a separate project. Let’s import it first.
Navigate to https://gitlab.com/projects/new (or click Projects in the left sidebar)
Select Import project
Click the Repository by URL button
Under Git repository URL, enter:
https://gitlab.com/gitlab-da/tutorials/security-and-governance/tanuki-shop-custom-pipelines.gitUnder Project URL, select the same group where you imported the Tanuki Shop project
Under Visibility Level, select Public
Click the Create project button
Wait for the import to complete.
Creating the pipeline execution policy#
Navigate back to your Tanuki Shop project
Go to Secure > Policies in the left sidebar
Click New policy
Click Select policy under Pipeline execution policy
Fill out the following:
- Name:
inject_custom_soc2_echo_job - Description (optional): Injects a custom compliance job to echo ‘SOC2 Compliance Check’ into the pipeline
Ensure Enabled is selected
Under Actions, create an action with the following criteria:
Injectinto the.gitlab-ci.ymlwith the following pipeline execution file fromTanuki Shop - Custom Pipelines
File path:
soc2.yml
File reference (Optional):
default branch
Add job name suffix:
On conflict
Leave the Additional configuration options at their defaults
Click Configure with a merge request and merge the resulting MR
You now have:
- Built-in scanners running on every pipeline (SAST + Advanced SAST, Dependency Scanning + Static Reachability, Container Scanning, Secret Detection, DAST)
- Secret Push Protection blocking secrets before they hit the server
- A third-party scanner (Semgrep) feeding findings into the same vulnerability report via SARIF
- Three security policies: a vulnerability-approval gate, an enforced IaC scan, and an injected compliance job
Next we’ll look at what all of this feels like from the developer side, in a merge request.