@@ -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