Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
38eeab5
ci: add Trivy security scan to PR pipeline
yaroslavmokflmg Aug 3, 2026
e47e7b4
ci: harden trivy gate — no persisted git credentials, visible warning…
yaroslavmokflmg Aug 3, 2026
9c899aa
ci: make trivy gate non-blocking (allow fail) until devs have capacity
yaroslavmokflmg Aug 5, 2026
a333029
ci: trivy code+docker scan in one job, in-build image scan, per-scan …
yaroslavmokflmg Aug 5, 2026
7ea71c1
ci: merge all trivy reports into one artifact per run, skip empty rep…
yaroslavmokflmg Aug 5, 2026
d3e6f47
ci: merge all trivy reports into one artifact per run, dedupe upload …
yaroslavmokflmg Aug 5, 2026
07a1758
ci: compact trivy action — piped scans, unified evaluate, single merg…
yaroslavmokflmg Aug 5, 2026
21c7f77
ci: report unique vulnerability count in trivy error message
yaroslavmokflmg Aug 5, 2026
21c0dab
ci: rename scan_code/report jobs, split scan_code and scan_image repo…
yaroslavmokflmg Aug 18, 2026
8d44065
Merge remote-tracking branch 'origin/main' into security/trivy
yaroslavmokflmg Aug 18, 2026
b8821f2
ci: rename scan steps — Scan code / Scan image, action Trivy -> Scan
yaroslavmokflmg Aug 18, 2026
c5b6fc3
ci: rename scan steps — Scan code / Scan image, action name Vulnerabi…
yaroslavmokflmg Aug 18, 2026
e09f9e0
ci: point error message to scan_code/scan_image artifact
yaroslavmokflmg Aug 18, 2026
3dda75d
ci: per-service code scan matrix with root coverage, merge artifacts …
yaroslavmokflmg Aug 21, 2026
e24c2fd
ci: strip trailing whitespace in test.yml
yaroslavmokflmg Aug 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions .github/steps/trivy/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
name: "Vulnerability Scan"
description: "Trivy PR security scans. scan: code = repo dependencies (HIGH/CRITICAL) + Dockerfile base images (CRITICAL), image = image built in the pipeline (HIGH/CRITICAL). Each scan uploads its TSV report as an artifact"

inputs:
scan:
description: "code | image"
required: true
maven-token:
description: "Token for internal GitHub Packages: Maven resolution (scan: code) and docker build secret (scan: image)"
required: false
default: ${{ github.token }}
image-name:
description: "Service name from the matrix; used in the artifact name"
required: false
path:
description: "Directory to scan (scan: code), e.g. a single service; defaults to the whole repo"
required: false
default: "."
skip-dirs:
description: "Comma-separated directories to exclude from the code scan (e.g. service dirs already scanned by the matrix)"
required: false
default: ""
dockerfile:
description: "Path to the service Dockerfile (scan: image)"
required: false
context:
description: "Docker build context (scan: image)"
required: false

runs:
using: composite
steps:
- name: Install Trivy
uses: aquasecurity/setup-trivy@v0.3.1
with:
version: v0.73.0
cache: true

- name: Scan dependencies and Dockerfile base images
if: inputs.scan == 'code'
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.maven-token }}
GITHUB_ACTOR: ${{ github.actor }}
Comment on lines +42 to +44

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repository files of interest:"
git ls-files | grep -E '^\.(github/(steps/trivy/action\.yml|workflows/test\.yml)|mvn/settings\.xml|pom\.xml)' || true

echo
echo "test.yml excerpt:"
if [ -f .github/workflows/test.yml ]; then
  nl -ba .github/workflows/test.yml | sed -n '1,120p'
fi

echo
echo "trivy action.yml excerpt:"
if [ -f .github/steps/trivy/action.yml ]; then
  nl -ba .github/steps/trivy/action.yml | sed -n '1,120p'
fi

echo
echo "Search for maven-token / package permission / settings usage:"
rg -n "maven-token|GITHUB_TOKEN|permissions:|packages:|persist-credentials|\.mvn/settings\.xml|settings\.xml|mvn\s+-|dependency:go-offline" .github pom.xml || true

