diff --git a/.github/workflows/snyk.yml b/.github/workflows/snyk.yml index c47b333..fef8f82 100644 --- a/.github/workflows/snyk.yml +++ b/.github/workflows/snyk.yml @@ -54,6 +54,11 @@ jobs: - uses: taiki-e/install-action@1e29709c0ec63babbe8f266397a465f29a825f91 # v2.87.31 with: tool: cargo-cyclonedx@0.5.9 + # `snyk test` applies .snyk itself; `snyk sbom test` does not, so its ignores + # are applied here: a finding is dropped when .snyk ignores its ID for every + # path ('*') with an `expires` still to come. Without a .snyk nothing is + # ignored. Snyk exiting above 1 (an error), or output the filter cannot + # read, fails the job. - name: Test each dependency set env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} @@ -64,6 +69,50 @@ jobs: if [ -z "$SNYK_TOKEN" ]; then echo "::error::The SNYK_TOKEN repository secret is not set, so nothing was scanned." fi + # {"": ""} for each ID .snyk ignores everywhere. + ignored='{}' + if [ -f .snyk ]; then + ignored=$(yq -o=json '.ignore // {}' .snyk | jq -c ' + with_entries(.value = ([.value[] | .["*"]? | .expires? // empty + | select((sub("\\.[0-9]+"; "") | fromdateiso8601) > now)] | max)) + | with_entries(select(.value != null))') + fi + report=$RUNNER_TEMP/snyk.json + # Tests one SBOM and prints each finding as open or ignored; fails on an + # open finding or a result it cannot read. + sbom_test() { + local code=0 findings open + echo "== $1" + snyk sbom test --experimental --json --file="$1" > "$report" || code=$? + if [ "$code" -gt 1 ] || [ ! -s "$report" ]; then + cat "$report"; echo + echo "::error::snyk sbom test failed on $1 (exit $code)." + return 1 + fi + # One line per vulnerable path, `open` or `ignored`. + if ! findings=$(jq -rn --argjson ignored "$ignored" ' + [inputs | if type == "array" then .[] else . end] + | if length == 0 then error("no result in the Snyk output") else .[] end + | if (.vulnerabilities | type) == "array" then .vulnerabilities[] + else error("no vulnerabilities list in the Snyk output") end + | $ignored[.id] as $until + | "\(if $until then "ignored" else "open" end) [\(.severity // "?" | ascii_upcase)] \(.title) (\(.id)) in \(.packageName)@\(.version) via \(.from // [] | join(" > "))\(if $until then ", ignored until \($until)" else "" end)" + ' "$report"); then + cat "$report"; echo + echo "::error::Could not read the Snyk results for $1." + return 1 + fi + [ -z "$findings" ] || printf '%s\n' "$findings" + open=$(printf '%s\n' "$findings" | grep -c '^open ' || true) + if [ "$open" -gt 0 ]; then + echo "::error::$open open Snyk finding(s) in $1." + return 1 + elif [ "$code" -eq 1 ] && [ -z "$findings" ]; then + cat "$report"; echo + echo "::error::Snyk failed $1 without listing a vulnerability." + return 1 + fi + } status=0 while IFS=: read -r kind target; do [ -n "$kind" ] || continue @@ -82,7 +131,7 @@ jobs: || { echo "::error::Cannot list the SBOMs for $target"; status=1; continue; } [ -s "$list" ] || { echo "::error::cargo cyclonedx wrote no SBOM for $target"; status=1; continue; } while IFS= read -r -d '' sbom; do - [ -z "$SNYK_TOKEN" ] || snyk sbom test --experimental --file="$sbom" || status=1 + [ -z "$SNYK_TOKEN" ] || sbom_test "$sbom" || status=1 done < "$list" ;; *) echo "::error::Unknown dependency set $kind:$target"; status=1 ;; esac