|
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 }}' |
3 | 3 | 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 |
4 | 19 | workflow_dispatch: |
5 | 20 | inputs: |
6 | 21 | branch_a: |
7 | | - description: 'First branch to compare (e.g. main)' |
| 22 | + description: 'Base branch (e.g. develop)' |
8 | 23 | required: true |
9 | 24 | default: 'develop' |
10 | 25 | branch_b: |
11 | | - description: 'Second branch to compare (e.g. feature/fix-branch)' |
| 26 | + description: 'Head branch (e.g. TASK-1234)' |
12 | 27 | required: true |
13 | | - default: '' |
14 | 28 |
|
15 | 29 | 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 }} |
32 | 32 |
|
33 | | - - name: Checkout branch B |
| 33 | + steps: |
| 34 | + # 1) Checkout head branch only |
| 35 | + - name: Checkout head branch |
34 | 36 | uses: actions/checkout@v4 |
35 | 37 | with: |
36 | 38 | ref: ${{ github.event.inputs.branch_b }} |
37 | | - path: branchB |
38 | 39 | fetch-depth: 0 |
| 40 | + fetch-tags: true |
39 | 41 |
|
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/vuln-diff-action@v2.0.0-alpha.1 |
| 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" |
254 | 55 |
|
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 |
262 | 56 |
|
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" |
268 | 65 |
|
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