Repository: flamingo-stack/fleetmdm

Length of output: 309


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "test.yml excerpt:"
if [ -f .github/workflows/test.yml ]; then
  awk '{printf "%6d\t%s\n", NR, $0}' .github/workflows/test.yml | sed -n '1,140p'
fi

echo
echo "trivy action.yml excerpt:"
if [ -f .github/steps/trivy/action.yml ]; then
  awk '{printf "%6d\t%s\n", NR, $0}' .github/steps/trivy/action.yml | sed -n '1,120p'
fi

echo
echo "Search for relevant identifiers:"
rg -n "maven-token|GITHUB_TOKEN|permissions:|packages:|persist-credentials|\.mvn/settings\.xml|settings\.xml|mvn\s|dependency:go-offline|github\.event\.pull_request" .github pom.xml || true

Repository: flamingo-stack/fleetmdm

Length of output: 12620


🌐 Web query:

Maven Settings reference -s settings and -gs global settings can invoke code plugin extensions

💡 Result:

Maven's settings.xml and global settings files cannot directly invoke or register code plugin extensions [1][2][3]. The Maven settings files (~/.m2/settings.xml and ${MAVEN_HOME}/conf/settings.xml) are designed for environment-specific configuration, such as local repository paths, authentication credentials, proxies, and profile definitions [2][4][3]. They do not possess the capability to load or activate Maven extensions [1][5]. Maven extensions, which adjust build behavior or add functionality to the Maven Core, are instead configured through the following dedicated mechanisms [5][6]: 1..mvn/extensions.xml: The standard, project-level approach for registering extensions [1][5]. You define the extension using its Maven coordinates (groupId, artifactId, version) within this file located in the project's root .mvn/ directory [1][7]. 2. Project-level Build Extensions: Configured within a project's pom.xml under <build><extensions> or by setting <extensions>true</extensions> within a specific plugin configuration [5]. 3. Command Line: Extensions can be loaded temporarily using the -Dmaven.ext.class.path=extension.jar argument [1][5][6]. While settings files can define plugin groups (via the <pluginGroups> element) to simplify command-line execution by allowing Maven to resolve plugin group IDs automatically, this is distinct from invoking or loading code extensions [2][8].

Citations:


🌐 Web query:

Maven dependency plugin lifecycle phases extensions settings include extensions in dependency tree documentation

💡 Result:

The Apache Maven Dependency Plugin does not have a dedicated configuration setting or lifecycle phase parameter to explicitly "include extensions" in the dependency tree. In Maven, extensions (artifacts with the type maven-extension) are typically loaded at the start of the build process to extend core Maven functionality and are generally not managed as standard project dependencies that appear in the typical mvn dependency:tree output [1][2][3]. Key points regarding dependency tree and plugin configuration: 1. Dependency Tree Filtering: The dependency:tree goal allows filtering by group ID, artifact ID, type, and version using the includes and excludes parameters [1][4][5]. Because extensions have a specific maven-extension type, you can attempt to include them in the tree output by specifying this type in your filter pattern if they are present in the project's dependency graph [1][4]. The filter syntax follows the pattern [groupId]:[artifactId]:[type]:[version] [1][4]. 2. Dependency Tree Scope: You can use the scope parameter to filter the dependency tree resolution by a specific scope (e.g., compile, runtime, test), or leave it empty to include all scopes [1][2]. 3. Plugin Lifecycle: The Maven Dependency Plugin goals (like tree, list, copy-dependencies) can be bound to any lifecycle phase via the standard Maven plugin configuration in your pom.xml [6]. However, they do not inherently alter how extensions are loaded or reported [1][7]. If you are trying to view or verify build extensions, these are usually defined in the <build><extensions> section of the pom.xml or via project-level metadata, rather than as standard dependencies that the dependency:tree goal is designed to visualize [3].

Citations:


🌐 Web query:

search.maven.org metadata maven extension type artifact extensions repository documentation

💡 Result:

