From 1ad940da8eb5d8d7c9839a711d25f8b16c46f19e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira da Silva Date: Mon, 31 Aug 2026 18:50:40 -0300 Subject: [PATCH] Pick the latest CVE ID when parsing SecAlert response When a SecAlert reply subject references an old CVE (e.g., "Incomplete fix for CVE-2026-9083") while the body contains the newly assigned CVE, the bot picked the wrong one. Now collects all CVE IDs from both subject and body, and selects the one with the highest sequence number. Closes #80 Signed-off-by: Bruno Oliveira da Silva --- .../gh/bot/security/email/MailProcessor.java | 35 ++++-- .../bot/security/email/MailProcessorTest.java | 106 +++++++++++++----- 2 files changed, 109 insertions(+), 32 deletions(-) diff --git a/src/main/java/org/keycloak/gh/bot/security/email/MailProcessor.java b/src/main/java/org/keycloak/gh/bot/security/email/MailProcessor.java index 42899e1..193bfcb 100644 --- a/src/main/java/org/keycloak/gh/bot/security/email/MailProcessor.java +++ b/src/main/java/org/keycloak/gh/bot/security/email/MailProcessor.java @@ -22,6 +22,8 @@ import java.io.IOException; import java.time.Duration; +import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -39,6 +41,9 @@ public class MailProcessor { private static final Pattern PSIRT_CLOSURE_PATTERN = Pattern.compile( "Your request\\s+[A-Z]+-\\d+.*this request is now closed", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); + private static final Comparator CVE_COMPARATOR = Comparator + .comparingInt(MailProcessor::cveYear) + .thenComparingInt(MailProcessor::cveSequence); @ConfigProperty(name = "google.group.target") String targetGroupEmail; @@ -322,10 +327,7 @@ void recordSecAlertThreadIdIfMissing(GHIssue issue, String threadId) throws IOEx } void applyCveIdFromSecAlert(GHIssue issue, String subject, String body) throws IOException { - String cveId = extractCveId(subject); - if (cveId == null) { - cveId = extractCveId(body); - } + String cveId = selectLatestCveId(subject, body); if (cveId == null) return; String title = issue.getTitle(); @@ -350,10 +352,29 @@ void applyCveIdFromSecAlert(GHIssue issue, String subject, String body) throws I } } - static String extractCveId(String text) { - if (text == null) return null; + static List extractAllCveIds(String text) { + if (text == null) return List.of(); + List cves = new ArrayList<>(); Matcher matcher = Constants.CVE_PATTERN.matcher(text); - return matcher.find() ? matcher.group() : null; + while (matcher.find()) { + cves.add(matcher.group()); + } + return List.copyOf(cves); + } + + private static int cveYear(String cve) { + return Integer.parseInt(cve.substring(4, cve.lastIndexOf('-'))); + } + + private static int cveSequence(String cve) { + return Integer.parseInt(cve.substring(cve.lastIndexOf('-') + 1)); + } + + static String selectLatestCveId(String subject, String body) { + return Stream.concat(extractAllCveIds(subject).stream(), extractAllCveIds(body).stream()) + .distinct() + .max(CVE_COMPARATOR) + .orElse(null); } private boolean isValidGroupMessage(Map headers) { diff --git a/src/test/java/org/keycloak/gh/bot/security/email/MailProcessorTest.java b/src/test/java/org/keycloak/gh/bot/security/email/MailProcessorTest.java index c182fd8..66328a7 100644 --- a/src/test/java/org/keycloak/gh/bot/security/email/MailProcessorTest.java +++ b/src/test/java/org/keycloak/gh/bot/security/email/MailProcessorTest.java @@ -75,31 +75,6 @@ void formatReplyComment_containsOnlyFromAndSeparator() { assertEquals("From: bob@example.com\n\n---\n\nReply body", result); } - @Test - void extractCveId_findsIdInSubject() { - assertEquals("CVE-2026-1234", MailProcessor.extractCveId("Re: CVE-2026-1234 XSS in admin console")); - } - - @Test - void extractCveId_findsIdInBody() { - assertEquals("CVE-2026-45678", MailProcessor.extractCveId("The assigned CVE ID is CVE-2026-45678. Please confirm.")); - } - - @Test - void extractCveId_returnsNullWhenNoCvePresent() { - assertNull(MailProcessor.extractCveId("No CVE here, just a regular message.")); - } - - @Test - void extractCveId_returnsNullForNullInput() { - assertNull(MailProcessor.extractCveId(null)); - } - - @Test - void extractCveId_returnsFirstCveWhenMultiplePresent() { - assertEquals("CVE-2026-1111", MailProcessor.extractCveId("CVE-2026-1111 and CVE-2026-2222")); - } - @Test void applyCveIdFromSecAlert_replacesTitleAndRemovesCveRequestLabel() throws Exception { GHIssue issue = mock(GHIssue.class); @@ -181,6 +156,87 @@ void applyCveIdFromSecAlert_doesNotAddCveKindWhenAlreadyPresent() throws Excepti verify(issue, never()).addLabels(Kind.CVE.toLabel()); } + // --- extractAllCveIds --- + + @Test + void extractAllCveIds_findsMultipleCves() { + List result = MailProcessor.extractAllCveIds("CVE-2026-9083 and CVE-2026-19729"); + assertEquals(List.of("CVE-2026-9083", "CVE-2026-19729"), result); + } + + @Test + void extractAllCveIds_returnsEmptyForNull() { + assertTrue(MailProcessor.extractAllCveIds(null).isEmpty()); + } + + @Test + void extractAllCveIds_returnsEmptyWhenNoCve() { + assertTrue(MailProcessor.extractAllCveIds("No CVE here").isEmpty()); + } + + // --- selectLatestCveId --- + + @Test + void selectLatestCveId_picksHighestFromSubjectAndBody() { + assertEquals("CVE-2026-19729", + MailProcessor.selectLatestCveId("Incomplete fix for CVE-2026-9083", "*Reference : CVE-2026-19729")); + } + + @Test + void selectLatestCveId_picksHighestWhenMultipleInBody() { + assertEquals("CVE-2026-19729", + MailProcessor.selectLatestCveId(null, "Report about CVE-2026-9083. Assigned CVE-2026-19729.")); + } + + @Test + void selectLatestCveId_fallsBackToSubjectWhenBodyEmpty() { + assertEquals("CVE-2026-9083", + MailProcessor.selectLatestCveId("Fix for CVE-2026-9083", "No CVE in body")); + } + + @Test + void selectLatestCveId_returnsNullWhenNoCveAnywhere() { + assertNull(MailProcessor.selectLatestCveId("No CVE", "Also no CVE")); + } + + @Test + void selectLatestCveId_returnsNullWhenBothInputsNull() { + assertNull(MailProcessor.selectLatestCveId(null, null)); + } + + @Test + void selectLatestCveId_returnsSingleCveWhenSameInBoth() { + assertEquals("CVE-2026-9083", + MailProcessor.selectLatestCveId("CVE-2026-9083", "Reference: CVE-2026-9083")); + } + + @Test + void selectLatestCveId_comparesAcrossYears() { + assertEquals("CVE-2026-1", + MailProcessor.selectLatestCveId("CVE-2025-999999", "CVE-2026-1")); + } + + // --- applyCveIdFromSecAlert bug scenario --- + + @Test + void applyCveIdFromSecAlert_picksNewCveOverOldInSubject() throws Exception { + GHIssue issue = mock(GHIssue.class); + when(issue.getTitle()).thenReturn("[CVE-TBD] Incomplete fix for CVE-2026-9083"); + when(issue.getNumber()).thenReturn(992); + + GHLabel cveRequestLabel = mock(GHLabel.class); + when(cveRequestLabel.getName()).thenReturn(Status.CVE_REQUEST.toLabel()); + when(issue.getLabels()).thenReturn(List.of(cveRequestLabel)); + + MailProcessor processor = new MailProcessor(); + processor.applyCveIdFromSecAlert(issue, "Incomplete fix for CVE-2026-9083 - #GHI-992", + "*Reference : CVE-2026-19729\n*Embargo status : Public"); + + verify(issue).setTitle("[CVE-2026-19729] Incomplete fix for CVE-2026-9083"); + verify(issue).removeLabels(Status.CVE_REQUEST.toLabel()); + verify(issue).addLabels(Kind.CVE.toLabel()); + } + // --- resolveIssueByGhiTag --- @Test