Skip to content
shellhacks.dev
CI/CD

How to Scan Dependencies and Secrets in CI the Safe Way

Scan dependencies, secrets and images in CI with gitleaks, trufflehog, osv-scanner and trivy: fail thresholds, scheduled runs and ignore files.

6 min read
Summarize in AI
Illustration for "How to Scan Dependencies and Secrets in CI the Safe Way": a shell script in an editor, test results, a scrolling log.

Three kinds of problems reach production unseen: a secret committed by accident, a dependency with a known vulnerability and a base image with outdated packages. Each has a scanner that runs in under a minute, and the work is wiring them so that they block what matters without crying wolf. This post gives a pipeline. It extends gitleaks and trufflehog and auditing packages. Examples use gitleaks 8.21, trivy 0.58 and GitHub Actions; the commands assume any CI.

Secrets

.github/workflows/security.ymlYAML
jobs:
  secrets:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Install gitleaks
        run: |
          curl -fsSL https://github.com/gitleaks/gitleaks/releases/download/v8.21.2/gitleaks_8.21.2_linux_x64.tar.gz | tar xz gitleaks
          ./gitleaks git --redact --verbose

fetch-depth: 0 fetches the full history, so the scan covers old commits, not only the tip. --redact keeps found secrets out of the CI log. gitleaks exits non-zero on any finding, which fails the job. A secret that reaches the remote must be treated as leaked: rotate it first, then remove it from history if you wish. TruffleHog with --only-verified checks whether a found credential is still live, which reduces noise: trufflehog git file://. --only-verified --fail.

Dependencies

Shell
npm audit --audit-level=high --omit=dev
pip-audit -r requirements.txt --strict
osv-scanner --lockfile=package-lock.json
trivy fs --scanners vuln --severity HIGH,CRITICAL --exit-code 1 .

Each compares the versions in your lockfile or manifest with a vulnerability database. --audit-level=high makes npm fail only for high and above, and --omit=dev ignores development-only packages. osv-scanner and trivy fs cover many ecosystems with one tool, which is easier in a polyglot repository. A thing to expect: many findings are in code paths you never call. Triage them, but do not ignore the category.

Images

Shell
docker build -t app:ci .
trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 app:ci

--ignore-unfixed skips vulnerabilities with no available fix, which you cannot act on. Smaller base images, such as slim or distroless, have fewer packages and so fewer findings. Rebuilding with a fresh base image often clears most of them; see image scanning.

Choose the failure policy

  • Secrets: fail always. There is no acceptable level.
  • Dependencies: fail on high and critical for production dependencies; report the rest.
  • Images: fail on fixable high and critical issues.
  • Pull requests from forks: scan, but they cannot see secrets, so use tools that need none.

A gate that fails on every low finding gets bypassed. A gate that fails on a handful of serious ones gets fixed.

Schedule and ignore with care

YAML
on:
  push:
  pull_request:
  schedule:
    - cron: "41 4 * * *"

Advisories are published after your code merged, so a nightly run finds what a push-only scan misses; see CI scheduled jobs. For accepted risks, keep an ignore file and require a comment and a date:

.trivyignoreText
# CVE-2026-12345: not reachable, we do not call the XML parser. Review by 2027-01-31.
CVE-2026-12345

Where this does not apply

Scanners find known issues, not design flaws or unknown bugs. A clean report is not a guarantee. Private and internal advisories may not be in public databases. Scanning with network access means the vulnerability database is downloaded each run, so cache it or use the tool's cache option to save time, and know that rate limits on the database host can fail a job. For the secret side, finding secrets in a repository covers local checks, and pre-commit hooks stop the problem earlier.

What each kind of scan can and cannot tell you

Three kinds of scan answer three different questions, and a pipeline usually needs all three. A secret scanner looks for credentials in the code and its history, by patterns for known token formats and by entropy for random-looking strings. It finds mistakes after they were made, and a hit means the credential is already exposed to everyone with access to the repository, so the response is to revoke and rotate and not only to delete the line. A dependency scanner compares the versions of your libraries, direct and indirect, against vulnerability databases, and reports known problems. It cannot find unknown ones, and it reports vulnerabilities in code paths you may never call, which is why triage matters. An image scanner inspects the packages in a built image, both the base image's operating system packages and the application layers. Each reports different issues, and a clean result from one says nothing about the others, so do not use one to excuse the rest.

Scans also have a time dimension. A dependency that was clean on the day of the build can have a published vulnerability a week later, which is the reason to scan on a schedule and not only on changes.

Deciding what blocks a merge

A scan that fails the build for every finding gets disabled within a week. Decide the policy deliberately. A leaked secret always blocks. For vulnerabilities, block on severity, usually critical and high, and only where a fix exists, since failing on a problem nobody can fix punishes the team for waiting. Report the rest as warnings with a link, and keep a list of accepted exceptions. Each exception needs a reason, an owner and an expiry date, so that an ignore entry for a vulnerability that does not apply is reviewed again and not forgotten. Most scanners support an ignore file, such as .trivyignore or .gitleaksignore, and the file belongs in version control with the reasons in comments. Put a time limit on how long a high-severity finding may stay open, and track the count over time, since a rising number means the policy is being bypassed.

Running scans without slowing everyone

Run the fast checks on every pull request: secret scanning of the diff and a dependency check against the lock file take seconds. Run the slower ones, such as a full image scan and a scan of the whole history, on merges and on a nightly schedule. Cache the vulnerability databases between runs, since downloading them each time is the slowest part, and pin the scanner version so a new release does not change the result unannounced. Scan the exact image you will deploy, built from the final Dockerfile, not a similar one. Run a full history scan once when you adopt a secret scanner, to find old leaks, and then scan only new commits. Keep the output where people look, as annotations on the pull request, and not buried in a log. Remember the limits: scanners reduce risk and do not eliminate it, and a pipeline that passes all of them has not been proven secure.

Making findings actionable

A report that nobody reads protects nothing, so route findings to the people who can fix them. Post a summary on the pull request that introduced the problem, open a ticket for scheduled findings with an owner, and show new issues separately from existing ones, so a developer is blamed only for what their change added. Keep a baseline of existing findings when you first adopt a scanner, fail only on new ones and burn down the backlog over time, since a first scan of a mature project can report hundreds of items and discourage everyone. Update base images regularly, because most image findings disappear with a rebuild on a current base, and automate dependency updates with a bot that opens small pull requests, which keeps each upgrade easy to review and test.

See the gitleaks project and Trivy documentation.

Also inSecurity
Taggedsecurity
Share

Keep reading

Get new posts by email

One email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.