Skip to content

Vulnerability Scan

Vulnerability Scan #19

# This workflow scans all EPPlus SBOM files for known vulnerabilities using grype.
# It runs nightly Monday-Friday and uploads reports to Azure Blob Storage.
# For more information see: https://github.com/anchore/grype
name: Vulnerability Scan
on:
schedule:
# 01:00 UTC = 02:00 CET (Monday-Friday)
- cron: '0 1 * * 1-5'
workflow_dispatch: # Allow manual triggering
jobs:
scan:
runs-on: ubuntu-latest
steps:
- name: Authenticate to Azure
uses: Azure/login@v2
with:
creds: '{"clientId":"${{ secrets.EPPLUS_CODE_SIGNING_APPLICATION_ID }}","clientSecret":"${{ secrets.EPPLUS_CODE_SIGNING_SECRET }}","subscriptionId":"${{ secrets.EPPLUS_CODE_SIGNING_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.EPPLUS_CODE_SIGNING_TENENT_ID }}"}'
- name: Install grype
run: curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
- name: Fetch SBOM index
run: |
curl -sSf https://epplussoftware.com/security/sbom/all.json -o all.json
echo "SBOM index fetched:"
cat all.json
- name: Scan each SBOM
shell: bash
run: |
mkdir -p ./reports
versions=$(jq -r '.versions[] | @base64' all.json)
for row in $versions; do
entry=$(echo "$row" | base64 --decode)
version=$(echo "$entry" | jq -r '.version')
expected_sha256=$(echo "$entry" | jq -r '.sha256' | tr -d '\xEF\xBB\xBF' | tr -d '[:space:]')
echo "--- Processing EPPlus $version ---"
# Download combined SBOM directly from Azure Blob Storage to avoid web cache
sbom_file="epplus-${version}.sbom.json"
az storage blob download \
--account-name eppluswebprod \
--container-name sbom \
--name "$sbom_file" \
--file "$sbom_file" \
--auth-mode login
# Validate checksum
actual_sha256=$(sha256sum "$sbom_file" | awk '{ print $1 }')
if [ "$actual_sha256" != "$expected_sha256" ]; then
echo "ERROR: Checksum mismatch for EPPlus $version"
echo " Expected: $expected_sha256"
echo " Actual: $actual_sha256"
exit 1
fi
echo "Checksum OK for EPPlus $version"
# Scan per-TFM SBOMs if available, otherwise fall back to combined SBOM
tfm_entries=$(echo "$entry" | jq -r '.targetFrameworks // [] | .[] | @base64')
if [ -n "$tfm_entries" ]; then
for tfm_row in $tfm_entries; do
tfm_entry=$(echo "$tfm_row" | base64 --decode)
tfm=$(echo "$tfm_entry" | jq -r '.tfm')
expected_tfm_sha256=$(echo "$tfm_entry" | jq -r '.sha256' | tr -d '\xEF\xBB\xBF' | tr -d '[:space:]')
echo " Scanning TFM: $tfm"
# Download per-TFM SBOM directly from Azure Blob Storage
tfm_sbom_file="epplus-${version}.${tfm}.sbom.json"
az storage blob download \
--account-name eppluswebprod \
--container-name sbom \
--name "$tfm_sbom_file" \
--file "$tfm_sbom_file" \
--auth-mode login
# Validate checksum
actual_tfm_sha256=$(sha256sum "$tfm_sbom_file" | awk '{ print $1 }')
if [ "$actual_tfm_sha256" != "$expected_tfm_sha256" ]; then
echo "ERROR: Checksum mismatch for EPPlus $version / $tfm"
echo " Expected: $expected_tfm_sha256"
echo " Actual: $actual_tfm_sha256"
exit 1
fi
echo " Checksum OK for EPPlus $version / $tfm"
# Run grype directly against CycloneDX SBOM
mkdir -p "./reports/${version}/${tfm}"
grype --add-cpes-if-none "sbom:./${tfm_sbom_file}" --output json --file "./reports/${version}/${tfm}/report.json"
echo " Scan complete for EPPlus $version / $tfm"
done
else
# No per-TFM SBOMs — scan combined SBOM
echo " No per-TFM SBOMs found, scanning combined SBOM"
mkdir -p "./reports/${version}"
grype --add-cpes-if-none "sbom:./${sbom_file}" --output json --file "./reports/${version}/report.json"
echo " Scan complete for EPPlus $version (combined)"
fi
done
- name: Upload reports to Azure Blob Storage
shell: bash
run: |
find ./reports -name "report.json" | while read report; do
# Strip leading ./reports/ to get the blob name
blob_name="${report#./reports/}"
echo "Uploading $blob_name"
az storage blob upload \
--account-name eppluswebprod \
--container-name vulnerability-reports \
--name "$blob_name" \
--file "$report" \
--auth-mode login \
--overwrite
done
- name: Index vulnerability reports
shell: bash
run: |
versions=$(jq -r '.versions[] | @base64' all.json)
for row in $versions; do
entry=$(echo "$row" | base64 --decode)
version=$(echo "$entry" | jq -r '.version')
tfm_entries=$(echo "$entry" | jq -r '.targetFrameworks // [] | .[] | @base64')
if [ -n "$tfm_entries" ]; then
for tfm_row in $tfm_entries; do
tfm_entry=$(echo "$tfm_row" | base64 --decode)
tfm=$(echo "$tfm_entry" | jq -r '.tfm')
echo "--- Indexing EPPlus $version / $tfm ---"
response=$(curl -s -o response.json -w "%{http_code}" \
-X POST "https://epplussoftware.com/api/security/vulnerability/index/${version}?tfm=${tfm}" \
-H "X-Api-Key: ${{ secrets.EPPLUS_VULNERABILITY_API_KEY }}" \
-H "Content-Type: application/json" \
-d @"./reports/${version}/${tfm}/report.json")
if [ "$response" != "200" ]; then
echo "ERROR: Indexing failed for EPPlus $version / $tfm with HTTP $response"
cat response.json
exit 1
fi
echo "Indexed EPPlus $version / $tfm successfully"
cat response.json
done
else
echo "--- Indexing EPPlus $version (combined) ---"
response=$(curl -s -o response.json -w "%{http_code}" \
-X POST "https://epplussoftware.com/api/security/vulnerability/index/${version}" \
-H "X-Api-Key: ${{ secrets.EPPLUS_VULNERABILITY_API_KEY }}" \
-H "Content-Type: application/json" \
-d @"./reports/${version}/report.json")
if [ "$response" != "200" ]; then
echo "ERROR: Indexing failed for EPPlus $version with HTTP $response"
cat response.json
exit 1
fi
echo "Indexed EPPlus $version successfully"
cat response.json
fi
done