diff --git a/.github/workflows/compare-vulnerabilities-PR.yml b/.github/workflows/compare-vulnerabilities-PR.yml new file mode 100644 index 00000000..c9e27deb --- /dev/null +++ b/.github/workflows/compare-vulnerabilities-PR.yml @@ -0,0 +1,55 @@ +name: PR – Vulnerability guard +run-name: 'Compare vulnerabilities between (Base) ${{ github.event.pull_request.base.ref }} and (Head) ${{ github.event.pull_request.head.ref }} (${{ github.event.pull_request.head.sha }}) by @${{ github.actor }}' +on: + pull_request: + types: [ opened, synchronize, reopened ] + +permissions: + contents: read + pull-requests: write # needed to create/update PR comments + +jobs: + compare-branches: + runs-on: ${{ vars.UBUNTU_VERSION }} + + steps: + # 1) Checkout head branch only + - name: Checkout PR head + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + fetch-tags: true + + - name: Set up JDK 8 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '8' + cache: 'maven' + + # + # - name: Ensure base is available + # run: | + # git fetch origin ${{ github.event.pull_request.base.ref }}:refs/remotes/origin/${{ github.event.pull_request.base.ref }} --tags --prune + + # 3) Run the action + - name: Vulnerability Diff (Syft+Grype) + uses: sec-open/vuln-diff-action@v2.0.0-alpha.1 + with: + base_ref: ${{ github.event.pull_request.base.ref }} + head_ref: ${{ github.event.pull_request.head.sha }} + github_token: ${{ secrets.GITHUB_TOKEN }} + slack_webhook_url: ${{ secrets.SLACK_SECURITY_WEBHOOK_URL }} + html_logo_url: "https://zettagenomics.com/wp-content/uploads/2022/10/Zetta-reversed-out-full-logo-dark-background.png" + +# write_summary: "true" +# upload_artifact: "true" +# min_severity: "LOW" +# report_html: "true" +# report_pdf: "true" +# pr_comment: "true" +# artifact_name: "vulnerability-diff-${{ github.event.inputs.branch_a }}-vs-${{ github.event.inputs.branch_b }}" +# pr_comment_marker: "" + +# title_logo_url: "https://zettagenomics.com/wp-content/uploads/2022/10/Zetta-reversed-out-full-logo-dark-background.png" \ No newline at end of file diff --git a/.github/workflows/compare-vulnerabilities.yml b/.github/workflows/compare-vulnerabilities.yml index 0c7a98cd..70650b0b 100644 --- a/.github/workflows/compare-vulnerabilities.yml +++ b/.github/workflows/compare-vulnerabilities.yml @@ -1,273 +1,75 @@ -name: Compare vulnerabilities (Syft SBOM -> Grype) between two branches (robust) - +name: Compare vulnerabilities (Syft SBOM -> Grype) between two branches +run-name: 'Compare vulnerabilities between ${{ inputs.branch_a }} (base) and ${{ inputs.branch_b }} (head) by @${{ github.actor }}' on: + workflow_call: + inputs: + branch_a: + type: string + description: 'Base branch (e.g. develop)' + required: true + branch_b: + type: string + description: 'Head branch (e.g. TASK-1234)' + required: true + secrets: + SLACK_SECURITY_WEBHOOK_URL: + required: false + workflow_dispatch: inputs: branch_a: - description: 'First branch to compare (e.g. main)' + description: 'Base branch (e.g. develop)' required: true default: 'develop' branch_b: - description: 'Second branch to compare (e.g. feature/fix-branch)' + description: 'Head branch (e.g. TASK-1234)' required: true - default: '' jobs: - compare-sbom-grype: - runs-on: ubuntu-latest - env: - REPORT_DIR: reports - steps: - - name: Prepare workspace - run: | - set -euo pipefail - mkdir -p "${REPORT_DIR}" - - - name: Checkout branch A - uses: actions/checkout@v4 - with: - ref: ${{ github.event.inputs.branch_a }} - path: branchA - fetch-depth: 0 + compare-branches: + runs-on: ${{ vars.UBUNTU_VERSION }} - - name: Checkout branch B + steps: + - name: Checkout head branch uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.branch_b }} - path: branchB fetch-depth: 0 + fetch-tags: true + - name: Restore Puppeteer cache + uses: actions/cache@v4 + with: + path: ~/.cache/puppeteer + key: puppeteer-${{ runner.os }} - - name: Install dependencies (jq, unzip) - run: | - sudo apt-get update - sudo apt-get install -y jq unzip - - - name: Install Syft - run: | - set -euo pipefail - curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sudo sh -s -- -b /usr/local/bin - syft version - - - name: Install Grype - run: | - set -euo pipefail - curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sudo sh -s -- -b /usr/local/bin - grype version - - - name: Generate SBOM for branch A (CycloneDX JSON) - run: | - set -euo pipefail - syft dir:./branchA -o cyclonedx-json="${REPORT_DIR}/branchA-sbom.cdx.json" - - - name: Generate SBOM for branch B (CycloneDX JSON) - run: | - set -euo pipefail - syft dir:./branchB -o cyclonedx-json="${REPORT_DIR}/branchB-sbom.cdx.json" - - - name: Scan SBOM with Grype (branch A) - run: | - set -euo pipefail - grype sbom:"${REPORT_DIR}/branchA-sbom.cdx.json" -o json > "${REPORT_DIR}/branchA-grype.json" - - - name: Scan SBOM with Grype (branch B) - run: | - set -euo pipefail - grype sbom:"${REPORT_DIR}/branchB-sbom.cdx.json" -o json > "${REPORT_DIR}/branchB-grype.json" - - - name: "debug - show match counts and sample (for troubleshooting)" - id: debug - run: | - set -euo pipefail - echo "---- branchA summary ----" - if [ -f "${REPORT_DIR}/branchA-grype.json" ]; then - jq '.matches | length' "${REPORT_DIR}/branchA-grype.json" || true - jq '.matches[0:5]' "${REPORT_DIR}/branchA-grype.json" || true - else - echo "branchA-grype.json missing" - fi - echo "---- branchB summary ----" - if [ -f "${REPORT_DIR}/branchB-grype.json" ]; then - jq '.matches | length' "${REPORT_DIR}/branchB-grype.json" || true - jq '.matches[0:5]' "${REPORT_DIR}/branchB-grype.json" || true - else - echo "branchB-grype.json missing" - fi - - - name: Generate comparison report (table + full MD) - id: gen_report - run: | - set -euo pipefail - A_BRANCH="${{ github.event.inputs.branch_a }}" - B_BRANCH="${{ github.event.inputs.branch_b }}" - A_GRYPE="${REPORT_DIR}/branchA-grype.json" - B_GRYPE="${REPORT_DIR}/branchB-grype.json" - OUT="${REPORT_DIR}/comparison-report.md" - mkdir -p "$(dirname "$OUT")" - - # comprueba que los JSON existen - if [ ! -s "$A_GRYPE" ]; then - echo "ERROR: ${A_GRYPE} not found or empty" >&2 - exit 1 - fi - if [ ! -s "$B_GRYPE" ]; then - echo "ERROR: ${B_GRYPE} not found or empty" >&2 - exit 1 - fi - - # --- extraer entradas formateadas: ID|pkg:version|SEVERITY --- - jq -r '[ .matches[]? as $m | - ($m.vulnerability.id // "-") as $id | - ( if ($m.artifact | type) == "object" - then (($m.artifact.name // $m.artifact.id // "-") + ":" + ($m.artifact.version // "-")) - else (($m.artifact // "-") + ":" + "-") - end) as $pv | - (($id + "|" + $pv + "|" + (($m.vulnerability.severity // "") | ascii_upcase))) - ] | .[]' "$A_GRYPE" | sort -u > /tmp/a_entries.txt || true - - jq -r '[ .matches[]? as $m | - ($m.vulnerability.id // "-") as $id | - ( if ($m.artifact | type) == "object" - then (($m.artifact.name // $m.artifact.id // "-") + ":" + ($m.artifact.version // "-")) - else (($m.artifact // "-") + ":" + "-") - end) as $pv | - (($id + "|" + $pv + "|" + (($m.vulnerability.severity // "") | ascii_upcase))) - ] | .[]' "$B_GRYPE" | sort -u > /tmp/b_entries.txt || true - - # union de entradas (unique) - cat /tmp/a_entries.txt /tmp/b_entries.txt | sort -u > /tmp/all_entries.txt || true - - # --- crear archivo ordenado por severidad desc (rank) y luego por ID --- - awk -F'|' ' - BEGIN { - map["CRITICAL"]=5; map["HIGH"]=4; map["MEDIUM"]=3; map["LOW"]=2; map["UNKNOWN"]=1; - } - { - id=$1; pv=$2; sev=toupper($3); - rank = (sev in map ? map[sev] : 0); - # output: rank|sev|id|pv - printf("%d|%s|%s|%s\n", rank, sev, id, pv); - } - ' /tmp/all_entries.txt | sort -t'|' -k1,1nr -k3,3 | cut -d'|' -f2- > /tmp/all_sorted.txt || true - - # --- START MD FILE --- - echo "# Vulnerability comparison: ${A_BRANCH} **vs** ${B_BRANCH}" > "${OUT}" - echo "" >> "${OUT}" - - # --- TABLE requested: Severity | VulnerabilityID | package:version | branches --- - echo "| Severity | VulnerabilityID | package:version | branches |" >> "${OUT}" - echo "|---|---|---|---|" >> "${OUT}" - - if [ -s /tmp/all_sorted.txt ]; then - while IFS= read -r line; do - # line format: SEV|ID|PKG:VER - sev=$(echo "$line" | awk -F'|' '{print $1}') - id=$(echo "$line" | awk -F'|' '{print $2}') - pv=$(echo "$line" | awk -F'|' '{print $3}') - - inA=0; inB=0 - # membership checks use original a_entries/b_entries (ID|pkg|sev) - entry="${id}|${pv}|${sev}" - if grep -Fxq "$entry" /tmp/a_entries.txt 2>/dev/null; then inA=1; fi - if grep -Fxq "$entry" /tmp/b_entries.txt 2>/dev/null; then inB=1; fi - - if [ "$inA" -eq 1 ] && [ "$inB" -eq 1 ]; then - branches="**BOTH**" - elif [ "$inA" -eq 1 ]; then - branches="${A_BRANCH}" - else - branches="${B_BRANCH}" - fi - - echo "| ${sev} | ${id} | ${pv} | ${branches} |" >> "${OUT}" - done < /tmp/all_sorted.txt - else - echo "| - | - | - | - |" >> "${OUT}" - echo "" >> "${OUT}" - fi - - echo "" >> "${OUT}" - # --- Totals y resto del MD (se mantienen para contexto) --- - totalA=$(jq -r '[ .matches[]?.vulnerability?.id ] | unique | length' "${A_GRYPE}" 2>/dev/null || echo 0) - totalB=$(jq -r '[ .matches[]?.vulnerability?.id ] | unique | length' "${B_GRYPE}" 2>/dev/null || echo 0) - echo "- **Total unique vulnerability IDs**: ${totalA} (${A_BRANCH}) | ${totalB} (${B_BRANCH})" >> "${OUT}" - echo "" >> "${OUT}" - - # tabla de severidad (como antes) - echo "| Severity | ${A_BRANCH} | ${B_BRANCH} |" >> "${OUT}" - echo "|---:|---:|---:|" >> "${OUT}" - for sev in CRITICAL HIGH MEDIUM LOW UNKNOWN; do - ca=$(jq --arg s "$sev" '[ .matches[]?.vulnerability? | select((.severity // "") | ascii_upcase == $s) | .id ] | unique | length' "${A_GRYPE}" 2>/dev/null || echo 0) - cb=$(jq --arg s "$sev" '[ .matches[]?.vulnerability? | select((.severity // "") | ascii_upcase == $s) | .id ] | unique | length' "${B_GRYPE}" 2>/dev/null || echo 0) - echo "| $sev | $ca | $cb |" >> "${OUT}" - done - - echo "" >> "${OUT}" - echo "----" >> "${OUT}" - echo "Artifacts included:" >> "${OUT}" - echo "- ${REPORT_DIR}/branchA-sbom.cdx.json" >> "${OUT}" - echo "- ${REPORT_DIR}/branchB-sbom.cdx.json" >> "${OUT}" - echo "- ${REPORT_DIR}/branchA-grype.json" >> "${OUT}" - echo "- ${REPORT_DIR}/branchB-grype.json" >> "${OUT}" - echo "- ${REPORT_DIR}/comparison-report.md (this file)" >> "${OUT}" - - - - name: Create ZIP of reports - run: | - set -euo pipefail - cd "${REPORT_DIR}" - zip -r comparison-artifacts.zip . || true + - name: Set up JDK 8 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '8' + cache: 'maven' + # 3) Run the action + - name: Vulnerability Diff (Syft+Grype) + uses: sec-open/vuln-diff-action@v2.0.0-alpha.3 + with: + base_ref: ${{ github.event.inputs.branch_a }} # pass 'develop' + head_ref: ${{ github.event.inputs.branch_b }} # pass 'TASK-7908' + html_logo_url: "https://zettagenomics.com/wp-content/uploads/2022/10/Zetta-reversed-out-full-logo-dark-background.png" - - name: "Publish table to GitHub Actions summary (only the table, sorted)" + - name: Save Puppeteer cache if: always() - run: | - set -euo pipefail - SUMMARY="$GITHUB_STEP_SUMMARY" - A_BRANCH="${{ github.event.inputs.branch_a }}" - B_BRANCH="${{ github.event.inputs.branch_b }}" - - # Si no hay sorted file, intenta generarlo a partir de /tmp/all_entries.txt - if [ ! -s /tmp/all_sorted.txt ] && [ -s /tmp/all_entries.txt ]; then - awk -F'|' ' - BEGIN { map["CRITICAL"]=5; map["HIGH"]=4; map["MEDIUM"]=3; map["LOW"]=2; map["UNKNOWN"]=1; } - { - id=$1; pv=$2; sev=toupper($3); - rank = (sev in map ? map[sev] : 0); - printf("%d|%s|%s|%s\n", rank, sev, id, pv); - } - ' /tmp/all_entries.txt | sort -t'|' -k1,1nr -k3,3 | cut -d'|' -f2- > /tmp/all_sorted.txt || true - fi - - # Header table for summary - echo "| Severity | VulnerabilityID | package:version | branches |" >> "$SUMMARY" - echo "|---|---|---|---|" >> "$SUMMARY" - - if [ -s /tmp/all_sorted.txt ]; then - while IFS= read -r line; do - sev=$(echo "$line" | awk -F'|' '{print $1}') - id=$(echo "$line" | awk -F'|' '{print $2}') - pv=$(echo "$line" | awk -F'|' '{print $3}') - - inA=0; inB=0 - entry="${id}|${pv}|${sev}" - if grep -Fxq "$entry" /tmp/a_entries.txt 2>/dev/null; then inA=1; fi - if grep -Fxq "$entry" /tmp/b_entries.txt 2>/dev/null; then inB=1; fi + uses: actions/cache@v4 + with: + path: ~/.cache/puppeteer + key: puppeteer-${{ runner.os }} - if [ "$inA" -eq 1 ] && [ "$inB" -eq 1 ]; then - branches="**BOTH**" - elif [ "$inA" -eq 1 ]; then - branches="${A_BRANCH}" - else - branches="${B_BRANCH}" - fi - echo "| ${sev} | ${id} | ${pv} | ${branches} |" >> "$SUMMARY" - done < /tmp/all_sorted.txt - else - echo "| - | - | - | - |" >> "$SUMMARY" - fi +# build_command: "" +# write_summary: "true" +# upload_artifact: "true" +# artifact_name: "vulnerability-diff-${{ github.event.inputs.branch_a }}-vs-${{ github.event.inputs.branch_b }}" +# report_html: "true" +# report_pdf: "true" +# min_severity: "LOW" +# title_logo_url: "https://zettagenomics.com/wp-content/uploads/2022/10/Zetta-reversed-out-full-logo-dark-background.png" - - name: Upload artifacts (reports) - uses: actions/upload-artifact@v4 - with: - name: vuln-comparison-${{ github.run_id }}-${{ github.event.inputs.branch_a }}-vs-${{ github.event.inputs.branch_b }}-$(date +%s) - path: ${{ env.REPORT_DIR }} diff --git a/pom.xml b/pom.xml index eb71ba27..79eb1f45 100644 --- a/pom.xml +++ b/pom.xml @@ -18,11 +18,11 @@ commons-datastore - + 2.14.3 3.14.0 1.7.36 - 1.7.7 + 1.11.4 4.11.5 8.8.2 1.69