Security is no longer a task for the last stage of a project — it’s part of the pipeline itself.
In modern DevOps, we call this “shift-left security”: catching vulnerabilities and secrets as early as possible.

In this guide, I’ll show you how to integrate Static Application Security Testing (SAST) directly in GitLab CI using open-source tools that work perfectly on GitLab Free.

We’ll combine:

  • Trivy – for vulnerability, license, and misconfiguration scanning
  • Gitleaks – for secret detection
  • An optional custom Docker image with Trivy preinstalled for faster caching

By the end, you’ll have a plug-and-play DevSecOps pipeline any engineer can reuse.

This post summarizes how I implemented static analysis in a GitLab Free CI/CD pipeline using open-source tools only.


The goal: integrate DevSecOps checks directly into CI without paid features or external services.

2. Why Static Analysis Matters in DevSecOps

Static analysis helps identify vulnerabilities, dependency issues, and hardcoded secrets before they reach production.

Benefits:

  • Catch vulnerabilities earlier (no late firefighting)
  • Prevent credential leaks in repositories
  • Build a repeatable security baseline, even with free tooling

Open-source tools like Trivy and Gitleaks make this possible for every team — no paid GitLab tiers required.

3. Architecture Overview

Static Analysis Pipeline with Open-Source DevSecOps Tools in GitLab CI

Here’s a simplified view of the DevSecOps pipeline we’ll build:

build → test → security → deploy
StageToolDescription
SecurityTrivyScans code, dependencies, containers, and IaC for vulnerabilities
SecurityGitleaksDetects secrets and credentials in codebase

Each tool runs independently in the security stage and generates JSON reports stored as artifacts.

4. Prerequisites

Before you start:

  • A GitLab Free account
  • A GitLab Runner (Docker or Shell executor)
  • A project with a .gitlab-ci.yml file
  • Optional: jq to parse JSON results

5. Step-by-Step Implementation

GitLab CI Pipeline Flow with Expanded Security Stage (Trivy & Gitleaks)

5.1. Add the Security Stage

stages:
  - build
  - test
  - security
  - deploy

5.2. Trivy Filesystem Scan (Dependencies & Licenses)

This scans your repository for known CVEs and license issues.

