66 branch_a :
77 description : ' First branch to compare (e.g. main)'
88 required : true
9- default : ' main '
9+ default : ' develop '
1010 branch_b :
1111 description : ' Second branch to compare (e.g. feature/fix-branch)'
1212 required : true
9393 echo "branchB-grype.json missing"
9494 fi
9595
96- - name : Generate robust comparison report (by VulnerabilityID and by package:version)
96+ - name : Generate comparison report (table + full MD)
97+ id : gen_report
9798 run : |
9899 set -euo pipefail
99100 A_BRANCH="${{ github.event.inputs.branch_a }}"
@@ -103,7 +104,7 @@ jobs:
103104 OUT="${REPORT_DIR}/comparison-report.md"
104105 mkdir -p "$(dirname "$OUT")"
105106
106- # --- basic presence checks ---
107+ # comprueba que los JSON existen
107108 if [ ! -s "$A_GRYPE" ]; then
108109 echo "ERROR: ${A_GRYPE} not found or empty" >&2
109110 exit 1
@@ -113,147 +114,82 @@ jobs:
113114 exit 1
114115 fi
115116
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+ # unimos y normalizamos
137+ cat /tmp/a_entries.txt /tmp/b_entries.txt | sort -u > /tmp/all_entries.txt || true
138+
139+ # --- START MD FILE ---
116140 echo "# Vulnerability comparison: ${A_BRANCH} **vs** ${B_BRANCH}" > "${OUT}"
117141 echo "" >> "${OUT}"
118142
119- # Totals (unique vulnerability IDs)
143+ # --- TABLE requested: VulnerabilityID | package:version | Severity | branches ---
144+ echo "| VulnerabilityID | package:version | Severity | branches |" >> "${OUT}"
145+ echo "|---|---|---|---|" >> "${OUT}"
146+
147+ if [ -s /tmp/all_entries.txt ]; then
148+ while IFS= read -r line; do
149+ # line format: ID|pkg:version|SEVERITY
150+ id=$(echo "$line" | awk -F'|' '{print $1}')
151+ pv=$(echo "$line" | awk -F'|' '{print $2}')
152+ sev=$(echo "$line" | awk -F'|' '{print $3}')
153+
154+ inA=0; inB=0
155+ if grep -Fxq "$line" /tmp/a_entries.txt 2>/dev/null; then inA=1; fi
156+ if grep -Fxq "$line" /tmp/b_entries.txt 2>/dev/null; then inB=1; fi
157+
158+ if [ "$inA" -eq 1 ] && [ "$inB" -eq 1 ]; then
159+ branches="**BOTH**"
160+ elif [ "$inA" -eq 1 ]; then
161+ branches="${A_BRANCH}"
162+ else
163+ branches="${B_BRANCH}"
164+ fi
165+
166+ # salida segura (escapa pipes en campos si hay)
167+ # aquí asumimos que id/pv/sev no contienen pipes adicionales
168+ echo "| ${id} | ${pv} | ${sev} | ${branches} |" >> "${OUT}"
169+ done < /tmp/all_entries.txt
170+ else
171+ echo "| - | - | - | - |" >> "${OUT}"
172+ echo "" >> "${OUT}"
173+ fi
174+
175+ echo "" >> "${OUT}"
176+ # --- Totals y resto del MD (se mantienen para contexto) ---
120177 totalA=$(jq -r '[ .matches[]?.vulnerability?.id ] | unique | length' "${A_GRYPE}" 2>/dev/null || echo 0)
121178 totalB=$(jq -r '[ .matches[]?.vulnerability?.id ] | unique | length' "${B_GRYPE}" 2>/dev/null || echo 0)
122179 echo "- **Total unique vulnerability IDs**: ${totalA} (${A_BRANCH}) | ${totalB} (${B_BRANCH})" >> "${OUT}"
123180 echo "" >> "${OUT}"
124181
125- # Totals by package:version (unique vulnerable packages)
126- # Use a robust expression: handle .artifact being object or string
127- jq -r '[ .matches[]? |
128- ( if (.artifact | type) == "object"
129- then ((.artifact.name // .artifact.id // "-") + ":" + (.artifact.version // "-"))
130- else ((.artifact // "-") + ":" + ("-"))
131- end)
132- ] | unique | .[]' "${A_GRYPE}" 2>/dev/null | sort > /tmp/a_pkgs.txt || true
133- jq -r '[ .matches[]? |
134- ( if (.artifact | type) == "object"
135- then ((.artifact.name // .artifact.id // "-") + ":" + (.artifact.version // "-"))
136- else ((.artifact // "-") + ":" + ("-"))
137- end)
138- ] | unique | .[]' "${B_GRYPE}" 2>/dev/null | sort > /tmp/b_pkgs.txt || true
139-
140- pkgsA=$(wc -l < /tmp/a_pkgs.txt 2>/dev/null || echo 0)
141- pkgsB=$(wc -l < /tmp/b_pkgs.txt 2>/dev/null || echo 0)
142- echo "- **Total unique vulnerable package:version**: ${pkgsA} (${A_BRANCH}) | ${pkgsB} (${B_BRANCH})" >> "${OUT}"
143- echo "" >> "${OUT}"
144-
145- # Severity table (counts by unique vulnerability ID) — normalize severity uppercase
146- echo "## Vulnerabilities by severity (counted by unique VulnerabilityID)" >> "${OUT}"
147- echo "" >> "${OUT}"
182+ # tabla de severidad (como antes)
148183 echo "| Severity | ${A_BRANCH} | ${B_BRANCH} |" >> "${OUT}"
149184 echo "|---:|---:|---:|" >> "${OUT}"
150185 for sev in CRITICAL HIGH MEDIUM LOW UNKNOWN; do
151186 ca=$(jq --arg s "$sev" '[ .matches[]?.vulnerability? | select((.severity // "") | ascii_upcase == $s) | .id ] | unique | length' "${A_GRYPE}" 2>/dev/null || echo 0)
152187 cb=$(jq --arg s "$sev" '[ .matches[]?.vulnerability? | select((.severity // "") | ascii_upcase == $s) | .id ] | unique | length' "${B_GRYPE}" 2>/dev/null || echo 0)
153188 echo "| $sev | $ca | $cb |" >> "${OUT}"
154189 done
155- echo "" >> "${OUT}"
156-
157- # Prepare lists of unique vulnerability IDs
158- jq -r '[ .matches[]?.vulnerability?.id ] | unique | .[]' "${A_GRYPE}" 2>/dev/null | sort > /tmp/a_ids.txt || true
159- jq -r '[ .matches[]?.vulnerability?.id ] | unique | .[]' "${B_GRYPE}" 2>/dev/null | sort > /tmp/b_ids.txt || true
160-
161- # New vulnerabilities in A not in B (by ID)
162- echo "## VulnerabilityIDs present in ${A_BRANCH} but NOT in ${B_BRANCH}" >> "${OUT}"
163- echo "" >> "${OUT}"
164- if [ -s /tmp/a_ids.txt ]; then
165- comm -23 /tmp/a_ids.txt /tmp/b_ids.txt > /tmp/new_in_a_ids.txt || true
166- if [ -s /tmp/new_in_a_ids.txt ]; then
167- while read -r id; do
168- jq --arg id "$id" -r '
169- .matches[]? | select(.vulnerability?.id==$id) |
170- ("- " + (.vulnerability.id // "-") + " | " + ((.vulnerability.severity // "") | ascii_upcase // "-") + " | " +
171- ( if (.artifact | type) == "object" then (.artifact.name // .artifact.id // "-") else (.artifact // "-") end ) + " | " +
172- ( if (.artifact | type) == "object" then (.artifact.version // "-") else "-" end ) + " | " +
173- ((.vulnerability.description // "") | gsub("\n"; " ") | .[0:250] )
174- )
175- ' "${A_GRYPE}" | head -n 1 >> "${OUT}"
176- done < /tmp/new_in_a_ids.txt
177- else
178- echo "No unique VulnerabilityIDs in ${A_BRANCH} vs ${B_BRANCH}." >> "${OUT}"
179- fi
180- else
181- echo "No vulnerabilities found in ${A_BRANCH}." >> "${OUT}"
182- fi
183- echo "" >> "${OUT}"
184-
185- # New vulnerabilities in B not in A (by ID)
186- echo "## VulnerabilityIDs present in ${B_BRANCH} but NOT in ${A_BRANCH}" >> "${OUT}"
187- echo "" >> "${OUT}"
188- if [ -s /tmp/b_ids.txt ]; then
189- comm -13 /tmp/a_ids.txt /tmp/b_ids.txt > /tmp/new_in_b_ids.txt || true
190- if [ -s /tmp/new_in_b_ids.txt ]; then
191- while read -r id; do
192- jq --arg id "$id" -r '
193- .matches[]? | select(.vulnerability?.id==$id) |
194- ("- " + (.vulnerability.id // "-") + " | " + ((.vulnerability.severity // "") | ascii_upcase // "-") + " | " +
195- ( if (.artifact | type) == "object" then (.artifact.name // .artifact.id // "-") else (.artifact // "-") end ) + " | " +
196- ( if (.artifact | type) == "object" then (.artifact.version // "-") else "-" end ) + " | " +
197- ((.vulnerability.description // "") | gsub("\n"; " ") | .[0:250] )
198- )
199- ' "${B_GRYPE}" | head -n 1 >> "${OUT}"
200- done < /tmp/new_in_b_ids.txt
201- else
202- echo "No unique VulnerabilityIDs in ${B_BRANCH} vs ${A_BRANCH}." >> "${OUT}"
203- fi
204- else
205- echo "No vulnerabilities found in ${B_BRANCH}." >> "${OUT}"
206- fi
207- echo "" >> "${OUT}"
208190
209- # New vulnerable package:version in A not in B
210- echo "## package:version present in ${A_BRANCH} but NOT in ${B_BRANCH}" >> "${OUT}"
211191 echo "" >> "${OUT}"
212- if [ -s /tmp/a_pkgs.txt ]; then
213- comm -23 /tmp/a_pkgs.txt /tmp/b_pkgs.txt > /tmp/new_in_a_pkgs.txt || true
214- if [ -s /tmp/new_in_a_pkgs.txt ]; then
215- while read -r pv; do
216- jq -r --arg pv "$pv" '
217- .matches[]? | select(
218- ( if (.artifact|type) == "object" then ((.artifact.name // .artifact.id // "-") + ":" + (.artifact.version // "-")) else ((.artifact // "-") + ":" + "-") end) == $pv
219- ) |
220- ("- " + $pv + " | " + (.vulnerability.id // "-") + " | " + ((.vulnerability.severity // "") | ascii_upcase // "-") + " | " +
221- ((.vulnerability.description // "") | gsub("\n"; " ") | .[0:200])
222- )
223- ' "${A_GRYPE}" | head -n 1 >> "${OUT}"
224- done < /tmp/new_in_a_pkgs.txt
225- else
226- echo "No unique package:version in ${A_BRANCH} vs ${B_BRANCH}." >> "${OUT}"
227- fi
228- else
229- echo "No vulnerable packages found in ${A_BRANCH}." >> "${OUT}"
230- fi
231- echo "" >> "${OUT}"
232-
233- # New vulnerable package:version in B not in A
234- echo "## package:version present in ${B_BRANCH} but NOT in ${A_BRANCH}" >> "${OUT}"
235- echo "" >> "${OUT}"
236- if [ -s /tmp/b_pkgs.txt ]; then
237- comm -13 /tmp/a_pkgs.txt /tmp/b_pkgs.txt > /tmp/new_in_b_pkgs.txt || true
238- if [ -s /tmp/new_in_b_pkgs.txt ]; then
239- while read -r pv; do
240- jq -r --arg pv "$pv" '
241- .matches[]? | select(
242- ( if (.artifact|type) == "object" then ((.artifact.name // .artifact.id // "-") + ":" + (.artifact.version // "-")) else ((.artifact // "-") + ":" + "-") end) == $pv
243- ) |
244- ("- " + $pv + " | " + (.vulnerability.id // "-") + " | " + ((.vulnerability.severity // "") | ascii_upcase // "-") + " | " +
245- ((.vulnerability.description // "") | gsub("\n"; " ") | .[0:200])
246- )
247- ' "${B_GRYPE}" | head -n 1 >> "${OUT}"
248- done < /tmp/new_in_b_pkgs.txt
249- else
250- echo "No unique package:version in ${B_BRANCH} vs ${A_BRANCH}." >> "${OUT}"
251- fi
252- else
253- echo "No vulnerable packages found in ${B_BRANCH}." >> "${OUT}"
254- fi
255- echo "" >> "${OUT}"
256-
192+ # añadimos secciones de detalle (opcionales) - mantenidas o puedes comentarlas si no quieres
257193 echo "----" >> "${OUT}"
258194 echo "Artifacts included:" >> "${OUT}"
259195 echo "- ${REPORT_DIR}/branchA-sbom.cdx.json" >> "${OUT}"
@@ -268,6 +204,55 @@ jobs:
268204 cd "${REPORT_DIR}"
269205 zip -r comparison-artifacts.zip . || true
270206
207+ - name : " Publish table to GitHub Actions summary (only the table)"
208+ if : always()
209+ run : |
210+ set -euo pipefail
211+ SUMMARY="$GITHUB_STEP_SUMMARY"
212+ A_BRANCH="${{ github.event.inputs.branch_a }}"
213+ B_BRANCH="${{ github.event.inputs.branch_b }}"
214+
215+ # Comprueba que exista la tabla (en /tmp/all_entries o en reports MD)
216+ if [ ! -f /tmp/all_entries.txt ] || [ ! -s /tmp/all_entries.txt ]; then
217+ # Si all_entries no tiene datos, intentamos extraer del MD
218+ if [ -f "${REPORT_DIR}/comparison-report.md" ]; then
219+ # extraemos la tabla completa entre el header y la línea en blanco que sigue
220+ awk '
221+ BEGIN {found=0}
222+ /^\\| VulnerabilityID \\| package:version \\| Severity \\| branches \\|/ {found=1; print; next}
223+ found==1 { if (/^$/) exit; print }
224+ ' "${REPORT_DIR}/comparison-report.md" >> "$SUMMARY" || true
225+ else
226+ echo "No table available to show in summary." >> "$SUMMARY"
227+ fi
228+ exit 0
229+ fi
230+
231+ # Build the table header in the summary
232+ echo "| VulnerabilityID | package:version | Severity | branches |" >> "$SUMMARY"
233+ echo "|---|---|---|---|" >> "$SUMMARY"
234+
235+ # For each entry in /tmp/all_entries.txt, build row with branches decision (same logic as in MD)
236+ while IFS= read -r line; do
237+ id=$(echo "$line" | awk -F'|' '{print $1}')
238+ pv=$(echo "$line" | awk -F'|' '{print $2}')
239+ sev=$(echo "$line" | awk -F'|' '{print $3}')
240+
241+ inA=0; inB=0
242+ if grep -Fxq "$line" /tmp/a_entries.txt 2>/dev/null; then inA=1; fi
243+ if grep -Fxq "$line" /tmp/b_entries.txt 2>/dev/null; then inB=1; fi
244+
245+ if [ "$inA" -eq 1 ] && [ "$inB" -eq 1 ]; then
246+ branches="**BOTH**"
247+ elif [ "$inA" -eq 1 ]; then
248+ branches="${A_BRANCH}"
249+ else
250+ branches="${B_BRANCH}"
251+ fi
252+
253+ echo "| ${id} | ${pv} | ${sev} | ${branches} |" >> "$SUMMARY"
254+ done < /tmp/all_entries.txt
255+
271256 - name : Upload artifacts (reports)
272257 uses : actions/upload-artifact@v4
273258 with :
0 commit comments