Vulnerability Scan #26
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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: Get grype version | |
| id: grype_version | |
| run: | | |
| GRYPE_VERSION=$(grype version -o json | jq -r '.version') | |
| echo "GRYPE_VERSION=$GRYPE_VERSION" >> $GITHUB_ENV | |
| echo "Grype version: $GRYPE_VERSION" | |
| - name: Start scan | |
| id: start_scan | |
| run: | | |
| SCAN_GUID=$(uuidgen) | |
| echo "SCAN_GUID=$SCAN_GUID" >> $GITHUB_ENV | |
| response=$(curl -s -o start_response.json -w "%{http_code}" \ | |
| -X POST "https://epplussoftware.com/api/security/vulnerability/scan/start?scanId=${SCAN_GUID}&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 start_response.json | |
| exit 1 | |
| fi | |
| SCAN_DB_ID=$(jq -r '.scanDbId' start_response.json) | |
| echo "SCAN_DB_ID=$SCAN_DB_ID" >> $GITHUB_ENV | |
| echo "Scan started: GUID=$SCAN_GUID, DB ID=$SCAN_DB_ID" | |
| - 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}&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 | |
| 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}?scanId=${SCAN_DB_ID}" \ | |
| -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 | |
| - name: Complete scan (success) | |
| if: success() | |
| run: | | |
| curl -s -X POST "https://epplussoftware.com/api/security/vulnerability/scan/complete?scanId=${SCAN_GUID}&status=completed" \ | |
| -H "X-Api-Key: ${{ secrets.EPPLUS_VULNERABILITY_API_KEY }}" | |
| echo "Scan marked as completed" | |
| - name: Complete scan (failure) | |
| if: failure() && env.SCAN_GUID != '' | |
| run: | | |
| curl -s -X POST "https://epplussoftware.com/api/security/vulnerability/scan/complete?scanId=${SCAN_GUID}&status=failed" \ | |
| -H "X-Api-Key: ${{ secrets.EPPLUS_VULNERABILITY_API_KEY }}" | |
| echo "Scan marked as failed" |