Update procedure steps to generate links for mutliple events

This commit is contained in:
Ryan Carroll
2026-02-19 14:52:09 -06:00
parent e6e6008e41
commit d5f7e57201
7 changed files with 165 additions and 0 deletions

View File

@@ -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<EventDto> 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<EventDto> events) {
throw new UnsupportedOperationException();
}
}
}

View File

@@ -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<EventProcedureS
return action() != null ? action().config().getLink(event) : null;
}
public URIBuilder getLink(Set<EventDto> events) {
return action() != null ? action().config().getLink(events) : null;
}
@AutoValue.Builder
public abstract static class Builder implements ScopedEntity.Builder<Builder> {

View File

@@ -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<EventDto> 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() {

View File

@@ -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<EventDto> events) {
throw new UnsupportedOperationException("Bulk execution is not supported for Go To Dashboard steps");
}
@JsonIgnore
@Override
public String validate() {

View File

@@ -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<EventDto> events) {
return getLink((EventDto) null);
}
@JsonIgnore
@Override
public String validate() {

View File

@@ -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<EventDto> 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() {

View File

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