Skip to content

Commit cf23c96

Browse files
Prepare reusable Compare vulnerabilities #TASK-7908
1 parent d7f2c20 commit cf23c96

File tree

1 file changed

+46
-254
lines changed

1 file changed

+46
-254
lines changed
Lines changed: 46 additions & 254 deletions
Original file line numberDiff line numberDiff line change
@@ -1,273 +1,65 @@
1-
name: Compare vulnerabilities (Syft SBOM -> Grype) between two branches (robust)
2-
1+
name: Compare vulnerabilities (Syft SBOM -> Grype) between two branches
2+
run-name: 'Compare vulnerabilities between ${{ inputs.branch_a }} (base) and ${{ inputs.branch_b }} (head) by @${{ github.actor }}'
33
on:
4+
workflow_call:
5+
inputs:
6+
branch_a:
7+
type: string
8+
description: 'Base branch (e.g. develop)'
9+
required: true
10+
branch_b:
11+
type: string
12+
description: 'Head branch (e.g. TASK-1234)'
13+
required: true
14+
secrets:
15+
GITHUB_TOKEN:
16+
required: true
17+
SLACK_SECURITY_WEBHOOK_URL:
18+
required: false
419
workflow_dispatch:
520
inputs:
621
branch_a:
7-
description: 'First branch to compare (e.g. main)'
22+
description: 'Base branch (e.g. develop)'
823
required: true
924
default: 'develop'
1025
branch_b:
11-
description: 'Second branch to compare (e.g. feature/fix-branch)'
26+
description: 'Head branch (e.g. TASK-1234)'
1227
required: true
13-
default: ''
1428

1529
jobs:
16-
compare-sbom-grype:
17-
runs-on: ubuntu-latest
18-
env:
19-
REPORT_DIR: reports
20-
steps:
21-
- name: Prepare workspace
22-
run: |
23-
set -euo pipefail
24-
mkdir -p "${REPORT_DIR}"
25-
26-
- name: Checkout branch A
27-
uses: actions/checkout@v4
28-
with:
29-
ref: ${{ github.event.inputs.branch_a }}
30-
path: branchA
31-
fetch-depth: 0
30+
compare-branches:
31+
runs-on: ${{ vars.UBUNTU_VERSION }}
3232

33-
- name: Checkout branch B
33+
steps:
34+
# 1) Checkout head branch only
35+
- name: Checkout head branch
3436
uses: actions/checkout@v4
3537
with:
3638
ref: ${{ github.event.inputs.branch_b }}
37-
path: branchB
3839
fetch-depth: 0
40+
fetch-tags: true
3941