In Apache Maven, the term "extension" is used in two distinct, unrelated contexts that are often confused [1]. 1. Maven Build Extensions (Functional Extensions) These are JAR artifacts used to extend Maven's core functionality (e.g., adding wagon providers for repository transport or lifecycle enhancements) [2]. They are not defined by a "type" in the dependency sense, but are instead loaded via specific configurations in the project POM or command line [2]. - Configuration: Declared in the section of the pom.xml or via.mvn/extensions.xml [2][3]. - Documentation: Refer to the Maven guide on Using Extensions [2]. 2. Artifact Types and Extensions (File Extensions) In the context of repository metadata and dependency declarations, "extension" refers to the file format of an artifact (e.g., jar, pom, war) [1]. - Type: The element in a dependency declaration is used to determine the artifact's file extension, classifier, and how it is handled in the build (e.g., whether it is added to the classpath) [1][4][5]. - Mapping: Maven core defines default handlers that map specific "types" to their corresponding file extensions (e.g., the 'java-source' type maps to a 'jar' extension with a 'sources' classifier) [1][5]. Regarding search.maven.org: - Status: This service has been superseded by https://central.sonatype.com [6]. While the web interface has been redirected, the API remains available for programmatic access [6]. - Metadata: Repository metadata, including artifact file extensions, can be explored via the Central Repository REST API [7][8]. When searching, you can filter by artifact characteristics, though the distinction between a "build extension" and an "artifact extension" (file type) remains essential for interpreting results correctly [1][8].

Citations:


Remove package credentials from the pull-request scan.

trivy_scan checks out PR code, grants packages: read, and passes the GitHub token into Maven while also loading .mvn/settings.xml from the PR. Treat the packages scope as untrusted for this workflow and do not expose the token to untrusted Maven configuration.

  • .github/steps/trivy/action.yml#L21-L23: do not export maven-token as GITHUB_TOKEN for pull-request scans.
  • .github/steps/trivy/action.yml#L37-L39: do not load .mvn/settings.xml from the pull request when a credential is available; use a trusted settings file if credentials are required.
  • .github/workflows/test.yml#L40-L42: remove packages: read from the untrusted PR scan or keep credentials out of untrusted Maven execution.
📍 Affects 2 files
  • .github/steps/trivy/action.yml#L21-L23 (this comment)
  • .github/steps/trivy/action.yml#L37-L39
  • .github/workflows/test.yml#L40-L48
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/steps/trivy/action.yml around lines 21 - 23, Remove the maven-token
export as GITHUB_TOKEN in .github/steps/trivy/action.yml lines 21-23, and
prevent credentialed Maven execution from loading the PR-controlled
.mvn/settings.xml at lines 37-39 by using a trusted settings file when
credentials are required. In .github/workflows/test.yml lines 40-48, remove
packages: read from the untrusted PR scan or otherwise ensure no credentials
reach PR-controlled Maven execution.

Source: Linters/SAST tools

run: |
set -euo pipefail
dir="${{ inputs.path }}"
if [ -f "$dir/pom.xml" ]; then # warm ~/.m2 via Google's Central mirror so trivy sees parent-managed versions
echo '<settings><mirrors><mirror><id>google-central</id><url>https://maven-central.storage-download.googleapis.com/maven2/</url><mirrorOf>central</mirrorOf></mirror></mirrors></settings>' > /tmp/mirror.xml
mvn -B -q -fn -DskipTests -gs /tmp/mirror.xml -f "$dir/pom.xml" $([ -f .mvn/settings.xml ] && echo '-s .mvn/settings.xml') dependency:go-offline | tee /tmp/mvn.log || true
! grep -q '\[ERROR\]' /tmp/mvn.log || echo "::warning::Some Maven dependencies could not be resolved; scan depth may be reduced"
fi
skip="${{ inputs.skip-dirs }}"
targs=(--no-progress --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --offline-scan --format json)
[ -n "$skip" ] && targs+=(--skip-dirs "$skip")
trivy fs "${targs[@]}" "$dir" |
jq -r '.Results[]? as $r | $r.Vulnerabilities[]? | [.Severity, .PkgName, .InstalledVersion, (.FixedVersion // "-"), .VulnerabilityID, $r.Target] | @tsv' | sort -u > "trivy-${GITHUB_REPOSITORY##*/}-code-report.tsv"

