Skip to content

Secure your rust code with Trivy: local scan and gitHub actions ci/cd

Published: at 03:00 PM

Requirements: Rust installed (rustup), a GitHub account.

As a developer, security should not be the last thing you think about. You write code, add dependencies, and ship, but each dependency you add can bring its own security issues.

Trivy is a free and open source security scanner made by Aqua Security. It checks your project for known vulnerabilities and helps you fix them before they reach production.

Trivy can scan many things:

In this post, we will focus on what matters most for a developer: scanning your source code and its dependencies. We will use a small Rust app called speedtest-rs as our example project.

Installing Trivy

Trivy is a single binary there is nothing complex to set up.

macOS (with Homebrew)

If you are on macOS and already have Homebrew, just run:

brew install trivy

Ubuntu / Debian

Add the Trivy repository and install it with apt:

sudo apt-get install wget gnupg
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | gpg --dearmor | sudo tee /usr/share/keyrings/trivy.gpg > /dev/null
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb generic main" | sudo tee -a /etc/apt/sources.list.d/trivy.list
sudo apt-get update
sudo apt-get install trivy

Check that it works

trivy --version

project: speedtest-rs

To show how Trivy works, we need a real Rust project with dependencies. We will build a small app that measures your internet download speed by downloading a test file and checking how long it takes.

Create the project

cargo new speedtest-rs
cd speedtest-rs

Cargo.toml

[package]
name = "speedtest-rs"
version = "0.1.0"
edition = "2024"

[dependencies]
reqwest = "0.13.2"
tokio = { version = "1.49.0", features = ["macros", "rt-multi-thread"] }

src/main.rs

use std::time::Instant;
use reqwest::get;

const TEST_URL: &str = "https://proof.ovh.net/files/10Mb.dat";

#[tokio::main]
async fn main() {
    println!("Starting speed test...");
    let start = Instant::now();
    let response = get(TEST_URL).await.expect("Failed to download file");
    let bytes = response.bytes().await.expect("Failed to read response bytes");
    let duration = start.elapsed();

    let size_mb = bytes.len() as f64 / (1024.0 * 1024.0);
    let speed_mbps = (size_mb * 8.0) / duration.as_secs_f64();

    println!("Downloaded : {:.2} MB", size_mb);
    println!("Time       : {:.2} seconds", duration.as_secs_f64());
    println!("Speed      : {:.2} Mbps", speed_mbps);
}

Build the project

cargo build

When you run cargo build, Cargo creates a Cargo.lock file. This file records the exact version of every dependency your project uses. This is the file Trivy will read to find vulnerabilities.

Make sure to commit Cargo.lock to your repository. Without it, Trivy cannot check your full dependency tree.

Scanning your Rust code locally

When Trivy scans a Rust project, it reads Cargo.lock and builds a full list of all dependencies. Then it checks each one against several security databases:

Basic scan command

Run this from the root of your project:

trivy fs .

Trivy will automatically find your Cargo.lock and show a report like this:

Cargo.lock (cargo)

Total: 3 (UNKNOWN: 0, LOW: 1, MEDIUM: 2, HIGH: 0, CRITICAL: 0)

┌──────────┬───────────────┬──────────┬───────────────────┬───────────────┬──────────────────────────────────┐
│ Library  │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title                            │
├──────────┼───────────────┼──────────┼───────────────────┼───────────────┼──────────────────────────────────┤
│ openssl  │ CVE-2023-XXXX │ MEDIUM   │ 0.10.55           │ 0.10.60       │ OpenSSL: potential memory issue  │
└──────────┴───────────────┴──────────┴───────────────────┴───────────────┴──────────────────────────────────┘

Useful options

Only show HIGH and CRITICAL issues — ignore low-risk ones:

trivy fs --severity HIGH,CRITICAL .

Save the report as JSON — useful if you want to store or process results:

trivy fs --format json --output trivy-report.json .

Save the report as SARIF — the format used by GitHub Security tab:

trivy fs --format sarif --output trivy-results.sarif .

Scan only for vulnerabilities (skip config checks):

trivy fs --scanners vuln .

Also check for exposed secrets (API keys, tokens, etc.):

trivy fs --scanners vuln,secret .

Adding Trivy to a GitHub Actions pipeline

Running Trivy locally is great. But the real value comes when it runs automatically on every push or pull request

Create the workflow file

Create this file in your project: .github/workflows/trivy-security-scan.yml

name: Security Scan with Trivy

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  trivy-scan:
    name: Trivy Vulnerability Scan
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write
      actions: read

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Rust
        uses: actions-rust-lang/setup-rust-toolchain@v1
        with:
          toolchain: stable
          cache: true

      - name: Build project
        run: cargo build --release

      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: "fs"
          scan-ref: "."
          format: "sarif"
          output: "trivy-results.sarif"
          severity: "CRITICAL,HIGH,MEDIUM"
          scanners: "vuln,secret"
          ignore-unfixed: true

      - name: Upload Trivy results to GitHub Security tab
        uses: github/codeql-action/upload-sarif@v4
        if: always()
        with:
          sarif_file: "trivy-results.sarif"

Why these settings matter

security-events: write permission — this allows the workflow to write results into the GitHub Security(Code Scanning tab). Without it, the upload step will fail silently.

if: always() on the upload step — by default, a step is skipped if the previous one failed. Trivy returns an error code when it finds vulnerabilities. Without if: always(), the results would never be uploaded when issues are found.

SARIF format — SARIF (Static Analysis Results Interchange Format) is the standard format that GitHub Code Scanning understands. It lets GitHub display vulnerabilities directly in the repository interface with file-level annotations.

Block pull requests on critical issues

You can also make the pipeline fail if a critical vulnerability is found. This will block the pull request from being merged until the issue is fixed:

- name: Run Trivy (block on CRITICAL)
  uses: aquasecurity/trivy-action@master
  with:
    scan-type: "fs"
    scan-ref: "."
    format: "table"
    severity: "CRITICAL"
    exit-code: "1"        # Fail the job if a CRITICAL issue is found
    ignore-unfixed: true  # Skip issues that have no fix yet

What you see in GitHub

Once the workflow runs, results appear in two places:

  1. Actions tab: the full console output of each Trivy scan
  2. Security tab: a list of all vulnerabilities, where you can filter, assign, and mark them as resolved

Conclusion

Trivy is one of the easiest security tools to add to your workflow. It takes just a few minutes to install, runs with a single command, and integrates cleanly into GitHub Actions.

Here is a quick summary of what we covered:

The speedtest-rs project we used here is intentionally small, but the same setup works for any Rust project(big or small).

Resources:

Code, Peace and Love