dependency-scanning-trivy-fs:
  image: your-registry/custom-trivy-ci:latest
  stage: security
  variables:
    TRIVY_CACHE_DIR: "/trivy-cache"
    TRIVY_TIMEOUT: "5m"
    TRIVY_SCANNERS: "vuln,license"
    TRIVY_SEVERITY: "CRITICAL,HIGH,MEDIUM,LOW,UNKNOWN"
  cache:
    key: "trivy-cache"
    paths: ["/trivy-cache/"]
    policy: pull-push
  before_script:
    - set -euo pipefail
    # Install Node deps only when package.json exists
    - |
      if [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then
        npm ci --ignore-scripts --no-audit --no-fund
      elif [ -f yarn.lock ]; then
        yarn install --ignore-scripts --frozen-lockfile --silent
      elif [ -f pnpm-lock.yaml ]; then
        pnpm install --ignore-scripts --frozen-lockfile --silent
      fi
    # (optional) install deps for nested workspaces if needed:
    # find . -name package.json -not -path "*/node_modules/*" -execdir npm ci --ignore-scripts --no-audit --no-fund \;  
      # Also install in each sub-project that has a lockfile (monorepo / non-workspace packages)
    - |
      echo "Installing Node deps where lockfiles exist in subfolders..."
      # npm
      find . -path "*/node_modules" -prune -o -name package-lock.json -print0 \
        | xargs -0 -I{} bash -c 'd=$(dirname "{}"); echo "npm ci in $d"; (cd "$d" && npm ci --ignore-scripts --no-audit --no-fund)'
      # yarn (if yarn is available)
      if command -v yarn >/dev/null 2>&1; then
        find . -path "*/node_modules" -prune -o -name yarn.lock -print0 \
          | xargs -0 -I{} bash -c 'd=$(dirname "{}"); echo "yarn install in $d"; (cd "$d" && yarn install --ignore-scripts --frozen-lockfile --silent)'
      fi
      # pnpm (if pnpm is available)
      if command -v pnpm >/dev/null 2>&1; then
        find . -path "*/node_modules" -prune -o -name pnpm-lock.yaml -print0 \
          | xargs -0 -I{} bash -c 'd=$(dirname "{}"); echo "pnpm install in $d"; (cd "$d" && pnpm install --ignore-scripts --frozen-lockfile --silent)'
      fi
  script: |
    set -eo pipefail
    trivy fs "$CI_PROJECT_DIR" \
      --scanners "$TRIVY_SCANNERS" \
      --severity "$TRIVY_SEVERITY" \
      --pkg-types os,library \
      --include-dev-deps \
      --skip-db-update \
      --cache-dir "$TRIVY_CACHE_DIR" \
      --timeout "$TRIVY_TIMEOUT" \
      --format json --output trivy-fs.json
  artifacts:
    when: always
    expire_in: 2 weeks
    paths:
      - trivy-fs.json

5.3. Trivy Image Scan (Containers)

If your project builds Docker images, scan them too.

container-scanning-trivy-image:
  image: your-registry/custom-trivy-ci:latest
  stage: security
  variables:
    IMAGE_REF: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
  script:
    - |
      set -eo pipefail
      trivy image "$IMAGE_REF" \
        --scanners "$TRIVY_SCANNERS" \
        --severity "$TRIVY_SEVERITY" \
        --pkg-types os,library \
        --ignore-unfixed \
        --skip-db-update \
        --cache-dir "$TRIVY_CACHE_DIR" \
        --timeout "$TRIVY_TIMEOUT" \
        --format json --output trivy-image.jsonjson --output trivy-image.json
  artifacts:
    when: always
    paths: [trivy-image.json]

5.4. Trivy Config Scan (IaC Misconfigurations)

Scan Terraform, Kubernetes manifests, and Docker Compose files for misconfigurations.

iac-scanning-trivy-config:
  image: aquasec/trivy:latest
  stage: security
  script:
    - |    
      set -eo pipefail

      # Build safe --skip-dirs (absolute and existing)
      SKIPS=( ".git" "node_modules" "doc" "docs" "examples" "tmp" "dist" "build" )
      ARGS=()
      for d in "${SKIPS[@]}"; do
        [ -d "$d" ] && ARGS+=( --skip-dirs "$(pwd)/$d" )
      done
      echo "Trivy args: ${ARGS[*]}"

      RESULT=0
      trivy config \
        --severity HIGH,CRITICAL \
        --timeout "$TRIVY_TIMEOUT" \
        --cache-dir "$TRIVY_CACHE_DIR" \
        --format json -o trivy-config.json \
        --exit-code 1 \
        "${ARGS[@]}" \
        . || RESULT=$?; : ${RESULT:=0}

      exit $RESULT 
  artifacts:
    when: always
    paths: [trivy-config.json]

5.5. Gitleaks Secret Detection

Detect hardcoded secrets like tokens, passwords, or API keys.

secret-detection-gitleaks:
  image:
    name: zricethezav/gitleaks:latest
    entrypoint: [""]
  stage: security
  needs: []
  variables:
    GITLEAKS_REPORT_JSON: "gitleaks-report.json"
    GITLEAKS_IGNORE_PATH: "."
    GITLEAKS_EXIT_CODE: "1"
    GITLEAKS_REDACT: "100"
    GITLEAKS_MAX_DECODE_DEPTH: "2"
    GITLEAKS_MAX_ARCHIVE_DEPTH: "2"
  script:
    - gitleaks version || true
    - |
      set -eo pipefail
      RESULT=0
      # Ensure env vars don't implicitly force a missing config
      unset GITLEAKS_CONFIG GITLEAKS_CONFIG_TOML || true

      echo "[gitleaks:dir] scanning working directory..."
      gitleaks dir . \
        --verbose \
        --no-banner --no-color \
        --redact "${GITLEAKS_REDACT}" \
        --max-decode-depth "${GITLEAKS_MAX_DECODE_DEPTH}" \
        --max-archive-depth "${GITLEAKS_MAX_ARCHIVE_DEPTH}" \
        -i "${GITLEAKS_IGNORE_PATH}" \
        --report-format json --report-path "${GITLEAKS_REPORT_JSON}" \
        --exit-code "${GITLEAKS_EXIT_CODE}" || RESULT=$?; : ${RESULT:=0}

      exit $RESULT
  artifacts:
    <<: *security_artifacts_defaults
    paths:
      - gitleaks-report.json
  extends: .security_scan_rules

6. Optimizing Your CI Pipeline with a Custom Trivy Image

By default, every Trivy job downloads its vulnerability databases on each run — adding 60–120 seconds to the pipeline.
To eliminate that, we can build a custom base image that includes Trivy and its pre-downloaded databases.

6.1. Dockerfile for a Pre-Cached Trivy Image

FROM debian:stable-slim

# Bring VERSION arg into scope for build steps
ARG VERSION

# Base tools
RUN apt-get update -y \
 && apt-get install -y --no-install-recommends \
    ca-certificates curl jq git gettext-base \
 && update-ca-certificates \
 && rm -rf /var/lib/apt/lists/*

# yq (useful to manipulate YAML in CI scripts)
RUN curl -fsSL https://github.com/mikefarah/yq/releases/download/v4.44.3/yq_linux_amd64 \
  -o /usr/local/bin/yq && chmod +x /usr/local/bin/yq

# Trivy (install via official release)
RUN curl -fsSL https://github.com/aquasecurity/trivy/releases/download/v${VERSION}/trivy_${VERSION}_Linux-64bit.tar.gz \
  | tar -xz -C /usr/local/bin trivy

# Trivy HTML templates
RUN mkdir -p /usr/local/share/trivy/templates \
 && curl -fsSL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/html.tpl \
  -o /usr/local/share/trivy/templates/html.tpl

ENV TRIVY_CACHE_DIR=/trivy-cache
ENV TRIVY_TIMEOUT=10m

RUN mkdir -p ${TRIVY_CACHE_DIR}

# Pre-download vulnerability databases (base + Java)
RUN trivy fs --download-db-only --cache-dir ${TRIVY_CACHE_DIR} -d \
 && trivy fs --download-java-db-only --cache-dir ${TRIVY_CACHE_DIR} -d

VOLUME ["/trivy-cache"]

ENTRYPOINT [""]

6.2. Build and Push the Image

You can build and push this image to your own registry:

docker build -t your-registry/custom-trivy-ci:latest --build-arg VERSION=0.58.1 .
docker push your-registry/custom-trivy-ci:latest

This improves CI efficiency by:

  • Removing repeated DB downloads
  • Reusing cached vulnerability definitions
  • Keeping all jobs on a consistent Trivy version

7. Handling Noise and False Positives

Reduce unnecessary findings with ignore files.

.trivyignore

CVE-2024-12345
CVE-2023-67890
**/tests/**
**/examples/**

.gitleaksignore

tests/**
examples/**
.env.sample

8. Performance Tips

  • Use cache mounts for /trivy-cache to persist DBs between jobs.
  • Run security jobs in parallel for speed.
  • Start with allow_failure: true during adoption, then enforce once stable.

9. Troubleshooting

IssueCauseFix
Slow scanDB redownload each runUse custom Trivy image
Too many false positivesLow severity filtersAdjust TRIVY_SEVERITY
Secrets flagged in test dataNon-production contentAdd to .gitleaksignore

10. Conclusion

Security shouldn’t be a luxury feature — it’s part of a healthy CI/CD workflow.
With this setup, you’ve built a fully open-source DevSecOps pipeline for GitLab Free that runs efficiently, detects vulnerabilities early, and prevents secret leaks.

From here, you can extend the stack with:

  • Trivy HTML reports
  • Dependency dashboards in GitLab
  • Notifications on findings severity

Shift left. Stay secure. And keep your pipelines fast.

11. References

All configs are available here: GitHub – mayens/gitlab-ci-devsecops-sast