diff --git a/.circleci/config.yml b/.circleci/config.yml
index cd6d7ca4..284f463d 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,7 +1,7 @@
version: 2.1
orbs:
- snyk: snyk/snyk@2.3.3 # https://github.com/snyk/snyk-orb/
+ snyk: snyk/snyk@2.3.0 # https://github.com/snyk/snyk-orb/
jobs:
build:
@@ -68,7 +68,43 @@ jobs:
else
echo "Skipping monitor for non-main branch - no data sent to dashboard"
fi
- - run: snyk test --severity-threshold=high --all-projects --detection-depth=10 --scan-all-unmanaged || true # will NOT fail step if any results are found
+ - run: snyk test --severity-threshold=high --all-projects --detection-depth=10 --scan-all-unmanaged --json > snyk-results.json || true # will NOT fail step if any results are found
+ - run:
+ name: CVE Summary
+ command: |
+ if [ -f snyk-results.json ]; then
+ # Check if scan failed (error field present)
+ if jq -e '.error' snyk-results.json > /dev/null 2>&1; then
+ echo "Snyk scan failed - see snyk-results.json for details"
+ exit 0
+ fi
+ cve_count=$(jq '[.[] | select(type == "object") | select(.vulnerabilities != null) | .vulnerabilities[] | select( (.identifiers.CVE | length > 0 ) or (.identifiers.GHSA | length > 0) or (.identifiers.CWE | length > 0 ) )] | length' snyk-results.json)
+ if [ "$cve_count" -gt 0 ]; then
+ echo "=== CVEs Found ==="
+ jq -r '.[] | select(type == "object") | select(.vulnerabilities != null) | .vulnerabilities[] | select( (.identifiers.CVE | length > 0) or (.identifiers.GHSA | length > 0) or (.identifiers.CWE | length > 0 ) ) | [( [.identifiers.CVE,.identifiers.GHSA,.identifiers.CWE] | map(select(.!=null and .!=[])) | flatten | join(",")), .severity, .moduleName, .title] | @csv' snyk-results.json | sort -u | tee snyk-summary.csv
+ echo "=================="
+ else
+ echo "No CVEs found - clean scan!"
+ fi
+ else
+ echo "No snyk-results.json found — skipping CVE summary"
+ fi
+ - run:
+ name: Convert Snyk results to JUnit XML
+ when: always
+ command: |
+ if [ -f snyk-results.json ]; then
+ mkdir -p test-results/junit
+ bash scripts/convert_snyk_junit.sh snyk-results.json > test-results/junit/snyk-junit.xml
+ fi
+ - store_test_results:
+ path: test-results
+ - store_artifacts:
+ path: snyk-results.json
+ destination: snyk-results.json
+ - store_artifacts:
+ path: snyk-summary.csv
+ destination: snyk-summary.csv # will NOT fail step if any results are found
workflows:
version: 2
@@ -78,4 +114,4 @@ workflows:
context:
- PagerDuty
- Snyk
- - build
\ No newline at end of file
+ - build
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 0932bd95..0bce0a7a 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -13,7 +13,7 @@ shadow = "8.1.1"
ospackage = "11.11.2"
buildInfo = "0.9"
buildConfig = "3.1.0"
-jacksonDatabind = "2.21.1"
+jacksonDatabind = "2.21.2"
picocli = "4.7.7"
snakeYaml = "2.0"
#used for authz lib integration
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 072d09cf..d35678dd 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -172,6 +172,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -182,6 +192,11 @@
+
+
+
+
+
@@ -190,6 +205,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -198,6 +224,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -206,6 +243,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scripts/convert_snyk_junit.sh b/scripts/convert_snyk_junit.sh
new file mode 100755
index 00000000..d558ec46
--- /dev/null
+++ b/scripts/convert_snyk_junit.sh
@@ -0,0 +1,177 @@
+#!/bin/bash
+set -euo pipefail
+
+# Converts Snyk JSON output (from `snyk test --all-projects --json`) to JUnit XML
+# for native display in CircleCI's Tests tab.
+#
+# Key behaviors:
+# - Deduplicates findings by moduleName (package) across all scanned projects
+# - Filters out license issues (snyk:lic: prefix), keeping only security vulnerabilities
+# - Uses moduleName as classname for consistent CircleCI test tracking across runs
+# - Aggregates all CVEs/CWEs/Snyk IDs for a package into one test case
+# - Reports highest severity and highest CVSS score per package
+
+readonly IN=${1:-/dev/stdin}
+
+xml_escape() {
+ local s="$1"
+ s="${s//&/&}"
+ s="${s//<}"
+ s="${s//>/>}"
+ s="${s//\"/"}"
+ s="${s//\'/'}"
+ echo "$s"
+}
+
+emit_empty() {
+ echo ''
+}
+
+convert_snyk_junit() {
+ local inputfile="$IN"
+
+ # If reading from stdin, buffer to a temp file so we can read multiple times
+ if [ "$inputfile" = "/dev/stdin" ]; then
+ inputfile=$(mktemp)
+ cat > "$inputfile"
+ fi
+
+ echo "input file: $IN" >&2
+
+ if ! jq empty < "$inputfile" 2>/dev/null; then
+ echo "ERROR: Invalid JSON in $IN" >&2
+ emit_empty
+ return 0
+ fi
+
+ if jq -e 'type == "object" and has("error")' < "$inputfile" > /dev/null 2>&1; then
+ echo "Snyk scan had errors - generating empty JUnit output" >&2
+ emit_empty
+ return 0
+ fi
+
+ local tmpfile
+ tmpfile=$(mktemp)
+ trap "rm -f '$tmpfile'" EXIT
+
+ # Flatten all vulnerabilities across projects, filter out license issues,
+ # deduplicate by moduleName, and aggregate findings per package.
+ jq -c '
+ def sev_rank:
+ if . == "critical" then 0
+ elif . == "high" then 1
+ elif . == "medium" then 2
+ elif . == "low" then 3
+ else 4 end;
+
+ [ .[] | select(type == "object") | .vulnerabilities[]? ]
+ | map(select(.id | startswith("snyk:lic:") | not))
+ | group_by(.moduleName)
+ | map({
+ moduleName: .[0].moduleName,
+ versions: ([.[].version] | unique | sort),
+ snykIds: ([.[].id] | unique),
+ cves: [.[].identifiers.CVE[]?] | unique | sort,
+ cwes: [.[].identifiers.CWE[]?] | unique | sort,
+ titles: ([.[].title] | unique),
+ highestSeverity: ([ .[].severity ] | map({sev: ., rank: (. | sev_rank)}) | sort_by(.rank) | .[0].sev),
+ maxCvssScore: ([.[].cvssScore // 0] | max),
+ fixedIn: [.[].fixedIn[]?] | unique | sort,
+ isUpgradable: ([.[].isUpgradable] | any),
+ totalOccurrences: length
+ })
+ | sort_by((.highestSeverity | sev_rank), .moduleName)
+ ' < "$inputfile" > "$tmpfile"
+
+ local uniquePackages
+ uniquePackages=$(jq 'length // 0' < "$tmpfile")
+ uniquePackages=${uniquePackages:-0}
+ local totalOccurrences
+ totalOccurrences=$(jq '[.[].totalOccurrences] | add // 0' < "$tmpfile")
+ totalOccurrences=${totalOccurrences:-0}
+
+ echo "uniquePackages: $uniquePackages (from $totalOccurrences total occurrences across projects)" >&2
+
+ echo ''
+ echo ""
+
+ for sev in critical high medium low; do
+ local sevPkgs
+ sevPkgs=$(jq -c "[.[] | select(.highestSeverity == \"$sev\")]" < "$tmpfile")
+ local sevCount
+ sevCount=$(echo "$sevPkgs" | jq 'length')
+ sevCount=${sevCount:-0}
+
+ if [ "$sevCount" -eq 0 ]; then
+ continue
+ fi
+
+ echo " "
+
+ echo "$sevPkgs" | jq -c '.[]' | while IFS= read -r pkg; do
+ local moduleName highestSev maxCvss isUpgradable totalOcc
+ moduleName=$(echo "$pkg" | jq -r '.moduleName')
+ highestSev=$(echo "$pkg" | jq -r '.highestSeverity')
+ maxCvss=$(echo "$pkg" | jq -r '.maxCvssScore')
+ isUpgradable=$(echo "$pkg" | jq -r '.isUpgradable')
+ totalOcc=$(echo "$pkg" | jq -r '.totalOccurrences')
+
+ local versions snykIds cves cwes titles fixedIn
+ versions=$(echo "$pkg" | jq -r '.versions | join(", ")')
+ snykIds=$(echo "$pkg" | jq -r '.snykIds | join(", ")')
+ cves=$(echo "$pkg" | jq -r '.cves | join(", ")')
+ cwes=$(echo "$pkg" | jq -r '.cwes | join(", ")')
+ titles=$(echo "$pkg" | jq -r '.titles | join("; ")')
+ fixedIn=$(echo "$pkg" | jq -r '.fixedIn | join(", ")')
+
+ local idLabel=""
+ if [ -n "$cves" ]; then
+ idLabel="$cves"
+ elif [ -n "$cwes" ]; then
+ idLabel="$cwes"
+ else
+ idLabel="$snykIds"
+ fi
+
+ local sevUpper
+ sevUpper=$(echo "$highestSev" | tr '[:lower:]' '[:upper:]')
+
+ local escapedName escapedClass escapedTitle
+ escapedName=$(xml_escape "${sevUpper} ${moduleName} (${totalOcc} findings) ${idLabel}")
+ escapedClass=$(xml_escape "$moduleName")
+ escapedTitle=$(xml_escape "$titles")
+
+ cat <
+
+
+
+
+END
+ done
+
+ echo " "
+ done
+
+ if [ "$uniquePackages" -eq 0 ]; then
+ echo " "
+ echo " "
+ echo " "
+ fi
+
+ echo ""
+}
+
+convert_snyk_junit