From d5f7e57201343eda5449706f9fbf9ab559bf0378 Mon Sep 17 00:00:00 2001 From: Ryan Carroll Date: Thu, 19 Feb 2026 14:52:09 -0600 Subject: [PATCH] Update procedure steps to generate links for mutliple events --- .../events/procedures/ActionConfig.java | 9 ++ .../events/procedures/EventProcedureStep.java | 6 ++ .../procedures/ExecuteNotification.java | 15 ++++ .../events/procedures/GoToDashboard.java | 7 ++ .../org/graylog/events/procedures/Link.java | 7 ++ .../events/procedures/PerformSearch.java | 38 +++++++++ .../graylog/events/procedures/ActionTest.java | 83 +++++++++++++++++++ 7 files changed, 165 insertions(+) diff --git a/graylog2-server/src/main/java/org/graylog/events/procedures/ActionConfig.java b/graylog2-server/src/main/java/org/graylog/events/procedures/ActionConfig.java index ec820d04e7..5fba9b35e6 100644 --- a/graylog2-server/src/main/java/org/graylog/events/procedures/ActionConfig.java +++ b/graylog2-server/src/main/java/org/graylog/events/procedures/ActionConfig.java @@ -21,6 +21,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import org.apache.http.client.utils.URIBuilder; import org.graylog.events.event.EventDto; +import java.util.Set; + @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, @@ -36,6 +38,8 @@ public interface ActionConfig { URIBuilder getLink(EventDto event); + URIBuilder getLink(Set events); + String validate(); class FallbackConfig implements ActionConfig { @@ -53,5 +57,10 @@ public interface ActionConfig { public URIBuilder getLink(EventDto event) { throw new UnsupportedOperationException(); } + + @Override + public URIBuilder getLink(Set events) { + throw new UnsupportedOperationException(); + } } } diff --git a/graylog2-server/src/main/java/org/graylog/events/procedures/EventProcedureStep.java b/graylog2-server/src/main/java/org/graylog/events/procedures/EventProcedureStep.java index 5b393b29fd..ab334b4aa5 100644 --- a/graylog2-server/src/main/java/org/graylog/events/procedures/EventProcedureStep.java +++ b/graylog2-server/src/main/java/org/graylog/events/procedures/EventProcedureStep.java @@ -25,6 +25,8 @@ import jakarta.annotation.Nullable; import org.apache.http.client.utils.URIBuilder; import org.graylog.events.event.EventDto; import org.graylog2.database.entities.DefaultEntityScope; + +import java.util.Set; import org.graylog2.database.entities.ScopedEntity; import org.graylog2.security.html.HTMLSanitizerConverter; import org.mongojack.Id; @@ -59,6 +61,10 @@ public abstract class EventProcedureStep implements ScopedEntity events) { + return action() != null ? action().config().getLink(events) : null; + } + @AutoValue.Builder public abstract static class Builder implements ScopedEntity.Builder { diff --git a/graylog2-server/src/main/java/org/graylog/events/procedures/ExecuteNotification.java b/graylog2-server/src/main/java/org/graylog/events/procedures/ExecuteNotification.java index c2e8cf85c8..e59c77e12a 100644 --- a/graylog2-server/src/main/java/org/graylog/events/procedures/ExecuteNotification.java +++ b/graylog2-server/src/main/java/org/graylog/events/procedures/ExecuteNotification.java @@ -28,6 +28,9 @@ import jakarta.inject.Inject; import org.apache.http.client.utils.URIBuilder; import org.graylog.events.event.EventDto; +import java.util.Set; +import java.util.stream.Collectors; + /** * Executes an existing notification with the event. */ @@ -70,6 +73,18 @@ public class ExecuteNotification extends Action { return uriBuilder.build().getLinkPath(); } + @JsonIgnore + @Override + public URIBuilder getLink(Set events) { + final String combinedQuery = events.stream() + .map(e -> "id:" + e.id()) + .collect(Collectors.joining(" OR ")); + final TemplateURI.Builder uriBuilder = new TemplateURI.Builder(); + uriBuilder.setPath("security/security-events/alerts"); + uriBuilder.addParameter("query", combinedQuery); + return uriBuilder.build().getLinkPath(); + } + @JsonIgnore @Override public String validate() { diff --git a/graylog2-server/src/main/java/org/graylog/events/procedures/GoToDashboard.java b/graylog2-server/src/main/java/org/graylog/events/procedures/GoToDashboard.java index 1565d55bcd..3dfc120ddb 100644 --- a/graylog2-server/src/main/java/org/graylog/events/procedures/GoToDashboard.java +++ b/graylog2-server/src/main/java/org/graylog/events/procedures/GoToDashboard.java @@ -31,6 +31,7 @@ import org.graylog.events.event.EventDto; import java.util.Collections; import java.util.Map; +import java.util.Set; /** * Redirects the frontend to an existing dashboard. @@ -81,6 +82,12 @@ public class GoToDashboard extends Action { return uriBuilder.build().getLinkPath(); } + @JsonIgnore + @Override + public URIBuilder getLink(Set events) { + throw new UnsupportedOperationException("Bulk execution is not supported for Go To Dashboard steps"); + } + @JsonIgnore @Override public String validate() { diff --git a/graylog2-server/src/main/java/org/graylog/events/procedures/Link.java b/graylog2-server/src/main/java/org/graylog/events/procedures/Link.java index 7d9a9b55c1..27fd0d275e 100644 --- a/graylog2-server/src/main/java/org/graylog/events/procedures/Link.java +++ b/graylog2-server/src/main/java/org/graylog/events/procedures/Link.java @@ -29,6 +29,7 @@ import org.apache.http.client.utils.URIBuilder; import org.graylog.events.event.EventDto; import java.net.URISyntaxException; +import java.util.Set; /** * Redirects the frontend to a link. @@ -75,6 +76,12 @@ public class Link extends Action { } } + @JsonIgnore + @Override + public URIBuilder getLink(Set events) { + return getLink((EventDto) null); + } + @JsonIgnore @Override public String validate() { diff --git a/graylog2-server/src/main/java/org/graylog/events/procedures/PerformSearch.java b/graylog2-server/src/main/java/org/graylog/events/procedures/PerformSearch.java index c89e4e2c3d..93fc049021 100644 --- a/graylog2-server/src/main/java/org/graylog/events/procedures/PerformSearch.java +++ b/graylog2-server/src/main/java/org/graylog/events/procedures/PerformSearch.java @@ -29,6 +29,8 @@ import jakarta.annotation.Nullable; import jakarta.inject.Inject; import org.apache.http.client.utils.URIBuilder; import org.graylog.events.event.EventDto; +import org.graylog.events.event.EventReplayInfo; +import org.joda.time.DateTime; import java.util.Collections; import java.util.Map; @@ -119,6 +121,42 @@ public class PerformSearch extends Action { return uriBuilder.build().getLinkPath(); } + @JsonIgnore + @Override + public URIBuilder getLink(Set events) { + final TemplateURI.Builder uriBuilder = new TemplateURI.Builder(); + if (Boolean.TRUE.equals(useSavedSearch())) { + uriBuilder.setPath("views/" + savedSearch()); + uriBuilder.setParameters(parameters()); + } else { + uriBuilder.setPath("search"); + uriBuilder.addParameter("q", query()); + } + + DateTime earliestStart = null; + DateTime latestEnd = null; + for (EventDto event : events) { + if (event.replayInfo().isPresent()) { + final EventReplayInfo replayInfo = event.replayInfo().get(); + if (replayInfo.timerangeStart() != null) { + earliestStart = earliestStart == null || replayInfo.timerangeStart().isBefore(earliestStart) + ? replayInfo.timerangeStart() : earliestStart; + } + if (replayInfo.timerangeEnd() != null) { + latestEnd = latestEnd == null || replayInfo.timerangeEnd().isAfter(latestEnd) + ? replayInfo.timerangeEnd() : latestEnd; + } + } + } + if (earliestStart != null && latestEnd != null) { + uriBuilder.addParameter("rangetype", "absolute"); + uriBuilder.addParameter("from", earliestStart.toString()); + uriBuilder.addParameter("to", latestEnd.toString()); + } + + return uriBuilder.build().getLinkPath(); + } + @JsonIgnore @Override public String validate() { diff --git a/graylog2-server/src/test/java/org/graylog/events/procedures/ActionTest.java b/graylog2-server/src/test/java/org/graylog/events/procedures/ActionTest.java index 5c6e8458f5..6aade47d68 100644 --- a/graylog2-server/src/test/java/org/graylog/events/procedures/ActionTest.java +++ b/graylog2-server/src/test/java/org/graylog/events/procedures/ActionTest.java @@ -31,6 +31,7 @@ import java.util.Optional; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.when; @MockitoSettings(strictness = Strictness.WARN) @@ -90,6 +91,88 @@ public class ActionTest { assertThat(actionText).contains("/security/security-events/alerts?query=id%3A" + ID); } + @Test + public void testPerformSearchGetLink_bulkCombinesTimeranges() { + EventDto event1 = mockEventWithReplayInfo("e1", + DateTime.parse("2024-01-01T00:00:00.000Z"), + DateTime.parse("2024-01-01T12:00:00.000Z")); + EventDto event2 = mockEventWithReplayInfo("e2", + DateTime.parse("2024-01-02T00:00:00.000Z"), + DateTime.parse("2024-01-02T12:00:00.000Z")); + + String link = performSearchQueryConfig().getLink(Set.of(event1, event2)).toString(); + + assertThat(link).contains("search?q="); + assertThat(link).contains("rangetype=absolute"); + assertThat(link).contains("from=2024-01-01T00%3A00%3A00.000Z"); + assertThat(link).contains("to=2024-01-02T12%3A00%3A00.000Z"); + } + + @Test + public void testPerformSearchGetLink_bulkSavedSearch() { + EventDto event1 = mockEventWithReplayInfo("e1", + DateTime.parse("2024-03-01T00:00:00.000Z"), + DateTime.parse("2024-03-01T06:00:00.000Z")); + EventDto event2 = mockEventWithReplayInfo("e2", + DateTime.parse("2024-02-15T00:00:00.000Z"), + DateTime.parse("2024-03-02T00:00:00.000Z")); + + String link = performSavedSearchConfig().getLink(Set.of(event1, event2)).toString(); + + assertThat(link).contains("views/" + ID); + assertThat(link).contains("rangetype=absolute"); + assertThat(link).contains("from=2024-02-15T00%3A00%3A00.000Z"); + assertThat(link).contains("to=2024-03-02T00%3A00%3A00.000Z"); + } + + @Test + public void testExecuteNotificationGetLink_bulk() { + EventDto event1 = mockEvent("event-abc"); + EventDto event2 = mockEvent("event-def"); + + String link = executeNotificationConfig().getLink(Set.of(event1, event2)).toString(); + + assertThat(link).contains("security/security-events/alerts"); + assertThat(link).contains("id%3Aevent-abc"); + assertThat(link).contains("id%3Aevent-def"); + assertThat(link).contains("OR"); + } + + @Test + public void testGoToDashboardGetLink_bulkThrowsUnsupported() { + assertThatThrownBy(() -> goToDashboardConfig().getLink(Set.of(event))) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessageContaining("Go To Dashboard"); + } + + @Test + public void testLinkGetLink_bulkReturnsSameLink() { + Link.Config linkConfig = Link.Config.builder() + .link("https://example.com/wiki") + .build(); + + String link = linkConfig.getLink(Set.of(event)).toString(); + + assertThat(link).isEqualTo("https://example.com/wiki"); + } + + private EventDto mockEvent(String id) { + EventDto mock = org.mockito.Mockito.mock(EventDto.class); + when(mock.id()).thenReturn(id); + return mock; + } + + private EventDto mockEventWithReplayInfo(String id, DateTime start, DateTime end) { + EventDto mock = mockEvent(id); + when(mock.replayInfo()).thenReturn(Optional.of(EventReplayInfo.builder() + .timerangeStart(start) + .timerangeEnd(end) + .query("") + .streams(Set.of()) + .build())); + return mock; + } + private ExecuteNotification.Config executeNotificationConfig() { return ExecuteNotification.Config.builder() .type(ExecuteNotification.NAME)