Skip to content

Commit 0d4c39c

Browse files
Auxiliar workflow to compare vulnerabilities betwen two branches #TASK-7908
1 parent 98a6229 commit 0d4c39c

File tree

1 file changed

+212
-0
lines changed

1 file changed

+212
-0
lines changed
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
name: Compare vulnerabilities (Syft SBOM -> Grype) between two branches
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
branch_a:
7+
description: 'First branch to compare (e.g. main)'
8+
required: true
9+
default: 'main'
10+
branch_b:
11+
description: 'Second branch to compare (e.g. feature/fix-branch)'
12+
required: true
13+
default: ''
14+
15+
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+
mkdir -p "${REPORT_DIR}"
24+
25+
- name: Checkout branch A
26+
uses: actions/checkout@v4
27+
with:
28+
ref: ${{ github.event.inputs.branch_a }}
29+
path: branchA
30+
fetch-depth: 0
31+
32+
- name: Checkout branch B
33+
uses: actions/checkout@v4
34+
with:
35+
ref: ${{ github.event.inputs.branch_b }}
36+
path: branchB
37+
fetch-depth: 0
38+
39+
- name: Install dependencies (jq, unzip)
40+
run: sudo apt-get update && sudo apt-get install -y jq unzip
41+
42+
- name: Install Syft (generate SBOMs)
43+
run: |
44+
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sudo sh -s -- -b /usr/local/bin
45+
46+
- name: Install Grype (scan SBOMs)
47+
run: |
48+
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sudo sh -s -- -b /usr/local/bin
49+
50+
- name: Generate SBOM for branch A (CycloneDX JSON)
51+
run: |
52+
set -euo pipefail
53+
syft dir:./branchA -o cyclonedx-json="${REPORT_DIR}/branchA-sbom.cdx.json"
54+
55+
- name: Generate SBOM for branch B (CycloneDX JSON)
56+
run: |
57+
set -euo pipefail
58+
syft dir:./branchB -o cyclonedx-json="${REPORT_DIR}/branchB-sbom.cdx.json"
59+
60+
- name: Scan SBOM with Grype (branch A)
61+
run: |
62+
set -euo pipefail
63+
grype sbom:"${REPORT_DIR}/branchA-sbom.cdx.json" -o json > "${REPORT_DIR}/branchA-grype.json" || true
64+
65+
- name: Scan SBOM with Grype (branch B)
66+
run: |
67+
set -euo pipefail
68+
grype sbom:"${REPORT_DIR}/branchB-sbom.cdx.json" -o json > "${REPORT_DIR}/branchB-grype.json" || true
69+
70+
- name: Generate comparison report (by VulnerabilityID and by package:version)
71+
run: |
72+
set -euo pipefail
73+
A_BRANCH="${{ github.event.inputs.branch_a }}"
74+
B_BRANCH="${{ github.event.inputs.branch_b }}"
75+
A_GRYPE="${REPORT_DIR}/branchA-grype.json"
76+
B_GRYPE="${REPORT_DIR}/branchB-grype.json"
77+
OUT="${REPORT_DIR}/comparison-report.md"
78+
mkdir -p "$(dirname "$OUT")"
79+
80+
echo "# Vulnerability comparison: ${A_BRANCH} **vs** ${B_BRANCH}" > "${OUT}"
81+
echo "" >> "${OUT}"
82+
# Totals (unique vulnerability IDs)
83+
totalA=$(jq -r '[ .matches[]?.vulnerability?.id ] | unique | length' "${A_GRYPE}" 2>/dev/null || echo 0)
84+
totalB=$(jq -r '[ .matches[]?.vulnerability?.id ] | unique | length' "${B_GRYPE}" 2>/dev/null || echo 0)
85+
echo "- **Total unique vulnerability IDs**: ${totalA} (${A_BRANCH}) | ${totalB} (${B_BRANCH})" >> "${OUT}"
86+
echo "" >> "${OUT}"
87+
88+
# Totals by package:version (unique vulnerable packages)
89+
pkgsA=$(jq -r '[ .matches[]? | "\(.artifact.name // \"-\"):\(.artifact.version // \"-\")" ] | unique | length' "${A_GRYPE}" 2>/dev/null || echo 0)
90+
pkgsB=$(jq -r '[ .matches[]? | "\(.artifact.name // \"-\"):\(.artifact.version // \"-\")" ] | unique | length' "${B_GRYPE}" 2>/dev/null || echo 0)
91+
echo "- **Total unique vulnerable package:version**: ${pkgsA} (${A_BRANCH}) | ${pkgsB} (${B_BRANCH})" >> "${OUT}"
92+
echo "" >> "${OUT}"
93+
94+
# Severity table (counts by vulnerability ID)
95+
echo "## Vulnerabilities by severity (counted by unique VulnerabilityID)" >> "${OUT}"
96+
echo "" >> "${OUT}"
97+
echo "| Severity | ${A_BRANCH} | ${B_BRANCH} |" >> "${OUT}"
98+
echo "|---:|---:|---:|" >> "${OUT}"
99+
for sev in CRITICAL HIGH MEDIUM LOW UNKNOWN; do
100+
ca=$(jq --arg s "$sev" '[ .matches[]?.vulnerability? | select(.severity==$s) | .id ] | unique | length' "${A_GRYPE}" 2>/dev/null || echo 0)
101+
cb=$(jq --arg s "$sev" '[ .matches[]?.vulnerability? | select(.severity==$s) | .id ] | unique | length' "${B_GRYPE}" 2>/dev/null || echo 0)
102+
echo "| $sev | $ca | $cb |" >> "${OUT}"
103+
done
104+
echo "" >> "${OUT}"
105+
106+
# Create lists of unique vulnerability IDs
107+
jq -r '[ .matches[]?.vulnerability?.id ] | unique | .[]' "${A_GRYPE}" 2>/dev/null | sort > /tmp/a_ids.txt || true
108+
jq -r '[ .matches[]?.vulnerability?.id ] | unique | .[]' "${B_GRYPE}" 2>/dev/null | sort > /tmp/b_ids.txt || true
109+
110+
# Create lists of unique package:version pairs
111+
jq -r '[ .matches[]? | "\(.artifact.name // \"-\"):\(.artifact.version // \"-\")" ] | unique | .[]' "${A_GRYPE}" 2>/dev/null | sort > /tmp/a_pkgs.txt || true
112+
jq -r '[ .matches[]? | "\(.artifact.name // \"-\"):\(.artifact.version // \"-\")" ] | unique | .[]' "${B_GRYPE}" 2>/dev/null | sort > /tmp/b_pkgs.txt || true
113+
114+
# New vulnerabilities in A not in B (by ID)
115+
echo "## VulnerabilityIDs present in ${A_BRANCH} but NOT in ${B_BRANCH}" >> "${OUT}"
116+
echo "" >> "${OUT}"
117+
if [ -s /tmp/a_ids.txt ]; then
118+
comm -23 /tmp/a_ids.txt /tmp/b_ids.txt > /tmp/new_in_a_ids.txt || true
119+
if [ -s /tmp/new_in_a_ids.txt ]; then
120+
while read -r id; do
121+
jq --arg id "$id" -r '
122+
.matches[]? | select(.vulnerability?.id==$id) |
123+
("- " + (.vulnerability.id // "-") + " | " + (.vulnerability.severity // "-") + " | " + (.artifact.name // "-") + " | " + (.artifact.version // "-") + " | " + ((.vulnerability.description // "") | gsub("\n"; " ") | .[0:250]))
124+
' "${A_GRYPE}" | head -n 1 >> "${OUT}"
125+
done < /tmp/new_in_a_ids.txt
126+
else
127+
echo "No unique VulnerabilityIDs in ${A_BRANCH} vs ${B_BRANCH}." >> "${OUT}"
128+
fi
129+
else
130+
echo "No vulnerabilities found in ${A_BRANCH}." >> "${OUT}"
131+
fi
132+
echo "" >> "${OUT}"
133+
134+
# New vulnerabilities in B not in A (by ID)
135+
echo "## VulnerabilityIDs present in ${B_BRANCH} but NOT in ${A_BRANCH}" >> "${OUT}"
136+
echo "" >> "${OUT}"
137+
if [ -s /tmp/b_ids.txt ]; then
138+
comm -13 /tmp/a_ids.txt /tmp/b_ids.txt > /tmp/new_in_b_ids.txt || true
139+
if [ -s /tmp/new_in_b_ids.txt ]; then
140+
while read -r id; do
141+
jq --arg id "$id" -r '
142+
.matches[]? | select(.vulnerability?.id==$id) |
143+
("- " + (.vulnerability.id // "-") + " | " + (.vulnerability.severity // "-") + " | " + (.artifact.name // "-") + " | " + (.artifact.version // "-") + " | " + ((.vulnerability.description // "") | gsub("\n"; " ") | .[0:250]))
144+
' "${B_GRYPE}" | head -n 1 >> "${OUT}"
145+
done < /tmp/new_in_b_ids.txt
146+
else
147+
echo "No unique VulnerabilityIDs in ${B_BRANCH} vs ${A_BRANCH}." >> "${OUT}"
148+
fi
149+
else
150+
echo "No vulnerabilities found in ${B_BRANCH}." >> "${OUT}"
151+
fi
152+
echo "" >> "${OUT}"
153+
154+
# New vulnerable package:version in A not in B
155+
echo "## package:version present in ${A_BRANCH} but NOT in ${B_BRANCH}" >> "${OUT}"
156+
echo "" >> "${OUT}"
157+
if [ -s /tmp/a_pkgs.txt ]; then
158+
comm -23 /tmp/a_pkgs.txt /tmp/b_pkgs.txt > /tmp/new_in_a_pkgs.txt || true
159+
if [ -s /tmp/new_in_a_pkgs.txt ]; then
160+
while read -r pv; do
161+
# show a sample vulnerability that affects this pkg:version
162+
jq -r --arg pv "$pv" '
163+
.matches[]? | select((.artifact.name // "-") + ":" + (.artifact.version // "-") == $pv) |
164+
("- " + $pv + " | " + (.vulnerability.id // "-") + " | " + (.vulnerability.severity // "-") + " | " + ((.vulnerability.description // "") | gsub("\n"; " ") | .[0:200]))
165+
' "${A_GRYPE}" | head -n 1 >> "${OUT}"
166+
done < /tmp/new_in_a_pkgs.txt
167+
else
168+
echo "No unique package:version in ${A_BRANCH} vs ${B_BRANCH}." >> "${OUT}"
169+
fi
170+
else
171+
echo "No vulnerable packages found in ${A_BRANCH}." >> "${OUT}"
172+
fi
173+
echo "" >> "${OUT}"
174+
175+
# New vulnerable package:version in B not in A
176+
echo "## package:version present in ${B_BRANCH} but NOT in ${A_BRANCH}" >> "${OUT}"
177+
echo "" >> "${OUT}"
178+
if [ -s /tmp/b_pkgs.txt ]; then
179+
comm -13 /tmp/a_pkgs.txt /tmp/b_pkgs.txt > /tmp/new_in_b_pkgs.txt || true
180+
if [ -s /tmp/new_in_b_pkgs.txt ]; then
181+
while read -r pv; do
182+
jq -r --arg pv "$pv" '
183+
.matches[]? | select((.artifact.name // "-") + ":" + (.artifact.version // "-") == $pv) |
184+
("- " + $pv + " | " + (.vulnerability.id // "-") + " | " + (.vulnerability.severity // "-") + " | " + ((.vulnerability.description // "") | gsub("\n"; " ") | .[0:200]))
185+
' "${B_GRYPE}" | head -n 1 >> "${OUT}"
186+
done < /tmp/new_in_b_pkgs.txt
187+
else
188+
echo "No unique package:version in ${B_BRANCH} vs ${A_BRANCH}." >> "${OUT}"
189+
fi
190+
else
191+
echo "No vulnerable packages found in ${B_BRANCH}." >> "${OUT}"
192+
fi
193+
echo "" >> "${OUT}"
194+
195+
echo "----" >> "${OUT}"
196+
echo "Artifacts included:" >> "${OUT}"
197+
echo "- ${REPORT_DIR}/branchA-sbom.cdx.json" >> "${OUT}"
198+
echo "- ${REPORT_DIR}/branchB-sbom.cdx.json" >> "${OUT}"
199+
echo "- ${REPORT_DIR}/branchA-grype.json" >> "${OUT}"
200+
echo "- ${REPORT_DIR}/branchB-grype.json" >> "${OUT}"
201+
echo "- ${REPORT_DIR}/comparison-report.md (this file)" >> "${OUT}"
202+
203+
- name: Create ZIP of reports
204+
run: |
205+
cd "${REPORT_DIR}"
206+
zip -r comparison-artifacts.zip . || true
207+
208+
- name: Upload artifacts (reports)
209+
uses: actions/upload-artifact@v4
210+
with:
211+
name: vuln-comparison-${{ github.run_id }}
212+
path: ${{ env.REPORT_DIR }}

0 commit comments

Comments
 (0)