out="trivy-${GITHUB_REPOSITORY##*/}-docker-report.tsv"; : > "$out"
fargs=(-name 'Dockerfile*' -not -path '*/.git/*' -not -path '*/node_modules/*')
[ -n "$skip" ] && for d in ${skip//,/ }; do fargs+=(-not -path "*/${d}/*" -not -path "${d}/*"); done
find "$dir" "${fargs[@]}" -print0 |
xargs -0 -r awk 'toupper($1)=="FROM" {i=$2; if (i ~ /^--/) i=$3; print i; if (toupper($3)=="AS") print "~"$4; if (toupper($4)=="AS") print "~"$5}' | sort -u > /tmp/from.txt
for img in $(grep -v '^~' /tmp/from.txt); do
[ "$img" = scratch ] && continue
case "$img" in *'$'*) echo "::warning::Skipping base image with unresolved variable: ${img}"; continue ;; esac
grep -qxF "~$img" /tmp/from.txt && continue
trivy image --no-progress --scanners vuln --severity CRITICAL --ignore-unfixed --format json "$img" |
jq -r --arg img "$img" '.Results[]? as $r | $r.Vulnerabilities[]? | [.Severity, .PkgName, .InstalledVersion, (.FixedVersion // "-"), .VulnerabilityID, $img] | @tsv' >> "$out" ||
echo "::warning::Base image ${img} could not be scanned (pull/scan error); it was NOT checked"
done
sort -u "$out" -o "$out"
find . -maxdepth 1 -name 'trivy-*.tsv' -empty -delete

- name: Scan built image
if: inputs.scan == 'image'
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.maven-token }}
GITHUB_ACTOR: ${{ github.actor }}
run: |
set -euo pipefail
docker buildx build --platform linux/amd64 --secret id=GITHUB_TOKEN,env=GITHUB_TOKEN --build-arg GITHUB_ACTOR="$GITHUB_ACTOR" \
-f "${{ inputs.dockerfile }}" --output type=oci,dest=/tmp/image.tar "${{ inputs.context }}"
out="trivy-${GITHUB_REPOSITORY##*/}-image-report-${{ inputs.image-name }}.tsv"
trivy image --no-progress --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --input /tmp/image.tar --format json |
jq -r '.Results[]? as $r | $r.Vulnerabilities[]? | [.Severity, .PkgName, .InstalledVersion, (.FixedVersion // "-"), .VulnerabilityID, $r.Target] | @tsv' | sort -u > "$out"
find . -maxdepth 1 -name 'trivy-*.tsv' -empty -delete

- name: Upload report
uses: actions/upload-artifact@v4
with:
name: ${{ inputs.scan == 'code' && (inputs.image-name && format('scan_code-{0}', inputs.image-name) || 'scan_code') || format('scan_image-{0}', inputs.image-name) }}
path: trivy-*.tsv
if-no-files-found: ignore

- name: Evaluate
shell: bash
env:
ARTIFACT: ${{ inputs.scan == 'code' && 'scan_code' || 'scan_image' }}
run: |
set -euo pipefail
files=$(ls trivy-*.tsv 2>/dev/null) || { echo "No vulnerabilities found."; exit 0; }
column -t -s "$(printf '\t')" $files
echo "::error::Trivy found $(cut -f1,2,5 $files | sort -u | wc -l | tr -d ' ') unique HIGH/CRITICAL vulnerabilities ($(cat $files | wc -l | tr -d ' ') occurrences across modules) — full report in the ${ARTIFACT} artifact"
exit 1
22 changes: 21 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,26 @@ jobs:
github.event_name == 'pull_request' &&
!github.event.pull_request.draft

scan:
name: "Scan"
runs-on: ubuntu-latest
needs: [changes]
permissions:
contents: read
packages: read
if: github.event_name == 'pull_request'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false

- name: Scan code
uses: ./.github/steps/trivy
with:
scan: code

test_client:
name: "Test Client (${{ matrix.name }})"
needs: [changes]
Expand Down Expand Up @@ -140,7 +160,7 @@ jobs:

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Install GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
Expand Down
3 changes: 3 additions & 0 deletions .trivyignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Vulnerabilities to skip in the Trivy scan: one CVE/GHSA id per line, e.g.:
#
# CVE-2026-12345