Shift Left Security using GitLab CI

by Aug 14, 2025DevOps

Printer Icon
f

In modern software development, security can no longer be an afterthought. Vulnerabilities introduced early in development, whether caused by dependencies, bad coding practices, or misconfigurations, can lead to serious issues once the software is deployed.

That’s why shifting the approach from tackling vulnerabilities after development to an early stage has become a fundamental principle of DevSecOps.

While working in a company or even as freelance, it is important to proactively approach this challenge. One way to do this is by integrating CI/CD security jobs directly into GitHub actions or GitLab pipelines. These jobs can be triggered e.g. during Merge Requests (MR) events, providing an early visibility into potential risks without slowing down developer workflows.

This article outlines some examples of tools that can be used, how can they be structured, the benefits of embedded security scanning directly into CI/CD pipelines.

Objective: Prevent Security Debt before production

It’s common that security reviews often come late in the development cycle, or they might never happen in some organizations. But by the time a vulnerability is discovered in production, it can be too late since its remediation can become costly, either in time or resources, and in worst case scenarios, systems may already be compromised.

What can be done:

  • Provide developers with early feedback.
  • Standardize security checks in different projects.

To achieve this in GitLab, a project that serves as a centralized repository of CI/CD job templates for security scanning can be created.

Chosen Tools

Since teams and organizations often run under a budget and there might not be capacity to include tools that can be expensive, only three widely adopted open-source tools are used.

    1. Trivy: Trivy is a powerful scanner that performs:
      • Filesystem-level vulnerability detection.
      • Secret exposure detection.
      • Misconfiguration checks (e.g. insecure Dockerfiles).

