Skip to content

Commit 963ff06

Browse files
Add GitHub Actions summary and improve notification messages
Adds formatted markdown summary output to workflow Summary tab showing: - CVE ID with severity badge emoji (πŸ”΄πŸŸ πŸŸ‘πŸŸ’) - Product, version, channel, download site details - Affected package and CVSS/EPSS metrics - Description preview, CWEs, install paths, PURL - Reference URLs and collapsible scan metadata Also improves notification log messages to include product/channel context: - Before: "[DRY RUN] Would send Teams notification for CVE-2025-1234" - After: "[DRY RUN] Would send Teams notification for CVE-2025-1234 in chef/stable" This helps differentiate multiple CVEs in workflow logs and provides stakeholders with a clear preview of notification content. Signed-off-by: peter-at-progress <parsenau@progress.com> Signed-off-by: Peter Arsenault <parsenau@progress.com>
1 parent 5953039 commit 963ff06

1 file changed

Lines changed: 134 additions & 6 deletions

File tree

β€Ž.github/actions/notify-new-cves/notify.pyβ€Ž

Lines changed: 134 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -542,8 +542,11 @@ def send_teams_notification(notifications: list[Notification], webhook_url: str,
542542
for notification in notifications:
543543
card = format_teams_card(notification)
544544

545+
# Build descriptive identifier
546+
identifier = f"{notification.cve_id} in {notification.product}/{notification.channel}"
547+
545548
if dry_run:
546-
gha_notice(f"[DRY RUN] Would send Teams notification for {notification.cve_id}")
549+
gha_notice(f"[DRY RUN] Would send Teams notification for {identifier}")
547550
print(json.dumps(card, indent=2))
548551
else:
549552
try:
@@ -557,14 +560,135 @@ def send_teams_notification(notifications: list[Notification], webhook_url: str,
557560

558561
with urllib.request.urlopen(req, timeout=10) as response:
559562
if response.status == 200:
560-
gha_notice(f"Sent Teams notification for {notification.cve_id}")
563+
gha_notice(f"Sent Teams notification for {identifier}")
561564
else:
562565
gha_warning(
563-
f"Teams webhook returned status {response.status} for {notification.cve_id}"
566+
f"Teams webhook returned status {response.status} for {identifier}"
564567
)
565568

566569
except Exception as e:
567-
gha_error(f"Failed to send Teams notification for {notification.cve_id}: {e}")
570+
gha_error(f"Failed to send Teams notification for {identifier}: {e}")
571+
572+
573+
# ---------------------------------------------------------------------------
574+
# GitHub Actions summary output
575+
# ---------------------------------------------------------------------------
576+
577+
def write_github_summary(notifications: list[Notification]) -> None:
578+
"""
579+
Write CVE notifications to GitHub Actions workflow summary.
580+
581+
Creates a markdown summary of each notification that appears in the
582+
workflow Summary tab, making it easy to visualize what would be sent
583+
to Teams without needing to check Teams directly.
584+
"""
585+
summary_file = os.environ.get("GITHUB_STEP_SUMMARY")
586+
if not summary_file:
587+
# Not running in GitHub Actions, skip
588+
return
589+
590+
try:
591+
with open(summary_file, "a", encoding="utf-8") as f:
592+
f.write("## 🚨 New CVE Notifications\n\n")
593+
f.write(f"**Total:** {len(notifications)} new CVE(s) detected\n\n")
594+
f.write("---\n\n")
595+
596+
for i, notification in enumerate(notifications, 1):
597+
canonical_cve = notification.get_canonical_cve()
598+
cvss_score = notification.get_cvss_score()
599+
epss_percentile = notification.get_epss_percentile()
600+
description = notification.get_description()
601+
cwes = notification.get_cwes()
602+
urls = notification.get_urls()
603+
install_paths = notification.get_install_paths()
604+
purl = notification.get_purl()
605+
606+
# Severity badge/emoji
607+
severity_emoji = {
608+
"Critical": "πŸ”΄",
609+
"High": "🟠",
610+
"Medium": "🟑",
611+
"Low": "🟒",
612+
}
613+
emoji = severity_emoji.get(notification.severity, "βšͺ")
614+
615+
# Header
616+
f.write(f"### {emoji} {i}. {canonical_cve} - {notification.severity}\n\n")
617+
618+
# Product info
619+
f.write(f"**Product:** `{notification.product}` ")
620+
if notification.resolved_version:
621+
f.write(f"version `{notification.resolved_version}` ")
622+
f.write(f"({notification.channel}/{notification.download_site})\n\n")
623+
624+
# Affected package
625+
f.write(f"**Affected Package:** `{notification.package_name} {notification.package_version}`\n\n")
626+
627+
# Metrics table
628+
f.write("| Metric | Value |\n")
629+
f.write("|--------|-------|\n")
630+
if cvss_score is not None:
631+
f.write(f"| CVSS Score | **{cvss_score:.1f}** |\n")
632+
if epss_percentile is not None:
633+
f.write(f"| EPSS Percentile | {epss_percentile:.1%} |\n")
634+
if notification.fix_available:
635+
fix_text = notification.fix_version or "Available (version unknown)"
636+
f.write(f"| Fix Available | βœ… {fix_text} |\n")
637+
else:
638+
f.write(f"| Fix Available | ❌ Not available |\n")
639+
f.write(f"| First Observed | {notification.first_observed_at.strftime('%Y-%m-%d %H:%M UTC')} |\n")
640+
f.write("\n")
641+
642+
# Description
643+
if description:
644+
desc_preview = description[:300] + "..." if len(description) > 300 else description
645+
f.write(f"**Description:**\n> {desc_preview}\n\n")
646+
647+
# CWEs
648+
if cwes:
649+
f.write(f"**CWEs:** {', '.join(f'`{cwe}`' for cwe in cwes)}\n\n")
650+
651+
# Install paths
652+
if install_paths:
653+
f.write("**Install Locations:**\n")
654+
for path in install_paths[:3]:
655+
f.write(f"- `{path}`\n")
656+
if len(install_paths) > 3:
657+
f.write(f"- *(and {len(install_paths) - 3} more)*\n")
658+
f.write("\n")
659+
660+
# PURL
661+
if purl:
662+
f.write(f"**Package URL:** `{purl}`\n\n")
663+
664+
# Reference URLs
665+
if urls:
666+
f.write("**References:**\n")
667+
for url in urls[:5]:
668+
f.write(f"- {url}\n")
669+
if len(urls) > 5:
670+
f.write(f"- *(and {len(urls) - 5} more)*\n")
671+
f.write("\n")
672+
673+
# Scan metadata
674+
if notification.scan_timestamp:
675+
f.write(f"<details>\n")
676+
f.write(f"<summary>Scan Metadata</summary>\n\n")
677+
f.write(f"- **Scan Date:** {notification.scan_timestamp.strftime('%Y-%m-%d %H:%M UTC')}\n")
678+
if notification.grype_version:
679+
f.write(f"- **Grype Version:** {notification.grype_version}\n")
680+
if notification.grype_db_version:
681+
f.write(f"- **Grype DB:** {notification.grype_db_version}\n")
682+
if notification.os:
683+
f.write(f"- **Platform:** {notification.os} {notification.os_version} ({notification.arch})\n")
684+
f.write(f"\n</details>\n\n")
685+
686+
f.write("---\n\n")
687+
688+
gha_notice(f"Wrote {len(notifications)} CVE(s) to workflow summary")
689+
690+
except Exception as e:
691+
gha_warning(f"Failed to write GitHub Actions summary: {e}")
568692

569693

570694
# ---------------------------------------------------------------------------
@@ -588,6 +712,9 @@ def dispatch_notifications(
588712

589713
gha_notice(f"Found {len(notifications)} new CVE(s) to notify")
590714

715+
# Write to GitHub Actions summary (always, for visibility)
716+
write_github_summary(notifications)
717+
591718
# Teams channel
592719
if teams_webhook:
593720
send_teams_notification(notifications, teams_webhook, dry_run)
@@ -668,8 +795,9 @@ def main() -> int:
668795

669796
if not notification.vulnerability:
670797
gha_warning(
671-
f"Could not find Grype match for {row['cve_id']} / "
672-
f"{row['package_name']} {row['package_version']} β€” "
798+
f"Could not find Grype match for {row['cve_id']} in "
799+
f"{row['product']}/{row['channel']} "
800+
f"({row['package_name']} {row['package_version']}) β€” "
673801
"notification will have limited details"
674802
)
675803

0 commit comments

Comments
Β (0)