Skip to content

Vulnerability Scan

Vulnerability Scan #18

# 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 v0.110.0
- name: Install syft
run: curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin v1.21.0
- 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: Start scan
shell: bash
run: |
SCAN_ID=$(uuidgen)
echo "SCAN_ID=$SCAN_ID" >> $GITHUB_ENV
GRYPE_VERSION=$(grype version | grep '^Version' | awk '{print $2}')
response=$(curl -s -o response.json -w "%{http_code}" \
-X POST "https://epplussoftware.com/api/security/vulnerability/scan/start?scanId=${SCAN_ID}&grypeVersion=${GRYPE_VERSION}" \
-H "X-Api-Key: ${{ secrets.EPPLUS_VULNERABILITY_API_KEY }}")
if [ "$response" != "200" ]; then
echo "ERROR: Failed to start scan with HTTP $response"
cat response.json
exit 1
fi
SCAN_DB_ID=$(jq -r '.scanDbId' response.json)
echo "SCAN_DB_ID=$SCAN_DB_ID" >> $GITHUB_ENV
echo "Scan started with ID $SCAN_ID (db id: $SCAN_DB_ID)"
cat response.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')
echo "--- Processing EPPlus $version ---"
# Scan per-TFM SBOMs
tfms=$(echo "$entry" | jq -r '.targetFrameworks[]? | @base64')
for tfm_row in $tfms; do
tfm_entry=$(echo "$tfm_row" | base64 --decode)
tfm=$(echo "$tfm_entry" | jq -r '.tfm')
expected_sha256=$(echo "$tfm_entry" | jq -r '.sha256' | tr -d '\xEF\xBB\xBF' | tr -d '[:space:]')
echo " -> TFM: $tfm"
# Download per-TFM SBOM
sbom_file="epplus-${version}.${tfm}.sbom.json"
curl -sSf "https://epplussoftware.com/security/sbom/${version}/${tfm}.json" -o "$sbom_file"
# Validate checksum
actual_sha256=$(sha256sum "$sbom_file" | awk '{ print $1 }')
if [ "$actual_sha256" != "$expected_sha256" ]; then
echo "ERROR: Checksum mismatch for EPPlus $version / $tfm"
echo " Expected: $expected_sha256"
echo " Actual: $actual_sha256"
exit 1
fi
echo " Checksum OK for $version / $tfm"
# Convert CycloneDX SBOM to Syft JSON format
syft_file="epplus-${version}.${tfm}.syft.json"
syft scan "file:./${sbom_file}" -o "syft-json=./${syft_file}"
# Run grype scan
mkdir -p "./reports/${version}/${tfm}"
grype --add-cpes-if-none "sbom:./${syft_file}" --output json --file "./reports/${version}/${tfm}/report.json"
echo " Scan complete for $version / $tfm"
done
done
- name: Upload reports to Azure Blob Storage
shell: bash
run: |
for report in ./reports/*/*/report.json; do
version=$(echo "$report" | sed 's|./reports/||' | cut -d'/' -f1)
tfm=$(echo "$report" | sed 's|./reports/||' | cut -d'/' -f2)
echo "Uploading report for EPPlus $version / $tfm"
az storage blob upload \
--account-name eppluswebprod \
--container-name vulnerability-reports \
--name "${version}/${tfm}/report.json" \
--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')
tfms=$(echo "$entry" | jq -r '.targetFrameworks[]? | @base64')
for tfm_row in $tfms; 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}&scanId=${SCAN_DB_ID}" \
-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
done
- name: Complete scan
shell: bash
if: success()
run: |
response=$(curl -s -o response.json -w "%{http_code}" \
-X POST "https://epplussoftware.com/api/security/vulnerability/scan/complete?scanId=${SCAN_ID}&status=completed" \
-H "X-Api-Key: ${{ secrets.EPPLUS_VULNERABILITY_API_KEY }}")
if [ "$response" != "200" ]; then
echo "ERROR: Failed to complete scan with HTTP $response"
cat response.json
exit 1
fi
echo "Scan $SCAN_ID completed successfully"
cat response.json
- name: Fail scan
shell: bash
if: failure()
run: |
if [ -n "$SCAN_ID" ]; then
response=$(curl -s -o response.json -w "%{http_code}" \
-X POST "https://epplussoftware.com/api/security/vulnerability/scan/complete?scanId=${SCAN_ID}&status=failed" \
-H "X-Api-Key: ${{ secrets.EPPLUS_VULNERABILITY_API_KEY }}")
if [ "$response" != "200" ]; then
echo "WARNING: Failed to mark scan as failed with HTTP $response"
cat response.json
else
echo "Scan $SCAN_ID marked as failed"
fi
else
echo "No SCAN_ID available, skipping fail notification"
fi