It’s fast, easy to configure and supports multiple scanning modes (filesystem, image, repository, etc.).

    • Semgrep: Semgrep is a static analysis tool (SAST) that parses the source code to identify patterns matching known security rules or custom policies. It detects insecure coding practices, supports rulesets for different languages like JavaScript, Python, and Go, and is highly customizable, making it ideal for enforcing consistent security standards across projects.
    • OSV-Scanner: OSV-Scanner is a tool that can scan your project dependencies using Google’s Open-Source Vulnerability (OSV) database. It focuses on package level Common Vulnerabilities and Exposures (CVE), supports popular ecosystems like NPM, PIP, Go Modules, and it’s both lightweight and fast.

    How to Structure GitLab CI Jobs for Security

    Each job can be defined as a template that can be easily imported into any project’s GitLab CI YAML file.

    .semgrep-scan:
      image: python:3.12-alpine
      variables:
        SEMGREP_CONFIG: "r/all"
        SEMGREP_FLAGS: "--severity=Error"
      before_script:
        - pip install semgrep
      script:
        - semgrep scan --config="$SEMGREP_CONFIG" $SEMGREP_FLAGS > semgrep-results.txt
        - cat semgrep-results.txt

    The official semgrep image behaved differently compared to the local Command Line Interface (CLI). By using the Python image and installing Semgrep manually, the tool’s behavior became more predictable, and results matched those from local runs.

    The config variable in this job is set to r/all which is to scan code with all the rules, but it can be changed to e.g. p/ci or a local file that needs to be in the template repository if preferred.

    Flags variable is used to enable additional CLI options, in this case we are using the severity filter to only get ERROR or higher severity, a different option could be WARNING.

    In the script the scan command is made non-blocking by using allow_failure set to true, so the pipeline doesn’t fail, but ensuring that the findings are still visible in the merge request jobs or overview (if you decide to use artifacts).

    .trivy-scan:
      image:
        name: aquasec/trivy:latest
        entrypoint: [""]
      variables:
        TRIVY_SCAN_CONFIG: "true"
        TRIVY_SCAN_SECRET: "true"
        TRIVY_SCAN_VULN: "false"
        TRIVY_SEVERITY: ""
      script:
        - |
          SCANNERS=""
          [ "$TRIVY_SCAN_CONFIG" = "true" ] && SCANNERS="${SCANNERS},misconfig"
          [ "$TRIVY_SCAN_SECRET" = "true" ] && SCANNERS="${SCANNERS},secret"
          [ "$TRIVY_SCAN_VULN" = "true" ] && SCANNERS="${SCANNERS},vuln"
          SCANNERS="${SCANNERS#,}"
          SEVERITY_OPTION=""
          [ -n "$TRIVY_SEVERITY" ] && SEVERITY_OPTION="--severity=$TRIVY_SEVERITY"
          trivy fs --scanners "$SCANNERS" $SEVERITY_OPTION . > trivy-results.txt
        - cat trivy-results.txt
    

    TRIVY_SCAN_* enable the detection of misconfiguration, secrets exposures and known CVEs.

    TRIVY_SCAN_VULN defaults to false since Semgrep already covers some vulnerability patterns in code, though in a different format, and we want to focus on leveraging the other two Trivy scanners.

    TRIVY_SEVERITY is like Semgrep filter, but in this case it’s a comma separated list to include the severity vulnerabilities, e.g. HIGH,CRITICAL

    .osv-scan:
      image: alpine:latest
      before_script:
        - apk update
        - apk add --no-cache osv-scanner git jq
      script:
        - osv-scanner scan --output=osv-results.txt -r . || true
        - cat osv-results.txt
    With OSV-Scanner, we only want to check for dependencies. For that, the -r flag is used to recursively scan directories containing manifest or lock files.

    How to use the templates

    Using the templates is simple, you need to include these templates into your pipeline, then create a job that is extended by the templates where you can tweak rules, artifacts, when to run, variables, etc. per project.

    For example, here is how the template looks when integrated into a project’s pipeline:

    stages:
      - security
    
    include:
      - project: "org/ci-security-jobs"
        file: "/jobs/semgrep.gitlab-ci.yml"
        ref: "main"
      - project: "org/ci-security-jobs"
        file: "/jobs/osv.gitlab-ci.yml"
        ref: "main"
      - project: "org/ci-security-jobs"
        file: "/jobs/trivy.gitlab-ci.yml"
        ref: "main"
    
    security:osv:
      extends: .osv-scan
      stage: security
      allow_failure: true
      rules:
        - if: $CI_PIPELINE_SOURCE == "merge_request_event"
          when: manual
    
    security:semgrep:
      extends: .semgrep-scan
      stage: security
      allow_failure: true
      variables:
        SEMGREP_CONFIG: "r/all"
        SEMGREP_FLAGS: "--severity=ERROR"
      rules:
        - if: $CI_PIPELINE_SOURCE == "merge_request_event"
          when: manual
    
    security:trivy:
      extends: .trivy-scan
      stage: security
      allow_failure: true
      variables:
        TRIVY_SCAN_VULN: "true"
        TRIVY_SEVERITY: "HIGH,CRITICAL"
      rules:
        - if: $CI_PIPELINE_SOURCE == "merge_request_event"
          when: manual
    
    Note that the include must be related to the path where you created the repository to store the templates.

    To avoid slowing down all pipelines, these security jobs are set to run only on Merge Requests using the manual flag, so developers or reviewers can trigger them when needed.

    Downsides of this approach

    While introducing these jobs across repositories can improve code quality and raise awareness of security best practices, their effectiveness ultimately depends on the developers. These tools are only impactful if developers know how to act on the findings and if the security practices have been successfully adopted into their daily workflows.

    Current job examples scan the whole codebase, so if they are used as they are, scans might be slow.

    Further Improvements

    These jobs can be optimized to scan only the changed files instead of performing a full scan on every run. If a full scan is still required, a separate template job can be added, ideally as a manual job triggered on the dev branch after a merge request has been merged.

    Improving the results can also be done, as some tools offer an HTML output that can be implemented on GitLab pages or as downloadable files, making it easier to navigate through scan reports.

    Final Thoughts

    Security is not just a compliance checkbox on a list. It is a continuous process that starts the moment code is written, not the day before release.

    By creating and sharing reusable CI/CD job templates for vulnerability detection and pushing for their adoption, these could be an enabler for a scalable, developer friendly way to bake security into an organization pipeline.

    If a team is new or has just begun to integrate security or tackling vulnerabilities, don’t wait for the perfect solution. Start small, measure impact and iterate, focusing first in critical vulnerabilities, if any, and remember: The earlier you catch vulnerabilities, the less painful they are to fix.

    If you want to scan for these vulnerabilities locally, remember that most of the tools offer a CLI version useful for local scans.

    Bonus: Cloud & SaaS Tools

    These additional tools can help enhance vulnerability detection early in development, if they align with your team’s workflow and budget.

    BoostSecurity: Can be used for CI/CD pipeline hardening, SBOM tracking, secrets scanning, and it integrates well with GitLab or GitHub, and can be introduced gradually, starting with scan-only mode, and then enforcing policies as adoption grows.

    It can also be used along GitLab to automate comments in MRs when a vulnerability is found, providing code lines, and possible solution, along with a button to indicate if it was a false positive.

    Snyk: Offers Static Application Security Testing (SAST), Software Composition Analysis (SCA) and container scanning. It integrates easily with platforms like GitLab, supports auto-remediation by opening fix MRs, and can be configured to scan only active repositories.

    References

    0 Comments

    Submit a Comment

    Related Blog Posts