Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ public class MailProcessor {
private static final Logger LOGGER = Logger.getLogger(MailProcessor.class);
private static final Pattern BRACKET_PREFIX_PATTERN = Pattern.compile("^.*?\\[.*?]\\s*");
private static final Pattern REPLY_PREFIX_PATTERN = Pattern.compile("^(?:\\s*(?:Re|Fwd|Fw)\\s*:\\s*)+", Pattern.CASE_INSENSITIVE);
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);

@ConfigProperty(name = "google.group.target")
String targetGroupEmail;
Expand Down Expand Up @@ -138,9 +141,7 @@ private void processSingleMessage(Message msgSummary, GitHub github, GHRepositor
if (issueOpt.isPresent()) {
var issue = issueOpt.get();
if (issue.getState() == GHIssueState.CLOSED) {
issue.reopen();
issue.addLabels(Labels.STATUS_TRIAGE, Labels.REOPENED_BY_BOT);
LOGGER.infof("Reopened existing closed issue #%d for thread %s", issue.getNumber(), threadId);
handleClosedIssue(issue, fromSecAlert, body, threadId);
}
appendComment(issue, from, body, attachmentSection);

Expand Down Expand Up @@ -241,6 +242,22 @@ private boolean isFromSecAlert(String from, String replyTo) {
.anyMatch(header -> header.toLowerCase().contains(needle));
}

private void handleClosedIssue(GHIssue issue, boolean fromSecAlert, String body, String threadId) throws IOException {
if (fromSecAlert && isPsirtClosureNotification(body)) {
LOGGER.infof("PSIRT closure notification for issue #%d — skipping reopen for thread %s",
issue.getNumber(), threadId);
return;
}
issue.reopen();
issue.addLabels(Labels.STATUS_TRIAGE, Labels.REOPENED_BY_BOT);
LOGGER.infof("Reopened existing closed issue #%d for thread %s", issue.getNumber(), threadId);
}

static boolean isPsirtClosureNotification(String body) {
if (body == null || body.isBlank()) return false;
return PSIRT_CLOSURE_PATTERN.matcher(body).find();
}

private Optional<GHIssue> resolveIssueBySecAlertThreadId(GitHub github, GHRepository repository, String threadId) {
try {
var expectedMarker = Constants.SECALERT_THREAD_ID_PREFIX + " " + threadId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
Expand Down Expand Up @@ -276,6 +277,50 @@ void recordSecAlertThreadIdIfMissing_usesCache() throws Exception {
verify(issue.queryComments(), org.mockito.Mockito.times(1)).list();
}

// --- isPsirtClosureNotification ---

@Test
void isPsirtClosureNotification_detectsRealClosureBody() {
String body = "Your request PSIRTSUPT-21634: Incomplete fix for CVE-2026-9083 - #GHI-992 has been resolved.\n\n"
+ "This request is now closed. If you have a new security concern, please reach out.";
assertTrue(MailProcessor.isPsirtClosureNotification(body));
}

@Test
void isPsirtClosureNotification_detectsDifferentProjectKey() {
String body = "Your request SECVULN-1234: Some issue has been resolved.\n\n"
+ "This request is now closed.";
assertTrue(MailProcessor.isPsirtClosureNotification(body));
}

@Test
void isPsirtClosureNotification_returnsFalseForCveAssignment() {
String body = "The assigned CVE ID is CVE-2026-9999. Please confirm the details.";
assertFalse(MailProcessor.isPsirtClosureNotification(body));
}

@Test
void isPsirtClosureNotification_returnsFalseForTicketRefOnly() {
String body = "Your request PSIRTSUPT-21634 has been updated with new information.";
assertFalse(MailProcessor.isPsirtClosureNotification(body));
}

@Test
void isPsirtClosureNotification_returnsFalseForClosurePhraseOnly() {
String body = "This request is now closed. Thank you.";
assertFalse(MailProcessor.isPsirtClosureNotification(body));
}

@Test
void isPsirtClosureNotification_returnsFalseForNullBody() {
assertFalse(MailProcessor.isPsirtClosureNotification(null));
}

@Test
void isPsirtClosureNotification_returnsFalseForBlankBody() {
assertFalse(MailProcessor.isPsirtClosureNotification(""));
}

@SuppressWarnings("unchecked")
private void stubIssueComments(GHIssue issue, String... commentBodies) throws IOException {
GHIssueCommentQueryBuilder queryBuilder = mock(GHIssueCommentQueryBuilder.class);
Expand Down
Loading