40-
- name: Install dependencies (jq, unzip)
41-
run: |
42-
sudo apt-get update
43-
sudo apt-get install -y jq unzip
44-
45-
- name: Install Syft
46-
run: |
47-
set -euo pipefail
48-
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sudo sh -s -- -b /usr/local/bin
49-
syft version
50-
51-
- name: Install Grype
52-
run: |
53-
set -euo pipefail
54-
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sudo sh -s -- -b /usr/local/bin
55-
grype version
56-
57-
- name: Generate SBOM for branch A (CycloneDX JSON)
58-
run: |
59-
set -euo pipefail
60-
syft dir:./branchA -o cyclonedx-json="${REPORT_DIR}/branchA-sbom.cdx.json"
61-
62-
- name: Generate SBOM for branch B (CycloneDX JSON)
63-
run: |
64-
set -euo pipefail
65-
syft dir:./branchB -o cyclonedx-json="${REPORT_DIR}/branchB-sbom.cdx.json"
66-
67-
- name: Scan SBOM with Grype (branch A)
68-
run: |
69-
set -euo pipefail
70-
grype sbom:"${REPORT_DIR}/branchA-sbom.cdx.json" -o json > "${REPORT_DIR}/branchA-grype.json"
71-
72-
- name: Scan SBOM with Grype (branch B)
73-
run: |
74-
set -euo pipefail
75-
grype sbom:"${REPORT_DIR}/branchB-sbom.cdx.json" -o json > "${REPORT_DIR}/branchB-grype.json"
76-
77-
- name: "debug - show match counts and sample (for troubleshooting)"
78-
id: debug
79-
run: |
80-
set -euo pipefail
81-
echo "---- branchA summary ----"
82-
if [ -f "${REPORT_DIR}/branchA-grype.json" ]; then
83-
jq '.matches | length' "${REPORT_DIR}/branchA-grype.json" || true
84-
jq '.matches[0:5]' "${REPORT_DIR}/branchA-grype.json" || true
85-
else
86-
echo "branchA-grype.json missing"
87-
fi
88-
echo "---- branchB summary ----"
89-
if [ -f "${REPORT_DIR}/branchB-grype.json" ]; then
90-
jq '.matches | length' "${REPORT_DIR}/branchB-grype.json" || true
91-
jq '.matches[0:5]' "${REPORT_DIR}/branchB-grype.json" || true
92-
else
93-
echo "branchB-grype.json missing"
94-
fi
95-
96-
- name: Generate comparison report (table + full MD)
97-
id: gen_report
98-
run: |
99-
set -euo pipefail
100-
A_BRANCH="${{ github.event.inputs.branch_a }}"
101-
B_BRANCH="${{ github.event.inputs.branch_b }}"
102-
A_GRYPE="${REPORT_DIR}/branchA-grype.json"
103-
B_GRYPE="${REPORT_DIR}/branchB-grype.json"
104-
OUT="${REPORT_DIR}/comparison-report.md"
105-
mkdir -p "$(dirname "$OUT")"
106-
107-
# comprueba que los JSON existen
108-
if [ ! -s "$A_GRYPE" ]; then
109-
echo "ERROR: ${A_GRYPE} not found or empty" >&2
110-
exit 1
111-
fi
112-
if [ ! -s "$B_GRYPE" ]; then
113-
echo "ERROR: ${B_GRYPE} not found or empty" >&2
114-
exit 1
115-
fi
116-
117-
# --- extraer entradas formateadas: ID|pkg:version|SEVERITY ---
118-
jq -r '[ .matches[]? as $m |
119-
($m.vulnerability.id // "-") as $id |
120-
( if ($m.artifact | type) == "object"
121-
then (($m.artifact.name // $m.artifact.id // "-") + ":" + ($m.artifact.version // "-"))
122-
else (($m.artifact // "-") + ":" + "-")
123-
end) as $pv |
124-
(($id + "|" + $pv + "|" + (($m.vulnerability.severity // "") | ascii_upcase)))
125-
] | .[]' "$A_GRYPE" | sort -u > /tmp/a_entries.txt || true
126-
127-
jq -r '[ .matches[]? as $m |
128-
($m.vulnerability.id // "-") as $id |
129-
( if ($m.artifact | type) == "object"
130-
then (($m.artifact.name // $m.artifact.id // "-") + ":" + ($m.artifact.version // "-"))
131-
else (($m.artifact // "-") + ":" + "-")
132-
end) as $pv |
133-
(($id + "|" + $pv + "|" + (($m.vulnerability.severity // "") | ascii_upcase)))
134-
] | .[]' "$B_GRYPE" | sort -u > /tmp/b_entries.txt || true
135-
136-
# union de entradas (unique)
137-
cat /tmp/a_entries.txt /tmp/b_entries.txt | sort -u > /tmp/all_entries.txt || true
138-
139-
# --- crear archivo ordenado por severidad desc (rank) y luego por ID ---
140-
awk -F'|' '
141-
BEGIN {
142-
map["CRITICAL"]=5; map["HIGH"]=4; map["MEDIUM"]=3; map["LOW"]=2; map["UNKNOWN"]=1;
143-
}
144-
{
145-
id=$1; pv=$2; sev=toupper($3);
146-
rank = (sev in map ? map[sev] : 0);
147-
# output: rank|sev|id|pv
148-
printf("%d|%s|%s|%s\n", rank, sev, id, pv);
149-
}
150-
' /tmp/all_entries.txt | sort -t'|' -k1,1nr -k3,3 | cut -d'|' -f2- > /tmp/all_sorted.txt || true
151-
152-
# --- START MD FILE ---
153-
echo "# Vulnerability comparison: ${A_BRANCH} **vs** ${B_BRANCH}" > "${OUT}"
154-
echo "" >> "${OUT}"
155-
156-
# --- TABLE requested: Severity | VulnerabilityID | package:version | branches ---
157-
echo "| Severity | VulnerabilityID | package:version | branches |" >> "${OUT}"
158-
echo "|---|---|---|---|" >> "${OUT}"
159-
160-
if [ -s /tmp/all_sorted.txt ]; then
161-
while IFS= read -r line; do
162-
# line format: SEV|ID|PKG:VER
163-
sev=$(echo "$line" | awk -F'|' '{print $1}')
164-
id=$(echo "$line" | awk -F'|' '{print $2}')
165-
pv=$(echo "$line" | awk -F'|' '{print $3}')
166-
167-
inA=0; inB=0
168-
# membership checks use original a_entries/b_entries (ID|pkg|sev)
169-
entry="${id}|${pv}|${sev}"
170-
if grep -Fxq "$entry" /tmp/a_entries.txt 2>/dev/null; then inA=1; fi
171-
if grep -Fxq "$entry" /tmp/b_entries.txt 2>/dev/null; then inB=1; fi
172-
173-
if [ "$inA" -eq 1 ] && [ "$inB" -eq 1 ]; then
174-
branches="**BOTH**"
175-
elif [ "$inA" -eq 1 ]; then
176-
branches="${A_BRANCH}"
177-
else
178-
branches="${B_BRANCH}"
179-
fi
180-
181-
echo "| ${sev} | ${id} | ${pv} | ${branches} |" >> "${OUT}"
182-
done < /tmp/all_sorted.txt
183-
else
184-
echo "| - | - | - | - |" >> "${OUT}"
185-
echo "" >> "${OUT}"
186-
fi
187-
188-
echo "" >> "${OUT}"
189-
# --- Totals y resto del MD (se mantienen para contexto) ---
190-
totalA=$(jq -r '[ .matches[]?.vulnerability?.id ] | unique | length' "${A_GRYPE}" 2>/dev/null || echo 0)
191-
totalB=$(jq -r '[ .matches[]?.vulnerability?.id ] | unique | length' "${B_GRYPE}" 2>/dev/null || echo 0)
192-
echo "- **Total unique vulnerability IDs**: ${totalA} (${A_BRANCH}) | ${totalB} (${B_BRANCH})" >> "${OUT}"
193-
echo "" >> "${OUT}"
194-
195-
# tabla de severidad (como antes)
196-
echo "| Severity | ${A_BRANCH} | ${B_BRANCH} |" >> "${OUT}"
197-
echo "|---:|---:|---:|" >> "${OUT}"
198-
for sev in CRITICAL HIGH MEDIUM LOW UNKNOWN; do
199-
ca=$(jq --arg s "$sev" '[ .matches[]?.vulnerability? | select((.severity // "") | ascii_upcase == $s) | .id ] | unique | length' "${A_GRYPE}" 2>/dev/null || echo 0)
200-
cb=$(jq --arg s "$sev" '[ .matches[]?.vulnerability? | select((.severity // "") | ascii_upcase == $s) | .id ] | unique | length' "${B_GRYPE}" 2>/dev/null || echo 0)
201-
echo "| $sev | $ca | $cb |" >> "${OUT}"
202-
done
203-
204-
echo "" >> "${OUT}"
205-
echo "----" >> "${OUT}"
206-
echo "Artifacts included:" >> "${OUT}"
207-
echo "- ${REPORT_DIR}/branchA-sbom.cdx.json" >> "${OUT}"
208-
echo "- ${REPORT_DIR}/branchB-sbom.cdx.json" >> "${OUT}"
209-
echo "- ${REPORT_DIR}/branchA-grype.json" >> "${OUT}"
210-
echo "- ${REPORT_DIR}/branchB-grype.json" >> "${OUT}"
211-
echo "- ${REPORT_DIR}/comparison-report.md (this file)" >> "${OUT}"
212-
213-
214-
- name: Create ZIP of reports
215-
run: |
216-
set -euo pipefail
217-
cd "${REPORT_DIR}"
218-
zip -r comparison-artifacts.zip . || true
219-
220-
- name: "Publish table to GitHub Actions summary (only the table, sorted)"
221-
if: always()
222-
run: |
223-
set -euo pipefail
224-
SUMMARY="$GITHUB_STEP_SUMMARY"
225-
A_BRANCH="${{ github.event.inputs.branch_a }}"
226-
B_BRANCH="${{ github.event.inputs.branch_b }}"
227-
228-
# Si no hay sorted file, intenta generarlo a partir de /tmp/all_entries.txt
229-
if [ ! -s /tmp/all_sorted.txt ] && [ -s /tmp/all_entries.txt ]; then
230-
awk -F'|' '
231-
BEGIN { map["CRITICAL"]=5; map["HIGH"]=4; map["MEDIUM"]=3; map["LOW"]=2; map["UNKNOWN"]=1; }
232-
{
233-
id=$1; pv=$2; sev=toupper($3);
234-
rank = (sev in map ? map[sev] : 0);
235-
printf("%d|%s|%s|%s\n", rank, sev, id, pv);
236-
}
237-
' /tmp/all_entries.txt | sort -t'|' -k1,1nr -k3,3 | cut -d'|' -f2- > /tmp/all_sorted.txt || true
238-
fi
239-
240-
# Header table for summary
241-
echo "| Severity | VulnerabilityID | package:version | branches |" >> "$SUMMARY"
242-
echo "|---|---|---|---|" >> "$SUMMARY"
243-
244-
if [ -s /tmp/all_sorted.txt ]; then
245-
while IFS= read -r line; do
246-
sev=$(echo "$line" | awk -F'|' '{print $1}')
247-
id=$(echo "$line" | awk -F'|' '{print $2}')
248-
pv=$(echo "$line" | awk -F'|' '{print $3}')
249-
250-
inA=0; inB=0
251-
entry="${id}|${pv}|${sev}"
252-
if grep -Fxq "$entry" /tmp/a_entries.txt 2>/dev/null; then inA=1; fi
253-
if grep -Fxq "$entry" /tmp/b_entries.txt 2>/dev/null; then inB=1; fi
42+
- name: Set up JDK 8
43+
uses: actions/setup-java@v4
44+
with:
45+
distribution: 'temurin'
46+
java-version: '8'
47+
cache: 'maven'
48+
# 3) Run the action
49+
- name: Vulnerability Diff (Syft+Grype)
50+
uses: sec-open/[email protected]
51+
with:
52+
base_ref: ${{ github.event.inputs.branch_a }} # pass 'develop'
53+
head_ref: ${{ github.event.inputs.branch_b }} # pass 'TASK-7908'
54+
html_logo_url: "https://zettagenomics.com/wp-content/uploads/2022/10/Zetta-reversed-out-full-logo-dark-background.png"
25455

255-
if [ "$inA" -eq 1 ] && [ "$inB" -eq 1 ]; then
256-
branches="**BOTH**"
257-
elif [ "$inA" -eq 1 ]; then
258-
branches="${A_BRANCH}"
259-
else
260-
branches="${B_BRANCH}"
261-
fi
26256

263-
echo "| ${sev} | ${id} | ${pv} | ${branches} |" >> "$SUMMARY"
264-
done < /tmp/all_sorted.txt
265-
else
266-
echo "| - | - | - | - |" >> "$SUMMARY"
267-
fi
57+
# build_command: ""
58+
# write_summary: "true"
59+
# upload_artifact: "true"
60+
# artifact_name: "vulnerability-diff-${{ github.event.inputs.branch_a }}-vs-${{ github.event.inputs.branch_b }}"
61+
# report_html: "true"
62+
# report_pdf: "true"
63+
# min_severity: "LOW"
64+
# title_logo_url: "https://zettagenomics.com/wp-content/uploads/2022/10/Zetta-reversed-out-full-logo-dark-background.png"
26865

269-
- name: Upload artifacts (reports)
270-
uses: actions/upload-artifact@v4
271-
with:
272-
name: vuln-comparison-${{ github.run_id }}-${{ github.event.inputs.branch_a }}-vs-${{ github.event.inputs.branch_b }}-$(date +%s)
273-
path: ${{ env.REPORT_DIR }}

0 commit comments

Comments
 (0)