Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3493294
Add MITRE categories field to event definitions
danotorrey May 11, 2026
9255d03
Fix FilterPreviewContainer test fixture missing mitre_categories
danotorrey May 12, 2026
ad94b42
Merge remote-tracking branch 'origin/master' into feature/event-defin…
danotorrey May 20, 2026
7279b9d
Rename to tactics_techniques + add validator extension point
danotorrey May 22, 2026
6c3f288
Merge remote-tracking branch 'origin/master' into feature/event-defin…
danotorrey May 22, 2026
8804671
Merge remote-tracking branch 'origin/master' into feature/event-defin…
danotorrey May 22, 2026
ffc04b9
Merge branch 'master' into feature/event-definition-mitre-tactics-tec…
danotorrey May 22, 2026
a286070
Drop tactics_techniques UI surfaces
danotorrey May 22, 2026
9351a40
Merge branch 'feature/event-definition-mitre-tactics-techniques' of g…
danotorrey May 22, 2026
5f8a55a
Decouple tactics_techniques from UI: optional type + extension point
danotorrey May 22, 2026
6b692aa
Drop TableElement type cleanup from this PR
danotorrey May 23, 2026
f410039
Wording; always show TC Improve Coverage button
danotorrey May 23, 2026
45d5e61
Review fixes
danotorrey May 24, 2026
73d5de0
Review fixes: plugin contract location, key namespace, theme, lint
danotorrey May 24, 2026
17f6d61
Merge remote-tracking branch 'origin/master' into feature/event-defin…
danotorrey May 26, 2026
39a6a4d
Fix test: error message uses upper-cased IDs
danotorrey May 26, 2026
534fe5d
Add dedicated plugin slot for tactics/techniques column
danotorrey May 26, 2026
6261df5
Fix padding-line-between-statements lint
danotorrey May 26, 2026
dc35c83
Add plugin slots for tactics/techniques detail row and summary
danotorrey May 27, 2026
8713282
Merge branch 'master' into feature/event-definition-mitre-tactics-tec…
danotorrey Jun 1, 2026
889caac
Merge remote-tracking branch 'origin/master' into feature/event-defin…
danotorrey Jun 3, 2026
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 @@ -22,6 +22,7 @@
import jakarta.inject.Named;
import org.graylog.events.event.EventDto;
import org.graylog.events.processor.EventProcessorException;
import org.graylog.events.search.MitreBackwardsCompatibilityFilter;
import org.graylog.events.search.MoreSearch;
import org.graylog.events.search.MoreSearchAdapter;
import org.graylog.events.search.SourceStreamFilter;
Expand Down Expand Up @@ -228,7 +229,17 @@ private QueryBuilder createQuery(String queryString, TimeRange timerange, Set<St
.filter(termsQuery(EventDto.FIELD_STREAMS, eventStreams))
.filter(requireNonNull(TimeRangeQueryFactory.create(timerange)));

extraFilters.forEach((field, values) -> {
final BoolQueryBuilder mitreOr = boolQuery().minimumShouldMatch(1);
if (MitreBackwardsCompatibilityFilter.emitShouldClauses(extraFilters,
(k, v) -> mitreOr.should(buildExtraFilter(k, v)))) {
filter.filter(mitreOr);
}

extraFilters.entrySet().stream()
.filter(e -> !MitreBackwardsCompatibilityFilter.isMitreKey(e.getKey()))
.forEach(e -> {
final var field = e.getKey();
final var values = e.getValue();
values.stream()
.filter(MoreSearchAdapter::isRangeValue)
.map(value -> buildExtraFilter(field, value))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import jakarta.inject.Named;
import org.graylog.events.event.EventDto;
import org.graylog.events.processor.EventProcessorException;
import org.graylog.events.search.MitreBackwardsCompatibilityFilter;
import org.graylog.events.search.MoreSearch;
import org.graylog.events.search.MoreSearchAdapter;
import org.graylog.events.search.SourceStreamFilter;
Expand Down Expand Up @@ -229,7 +230,17 @@ private QueryBuilder createQuery(String queryString, TimeRange timerange, Set<St
.filter(termsQuery(EventDto.FIELD_STREAMS, eventStreams))
.filter(requireNonNull(TimeRangeQueryFactory.create(timerange)));

extraFilters.forEach((field, values) -> {
final BoolQueryBuilder mitreOr = boolQuery().minimumShouldMatch(1);
if (MitreBackwardsCompatibilityFilter.emitShouldClauses(extraFilters,
(k, v) -> mitreOr.should(buildExtraFilter(k, v)))) {
filter.filter(mitreOr);
}

extraFilters.entrySet().stream()
.filter(e -> !MitreBackwardsCompatibilityFilter.isMitreKey(e.getKey()))
.forEach(e -> {
final var field = e.getKey();
final var values = e.getValue();
values.stream()
.filter(MoreSearchAdapter::isRangeValue)
.map(value -> buildExtraFilter(field, value))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import jakarta.inject.Named;
import org.graylog.events.event.EventDto;
import org.graylog.events.processor.EventProcessorException;
import org.graylog.events.search.MitreBackwardsCompatibilityFilter;
import org.graylog.events.search.MoreSearch;
import org.graylog.events.search.MoreSearchAdapter;
import org.graylog.events.search.SourceStreamFilter;
Expand Down Expand Up @@ -164,7 +165,17 @@ private Query createQuery(String queryString, TimeRange timerange, Set<String> e
boolQuery.filter(timerangeQuery(timerange));


extraFilters.forEach((field, values) -> {
final BoolQuery.Builder mitreOr = BoolQuery.builder().minimumShouldMatch("1");
if (MitreBackwardsCompatibilityFilter.emitShouldClauses(extraFilters,
(k, v) -> mitreOr.should(buildExtraFilter(k, v)))) {
boolQuery.filter(Query.of(b -> b.bool(mitreOr.build())));
}

extraFilters.entrySet().stream()
.filter(e -> !MitreBackwardsCompatibilityFilter.isMitreKey(e.getKey()))
.forEach(e -> {
final var field = e.getKey();
final var values = e.getValue();
values.stream()
.filter(MoreSearchAdapter::isRangeValue)
.map(value -> buildExtraFilter(field, value))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
import org.graylog.events.processor.EventProcessorExecutionJob;
import org.graylog.events.processor.EventProcessorExecutionMetrics;
import org.graylog.events.processor.EventResolver;
import org.graylog.events.processor.TacticsTechniquesValidator;
import org.graylog.events.processor.aggregation.AggregationEventProcessor;
import org.graylog.events.processor.aggregation.AggregationEventProcessorConfig;
import org.graylog.events.processor.aggregation.AggregationEventProcessorParameters;
Expand Down Expand Up @@ -99,6 +100,9 @@ protected void configure() {
OptionalBinder.newOptionalBinder(binder(), EventResolver.class)
.setDefault().to(DefaultEventResolver.class);

OptionalBinder.newOptionalBinder(binder(), TacticsTechniquesValidator.class)
.setDefault().to(TacticsTechniquesValidator.NoOp.class);

addSystemRestResource(AvailableEntityTypesResource.class);
addSystemRestResource(EventDefinitionsResource.class);
addSystemRestResource(EventNotificationsResource.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public abstract class EventDefinitionEntity extends ScopedContentPackEntity impl
private static final String MATCHED_AT = "matched_at";
private static final String FIELD_EVENT_PROCEDURE = "event_procedure";
private static final String FIELD_EVENT_SUMMARY_TEMPLATE = "event_summary_template";
private static final String FIELD_TACTICS_TECHNIQUES = EventDefinitionDto.FIELD_TACTICS_TECHNIQUES;

@JsonProperty(FIELD_TITLE)
public abstract ValueReference title();
Expand Down Expand Up @@ -124,6 +125,9 @@ public abstract class EventDefinitionEntity extends ScopedContentPackEntity impl
@JsonProperty(FIELD_EVENT_SUMMARY_TEMPLATE)
public abstract ValueReference eventSummaryTemplate();

@JsonProperty(FIELD_TACTICS_TECHNIQUES)
public abstract ImmutableList<String> tacticsTechniques();

public static Builder builder() {
return Builder.create();
}
Expand All @@ -136,6 +140,7 @@ public static abstract class Builder extends ScopedContentPackEntity.AbstractBui
public static Builder create() {
return new AutoValue_EventDefinitionEntity.Builder()
.isScheduled(ValueReference.of(true))
.tacticsTechniques(ImmutableList.of())
.tags(ImmutableSet.of());
}

Expand Down Expand Up @@ -190,6 +195,9 @@ public static Builder create() {
@JsonProperty(FIELD_EVENT_SUMMARY_TEMPLATE)
public abstract Builder eventSummaryTemplate(ValueReference eventSummaryTemplate);

@JsonProperty(FIELD_TACTICS_TECHNIQUES)
public abstract Builder tacticsTechniques(ImmutableList<String> tacticsTechniques);

public abstract EventDefinitionEntity build();
}

Expand Down Expand Up @@ -230,6 +238,7 @@ public EventDefinitionDto toNativeEntity(Map<String, ValueReference> parameters,
.tags(tags())
.eventProcedureId(procedureId)
.eventSummaryTemplate(eventSummaryTemplate() != null ? eventSummaryTemplate().asString(parameters) : null)
.tacticsTechniques(tacticsTechniques())
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ public interface Event extends Indexable {

void addAssociatedAssets(Set<String> associatedAssets);

List<String> getTacticsTechniques();

void setTacticsTechniques(List<String> tacticsTechniques);

Set<String> getTags();

void setTags(Set<String> tags);
Expand All @@ -135,6 +139,7 @@ static Event fromDto(EventDto from) {
from.timerangeEnd().ifPresent(event::setTimerangeEnd);
from.originContext().ifPresent(event::setOriginContext);
from.replayInfo().ifPresent(event::setReplayInfo);
event.setTacticsTechniques(from.tacticsTechniques());
event.setTags(from.tags());

return event;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import org.joda.time.DateTime;
Expand Down Expand Up @@ -57,6 +58,7 @@ public abstract class EventDto {
public static final String FIELD_GROUP_BY_FIELDS = "group_by_fields";
public static final String FIELD_AGGREGATION_CONDITIONS = "aggregation_conditions";
public static final String FIELD_REPLAY_INFO = "replay_info";
public static final String FIELD_TACTICS_TECHNIQUES = "tactics_techniques";

@JsonProperty(FIELD_ID)
public abstract String id();
Expand Down Expand Up @@ -129,6 +131,9 @@ public abstract class EventDto {
@JsonProperty(FIELD_REPLAY_INFO)
public abstract Optional<EventReplayInfo> replayInfo();

@JsonProperty(FIELD_TACTICS_TECHNIQUES)
public abstract List<String> tacticsTechniques();

public static Builder builder() {
return Builder.create();
}
Expand All @@ -148,6 +153,7 @@ public static Builder create() {
.aggregationConditions(ImmutableMap.of())
.scores(ImmutableMap.of())
.associatedAssets(ImmutableSet.of())
.tacticsTechniques(ImmutableList.of())
.tags(ImmutableSet.of());
}

Expand Down Expand Up @@ -224,6 +230,9 @@ public static Builder create() {
@JsonProperty(FIELD_REPLAY_INFO)
public abstract Builder replayInfo(@Nullable EventReplayInfo replayInfo);

@JsonProperty(FIELD_TACTICS_TECHNIQUES)
public abstract Builder tacticsTechniques(List<String> tacticsTechniques);

public abstract EventDto build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public class EventImpl implements Event {
private final Set<String> associatedAssets = new HashSet<>();
private final Set<String> tags = new HashSet<>();
private EventReplayInfo replayInfo;
private List<String> tacticsTechniques = ImmutableList.of();

EventImpl(String eventId,
DateTime eventTimestamp,
Expand Down Expand Up @@ -339,6 +340,16 @@ public void setReplayInfo(EventReplayInfo replayInfo) {
this.replayInfo = replayInfo;
}

@Override
public List<String> getTacticsTechniques() {
return tacticsTechniques;
}

@Override
public void setTacticsTechniques(List<String> tacticsTechniques) {
this.tacticsTechniques = tacticsTechniques == null ? ImmutableList.of() : ImmutableList.copyOf(tacticsTechniques);
}

@Override
public EventDto toDto() {
final Map<String, String> fields = this.fields.entrySet()
Expand Down Expand Up @@ -375,6 +386,7 @@ public EventDto toDto() {
.groupByFields(ImmutableMap.copyOf(groupByFields))
.aggregationConditions(ImmutableMap.copyOf(aggregationConditions))
.replayInfo(getReplayInfo())
.tacticsTechniques(getTacticsTechniques())
.build();
}

Expand Down Expand Up @@ -431,6 +443,7 @@ public boolean equals(Object o) {
Objects.equals(scores, event.scores) &&
Objects.equals(associatedAssets, event.associatedAssets) &&
Objects.equals(tags, event.tags) &&
Objects.equals(tacticsTechniques, event.tacticsTechniques) &&
Objects.equals(replayInfo, event.replayInfo);
}

Expand All @@ -439,7 +452,7 @@ public int hashCode() {
return Objects.hash(eventId, eventDefinitionType, eventDefinitionId, originContext, eventTimestamp,
processingTimestamp, timerangeStart, timerangeEnd, streams, sourceStreams, message, source,
keyTuple, priority, alert, fields, groupByFields, aggregationConditions, scores,
associatedAssets, tags, replayInfo);
associatedAssets, tags, tacticsTechniques, replayInfo);
}

@Override
Expand Down Expand Up @@ -467,6 +480,7 @@ public String toString() {
.add("scores", scores)
.add("associatedAssets", associatedAssets)
.add("tags", tags)
.add("tacticsTechniques", tacticsTechniques)
.toString();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public Event createEvent(EventDefinition eventDefinition, DateTime eventTime, St
eventDefinition.priority(),
eventDefinition.alert()
);
event.setTacticsTechniques(eventDefinition.tacticsTechniques());
event.setTags(eventDefinition.tags());
return event;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,8 @@ default String eventProcedureId() {
default String eventSummaryTemplate() {
return null;
}

default ImmutableList<String> tacticsTechniques() {
return ImmutableList.of();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,22 @@ public class EventDefinitionConfiguration {
@Parameter(value = "event_definition_max_event_limit", validators = PositiveIntegerValidator.class)
private int maxEventLimit = 1000;

@Documentation("""
Enforce strict format validation on tactic/technique IDs assigned to event definitions.
When enabled, tactic IDs must match the pattern TA followed by 4 digits (e.g. TA0004) and
technique IDs must match T followed by 4 digits with an optional 3-digit sub-technique
suffix (e.g. T1021 or T1021.006). Disable as an emergency override if a new ID format is
introduced before Graylog ships an updated validator.
Default: true
""")
@Parameter(value = "event_definition_tactics_techniques_validation_enabled")
private boolean tacticsTechniquesValidationEnabled = true;

public int getMaxEventLimit() {
return maxEventLimit;
}

public boolean isTacticsTechniquesValidationEnabled() {
return tacticsTechniquesValidationEnabled;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ public abstract class EventDefinitionDto implements EventDefinition, ContentPack
public static final String FIELD_PRIORITY = "priority";
public static final String FIELD_ALERT = "alert";
public static final String FIELD_CONFIG = "config";
public static final String FIELD_TACTICS_TECHNIQUES = "tactics_techniques";
public static final int MAX_TACTICS_TECHNIQUES = 64;
public static final String FIELD_TAGS = "tags";
public static final int MAX_TAG_LENGTH = 128;
public static final int MAX_TAGS = 64;
Expand Down Expand Up @@ -172,6 +174,10 @@ public abstract class EventDefinitionDto implements EventDefinition, ContentPack
@JsonProperty(FIELD_EVENT_SUMMARY_TEMPLATE)
public abstract String eventSummaryTemplate();

@Override
@JsonProperty(FIELD_TACTICS_TECHNIQUES)
public abstract ImmutableList<String> tacticsTechniques();

public static Builder builder() {
return Builder.create();
}
Expand Down Expand Up @@ -209,6 +215,21 @@ public ValidationResult validate(@Nullable EventDefinitionDto oldEventDefinition
validation.addError(FIELD_KEY_SPEC, "Event Definition key_spec can only contain fields defined in field_spec.");
}

if (tacticsTechniques().size() > MAX_TACTICS_TECHNIQUES) {
validation.addError(FIELD_TACTICS_TECHNIQUES, "Event Definition cannot have more than " + MAX_TACTICS_TECHNIQUES + " tactics/techniques.");
}
if (eventDefinitionConfiguration.isTacticsTechniquesValidationEnabled()) {
final List<String> invalidIds = tacticsTechniques().stream()
.filter(id -> !TacticsTechniquesNormalizer.isValid(id))
.toList();
if (!invalidIds.isEmpty()) {
final String quoted = invalidIds.stream().map(id -> "\"" + id + "\"").collect(Collectors.joining(", "));
validation.addError(FIELD_TACTICS_TECHNIQUES,
"Invalid tactic/technique ID" + (invalidIds.size() > 1 ? "s" : "") + ": " + quoted
+ ". Expected format: TA0000, T0000, or T0000.000.");
}
}

if (tags().stream().anyMatch(tag -> tag.length() > MAX_TAG_LENGTH)) {
validation.addError(FIELD_TAGS, "Event Definition tags cannot exceed " + MAX_TAG_LENGTH + " characters.");
}
Expand All @@ -234,6 +255,7 @@ public static Builder create() {
.fieldSpec(ImmutableMap.of())
.notifications(ImmutableList.of())
.storage(ImmutableList.of())
.tacticsTechniques(ImmutableList.of())
.tags(ImmutableSet.of())
.state(EventDefinition.State.DISABLED);
}
Expand Down Expand Up @@ -305,12 +327,18 @@ public static Builder create() {
@JsonProperty(FIELD_EVENT_SUMMARY_TEMPLATE)
public abstract Builder eventSummaryTemplate(@Nullable String eventSummaryTemplate);

@JsonProperty(FIELD_TACTICS_TECHNIQUES)
public abstract Builder tacticsTechniques(ImmutableList<String> tacticsTechniques);

abstract ImmutableSet<String> tags();

abstract ImmutableList<String> tacticsTechniques();

abstract EventDefinitionDto autoBuild();

public EventDefinitionDto build() {
tags(TagNormalizer.normalize(tags()));
tacticsTechniques(TacticsTechniquesNormalizer.normalize(tacticsTechniques()));
final EventDefinitionDto dto = autoBuild();
final PersistToStreamsStorageHandler.Config withSystemEventsStream = PersistToStreamsStorageHandler.Config.createWithSystemEventsStream();
if (dto.storage().stream().anyMatch(withSystemEventsStream::equals)) {
Expand Down Expand Up @@ -368,6 +396,7 @@ public EventDefinitionEntity toContentPackEntity(EntityDescriptorIds entityDescr
.tags(tags())
.eventProcedureId(ValueReference.ofNullable(procedureDescriptorId))
.eventSummaryTemplate(ValueReference.ofNullable(eventSummaryTemplate()))
.tacticsTechniques(tacticsTechniques())
.build();
}

Expand Down
Loading
Loading