Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
42 changes: 39 additions & 3 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -78,4 +114,4 @@ workflows:
context:
- PagerDuty
- Snyk
- build
- build
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 48 additions & 0 deletions gradle/verification-metadata.xml
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,16 @@
<sha256 value="22006be70fd018099e99c6c27ac0982320f3a0f3c2ce053c55703579a5eea303" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.fasterxml.jackson" name="jackson-base" version="2.21.2">
<artifact name="jackson-base-2.21.2.pom">
<sha256 value="38c7898c763b8991fffbd769459972e0176bf0c505c739518c70f7c57d16eed7" origin="Maven Central"/>
</artifact>
</component>
<component group="com.fasterxml.jackson" name="jackson-bom" version="2.21.2">
<artifact name="jackson-bom-2.21.2.pom">
<sha256 value="bc6c549e632927bb9d8a0ada20c8467b5007d3b797c7f5db03a9b60d1de59be3" origin="Maven Central"/>
</artifact>
</component>
<component group="com.fasterxml.jackson" name="jackson-parent" version="2.14">
<artifact name="jackson-parent-2.14.pom">
<sha256 value="0906add855ae39f92357d63f485889b08fca4c43a5fe433522d75344e0757b19" origin="Generated by Gradle"/>
Expand All @@ -182,6 +192,11 @@
<sha256 value="23d186c8d8cd06015d022c710c715423d660d49e2973b15870b029cc26018682" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.fasterxml.jackson" name="jackson-parent" version="2.21">
<artifact name="jackson-parent-2.21.pom">
<sha256 value="3851df627faeb46887b956104630b74a8bbb5f7de22e974cf15982e20e0373de" origin="Maven Central"/>
</artifact>
</component>
<component group="com.fasterxml.jackson.core" name="jackson-annotations" version="2.19.2">
<artifact name="jackson-annotations-2.19.2.jar">
<sha256 value="e516743a316dcf83c572ffc9cb6e8c5e8c134880c8c5155b02f7b34e9c5dc3cf" origin="Generated by Gradle"/>
Expand All @@ -190,6 +205,17 @@
<sha256 value="319b5fd30776c0593cc8dc5a8d7e084e4b90709a4350641842abbd59fc81c815" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.fasterxml.jackson.core" name="jackson-annotations" version="2.21">
<artifact name="jackson-annotations-2.21.jar">
<sha256 value="53ca085f4a150f703f49e1aabd935bd03b43e1ea3d55d135438292af22cef56b" origin="Maven Central"/>
</artifact>
<artifact name="jackson-annotations-2.21.module">
<sha256 value="cae8ffecec3328b6ecbb138e274218f2fe1c7729a0e824da559268810095ad44" origin="Maven Central"/>
</artifact>
<artifact name="jackson-annotations-2.21.pom">
<sha256 value="71cac5392151e2a528cc9a09179f0a334179f05c52f8e596cf58ddf12bb27f2b" origin="Maven Central"/>
</artifact>
</component>
<component group="com.fasterxml.jackson.core" name="jackson-core" version="2.19.2">
<artifact name="jackson-core-2.19.2.jar">
<sha256 value="aa77eaf29293a868c47372194f7c5287d77d9370b04ea25d3fffc1e4904b5880" origin="Generated by Gradle"/>
Expand All @@ -198,6 +224,17 @@
<sha256 value="51af2e67783a5d72444de6a307c8e3ed93009256cd27e821915559a2165c0942" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.fasterxml.jackson.core" name="jackson-core" version="2.21.2">
<artifact name="jackson-core-2.21.2.jar">
<sha256 value="12e8655b100267b44d0f14d01b1f5082a5105e616174dc4307be1a761e92407b" origin="Maven Central"/>
</artifact>
<artifact name="jackson-core-2.21.2.module">
<sha256 value="98eee4ebd21c82b526b438888e03ad6b6e3966fc6e722ce4e37dd4590d3e4a27" origin="Maven Central"/>
</artifact>
<artifact name="jackson-core-2.21.2.pom">
<sha256 value="bab0e5599649ccb5ab21a9cf9d2e57192de1e48e6d23524dc253d62fc73c9634" origin="Maven Central"/>
</artifact>
</component>
<component group="com.fasterxml.jackson.core" name="jackson-databind" version="2.19.2">
<artifact name="jackson-databind-2.19.2.jar">
<sha256 value="0a1bd4e9b0d670e632d40ee8c625ad376233502f03c2f5889baea95d025b47a7" origin="Generated by Gradle"/>
Expand All @@ -206,6 +243,17 @@
<sha256 value="c601558344918f40834bb6d583811ea6c883bf06a28179a4c74e2fa18b306c68" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.fasterxml.jackson.core" name="jackson-databind" version="2.21.2">
<artifact name="jackson-databind-2.21.2.jar">
<sha256 value="8c982f01f148d805f0aaac2339011244757e0df7c6ff8951f2fa3f433b8ed849" origin="Maven Central"/>
</artifact>
<artifact name="jackson-databind-2.21.2.module">
<sha256 value="a8e0dd7f2bfd3e09a8c8b7349c61056fde9cb862f864ebc6f23193e5f6085f38" origin="Maven Central"/>
</artifact>
<artifact name="jackson-databind-2.21.2.pom">
<sha256 value="cf8e0ecdc35f02da06e27f4157600ba9d7649c5272e4d6b795ef2a42f070a262" origin="Maven Central"/>
</artifact>
</component>
<component group="com.github.docker-java" name="docker-java-api" version="3.4.2">
<artifact name="docker-java-api-3.4.2.jar">
<sha256 value="6789d68f95904cf274a6410fdfcc5530976bc94197c856909459bb8c64db557b" origin="Generated by Gradle"/>
Expand Down
177 changes: 177 additions & 0 deletions scripts/convert_snyk_junit.sh
Original file line number Diff line number Diff line change
@@ -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//&/&amp;}"
s="${s//</&lt;}"
s="${s//>/&gt;}"
s="${s//\"/&quot;}"
s="${s//\'/&apos;}"
echo "$s"
}

emit_empty() {
echo '<?xml version="1.0" encoding="UTF-8"?><testsuites tests="0" failures="0"/>'
}

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 '<?xml version="1.0" encoding="UTF-8"?>'
echo "<testsuites tests=\"$uniquePackages\" failures=\"$uniquePackages\">"

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 " <testsuite name=\"Snyk: ${sev} severity\" tests=\"${sevCount}\" failures=\"${sevCount}\">"

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
<testcase name="${escapedName}" classname="${escapedClass}">
<failure message="${escapedTitle}">
<![CDATA[
Highest Severity: ${highestSev}
Max CVSS Score: ${maxCvss}
Issues: ${titles}
CVEs: ${cves:-None}
CWEs: ${cwes:-None}
Package: ${moduleName}
Affected Versions: ${versions}
Fixed In: ${fixedIn:-No fix available}
Upgradable: ${isUpgradable}
Occurrences: ${totalOcc} (across dependency paths/projects)
Snyk IDs: ${snykIds}
]]>
</failure>
</testcase>
END
done

echo " </testsuite>"
done

if [ "$uniquePackages" -eq 0 ]; then
echo " <testsuite name=\"Snyk: security scan\" tests=\"1\" failures=\"0\">"
echo " <testcase name=\"No security vulnerabilities found\" classname=\"snyk.scan\"/>"
echo " </testsuite>"
fi

echo "</testsuites>"
}

convert_snyk_junit
Loading