Trivy Vulnerability + License Scan (PR + Scheduled) #51
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
| name: Trivy Vulnerability + License Scan (PR + Scheduled) | |
| # Trigger conditions: PR/push on main/3.0 branches, daily scheduled scan (00:00 UTC) | |
| on: | |
| workflow_dispatch: | |
| pull_request: | |
| branches: [ main, 3.0 ] | |
| types: [ opened, synchronize, reopened ] | |
| push: | |
| branches: [ main, 3.0 ] | |
| schedule: | |
| - cron: '0 0 * * *' # Daily scan at 00:00 UTC (8:00 Beijing Time) | |
| jobs: | |
| trivy-scan: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| security-events: write # Required for uploading SARIF to GitHub Security | |
| steps: | |
| # Step 1: Check out repository code | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| # Step 2: Generate SBOM using Trivy | |
| - name: Generate SBOM | |
| uses: aquasecurity/trivy-action@master | |
| with: | |
| scan-type: 'fs' | |
| scan-ref: '.' | |
| format: 'spdx-json' | |
| output: 'sbom-spdx.json' | |
| skip-dirs: 'target' | |
| # Step 3: Filter SBOM - Remove test-only dependencies | |
| - name: Filter SBOM (Exclude Test Dependencies) | |
| run: | | |
| # Filter out test-only dependencies from SBOM | |
| # Match by package name since externalRefs might not exist | |
| jq ' | |
| del(.packages[] | | |
| select( | |
| .name == "org.lz4:lz4-java" or | |
| .name == "com.ibm.icu:icu4j" or | |
| .name == "net.java.dev.jna:jna" or | |
| .name == "org.javassist:javassist" or | |
| .name == "junit:junit" or | |
| .name == "org.junit.platform:junit-platform-commons" or | |
| .name == "org.opentest4j:opentest4j" or | |
| .name == "org.apiguardian:apiguardian-api" | |
| ) | |
| ) | |
| ' sbom-spdx.json > sbom-filtered.json | |
| # Show statistics | |
| TOTAL=$(jq '.packages | length' sbom-spdx.json) | |
| FILTERED=$(jq '.packages | length' sbom-filtered.json) | |
| REMOVED=$((TOTAL - FILTERED)) | |
| echo "📊 SBOM Statistics:" | |
| echo " Total packages: $TOTAL" | |
| echo " Removed (test-only): $REMOVED" | |
| echo " Remaining (production): $FILTERED" | |
| echo "" | |
| echo "✅ Filtered SBOM created: sbom-filtered.json" | |
| # Validate filtered SBOM | |
| if [ ! -s sbom-filtered.json ]; then | |
| echo "❌ Error: sbom-filtered.json is empty!" | |
| exit 1 | |
| fi | |
| # Show sample packages | |
| echo "" | |
| echo "📦 Sample production packages:" | |
| jq -r '.packages[0:3] | .[].name' sbom-filtered.json | |
| # Step 3.5: Output SBOM content for debugging | |
| - name: Output SBOM Content (Debug) | |
| run: | | |
| echo "===== Original SBOM (first 100 lines) =====" | |
| head -100 sbom-spdx.json | |
| echo "" | |
| echo "===== Filtered SBOM (first 100 lines) =====" | |
| head -100 sbom-filtered.json | |
| echo "" | |
| echo "===== Filtered SBOM package list =====" | |
| jq -r '.packages[] | .name' sbom-filtered.json | sort | |
| # Step 4: Trivy Vulnerability Scan on filtered SBOM (JSON format) | |
| - name: Trivy Vulnerability Scan (SBOM) | |
| id: vuln_scan | |
| continue-on-error: true | |
| uses: aquasecurity/trivy-action@master | |
| with: | |
| scan-type: 'sbom' | |
| scan-ref: 'sbom-filtered.json' | |
| format: 'json' | |
| output: 'trivy-vuln.json' | |
| severity: 'CRITICAL,HIGH,MEDIUM' | |
| # Step 4.5: Debug - Output Trivy JSON structure | |
| - name: Debug Trivy JSON | |
| run: | | |
| echo "===== Trivy JSON structure (first 200 lines) =====" | |
| head -200 trivy-vuln.json | |
| echo "" | |
| echo "===== Check Results array =====" | |
| jq '.Results | length' trivy-vuln.json | |
| echo "" | |
| echo "===== Sample result =====" | |
| jq '.Results[0]' trivy-vuln.json 2>/dev/null || echo "No results" | |
| # Step 4.6: Convert Trivy JSON to SARIF | |
| - name: Convert JSON to SARIF | |
| run: | | |
| # First, check if there are any vulnerabilities | |
| VULN_COUNT=$(jq '.Results | length' trivy-vuln.json) | |
| echo "Found $VULN_COUNT vulnerabilities" | |
| if [ "$VULN_COUNT" -eq 0 ]; then | |
| # Create empty SARIF if no vulnerabilities | |
| jq '{ | |
| version: "2.1.0", | |
| "$schema": "https://json.schemastore.org/sarif-2.1.0.json", | |
| runs: [ | |
| { | |
| tool: { | |
| driver: { | |
| name: "Trivy", | |
| version: "0.68.1", | |
| informationUri: "https://github.com/aquasecurity/trivy" | |
| } | |
| }, | |
| results: [] | |
| } | |
| ] | |
| }' > trivy-vuln.sarif | |
| else | |
| # For dependency vulnerabilities, use pom.xml as the location | |
| # This way GitHub Security can find the file | |
| jq '{ | |
| version: "2.1.0", | |
| "$schema": "https://json.schemastore.org/sarif-2.1.0.json", | |
| runs: [ | |
| { | |
| tool: { | |
| driver: { | |
| name: "Trivy", | |
| informationUri: "https://github.com/aquasecurity/trivy", | |
| rules: [ | |
| .Results[] | { | |
| id: (.VulnerabilityID // "CVE-UNKNOWN"), | |
| name: (.VulnerabilityID // "CVE-UNKNOWN"), | |
| shortDescription: { | |
| text: (.Title // .Description // "Unknown vulnerability") | |
| }, | |
| fullDescription: { | |
| text: (.Description // .Title // "No description available") | |
| } | |
| } | if (.PrimaryURL // null) != null then . + {helpUri: .PrimaryURL} else . end | |
| ] | unique_by(.id) | |
| } | |
| }, | |
| results: [ | |
| .Results[] | { | |
| ruleId: (.VulnerabilityID // "CVE-UNKNOWN"), | |
| level: (if .Severity == "CRITICAL" then "error" elif .Severity == "HIGH" then "error" else "warning" end), | |
| message: { | |
| text: "\(.PkgName // "unknown")@\(.InstalledVersion // "unknown") has \(.Severity // "UNKNOWN") severity vulnerability: \(.Title // .Description // "No description")" | |
| }, | |
| locations: [ | |
| { | |
| physicalLocation: { | |
| artifactLocation: { | |
| uri: "pom.xml", | |
| description: { | |
| text: "Dependency: \(.PkgName // "unknown")@\(.InstalledVersion // "unknown")" | |
| } | |
| }, | |
| region: { | |
| startLine: 1, | |
| endLine: 1 | |
| } | |
| } | |
| } | |
| ], | |
| properties: { | |
| severity: .Severity, | |
| package: (.PkgName // "unknown"), | |
| version: (.InstalledVersion // "unknown"), | |
| fixedVersion: (.FixedVersion // null), | |
| primaryURL: (.PrimaryURL // null), | |
| vulnerabilityID: (.VulnerabilityID // null) | |
| } | |
| } | |
| ] | |
| } | |
| ] | |
| }' trivy-vuln.json > trivy-vuln.sarif | |
| fi | |
| # Validate SARIF | |
| echo "SARIF preview:" | |
| head -100 trivy-vuln.sarif | |
| # Step 5: Trivy License Scan on filtered SBOM | |
| - name: Trivy License Scan (SBOM) | |
| continue-on-error: true | |
| uses: aquasecurity/trivy-action@master | |
| with: | |
| scan-type: 'sbom' | |
| scan-ref: 'sbom-filtered.json' | |
| format: 'table' | |
| severity: 'CRITICAL,HIGH,UNKNOWN' | |
| scanners: 'license' | |
| # Step 6: Upload SARIF to GitHub Security | |
| - name: Upload Results to GitHub Security | |
| if: always() | |
| uses: github/codeql-action/upload-sarif@v4 | |
| with: | |
| sarif_file: 'trivy-vuln.sarif' | |
| # Step 7: Upload SBOM artifacts | |
| - name: Upload SBOM Artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: sbom-files | |
| path: | | |
| sbom-spdx.json | |
| sbom-filtered.json | |
| retention-days: 30 |