Skip to content

Commit 603146f

Browse files
danotorreyclaude
authored andcommitted
Add tactics_techniques field on event definitions (#25977)
* Add MITRE categories field to event definitions Adds a single `mitre_categories` keyword array on event definitions and stamps it onto every produced event (mapped at the top level of the events index). Validation accepts tactic, technique, and sub-technique IDs through one combined regex; the 64-value cap is enforced. Frontend types, fixtures, and event-def save telemetry track the new field. Includes a `MitreBackwardsCompatibilityFilter` helper used by the three storage-adapter `MoreSearchAdapter` implementations to OR the legacy `event.fields.sigma_rule_tag_*` filter shape with the new `mitre_categories` shape during the migration window. Scoped for removal alongside the legacy path. Co-Authored-By: Claude Opus 4.7 (1M context) <[EMAIL_ADDRESS_REDACTED]> * Fix FilterPreviewContainer test fixture missing mitre_categories CI yarn tsc surfaced a typed `EventDefinition` literal that hadn't been updated when the field was added. Co-Authored-By: Claude Opus 4.7 (1M context) <[EMAIL_ADDRESS_REDACTED]> * Rename to tactics_techniques + add validator extension point - Rename `mitre_categories` to `tactics_techniques` across DTO, JSON, ES mapping, content-pack entity, FE types, fixtures. - `TacticsTechniquesNormalizer` (mirrors `TagNormalizer`): trim, upper-case, drop empties. Applied in `EventDefinitionDto.Builder.build()`. - `TacticsTechniquesValidator` Guice extension point with OSS no-op default; wired into create/update/validate endpoints. Enterprise overrides to reject IDs not in the Illuminate LUTs. - New "Tactics/Techniques" column on the Event Definitions list. - Editor on the event-def edit form and chip on the list column are pluggable (`eventDefinitions.tacticsTechniquesEditor`, `eventDefinitions.tacticsTechniquesChip`). OSS leaves both empty. - Rename `TagsCell` → `ChipsCell` with generic prop names. Tags and Tactics/Techniques share the collapse / +N overflow UX. - `EventImpl.equals/hashCode/toString` now include the new field. - New TacticsTechniquesNormalizerTest; expanded EventDefinitionDtoTest, EventProcessorEventFactoryTest, EventDefinitionsResourceTest. * Drop tactics_techniques UI surfaces Remove the events list column, event-defs list column, summary view row, and expanded details row. Schema and plugin slot wiring remain. Assisted with Claude Code * Decouple tactics_techniques from UI: optional type + extension point - Make `tactics_techniques` optional on the FE EventDefinition type so consumers that don't set it (older fixtures, ad-hoc instances) still type-check. Mutation hook fills `[]` when sending to the API. - Drop tactics_techniques from the Event Definitions EntityAttribute list; keep it as an extraSearchField so API search still works. - Fix TableElement.getColumnRenderer typing to return the ColumnRenderersByAttribute map instead of a single ColumnRenderer. Make expandedSection / tableCellComponent optional and have the consumer handle their absence. Assisted with Claude Code * Drop TableElement type cleanup from this PR Roll back the getColumnRenderer signature fix and the optional expandedSection/tableCellComponent change. They're a worthwhile cleanup but they create cross-repo coupling — anywhere a downstream plugin had `@ts-expect-error` working around the old signature, the directive becomes unused once the type is corrected. That tripped CI on this PR. Will revisit in a standalone follow-up that touches both repos together. Assisted with Claude Code * Wording; always show TC Improve Coverage button Assisted with Claude Code * Review fixes Assisted with Claude Code * Review fixes: plugin contract location, key namespace, theme, lint Assisted with Claude Code * Fix test: error message uses upper-cased IDs Assisted with Claude Code * Add dedicated plugin slot for tactics/techniques column Replaces use of the shared entityTableElements slot to keep the column scoped to event definitions. The plugin owns the attribute (id/title/sortable), the cell component, and the license gate. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Fix padding-line-between-statements lint Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Add plugin slots for tactics/techniques detail row and summary Two new slots on the event-definition / events UI: - events.components.tacticsTechniquesDetailRow - consumed by the events details expanded section to render a row next to Tags. - eventDefinitions.components.tacticsTechniquesSummary - consumed by the event definition summary view to render a row next to Tags. Server defines the slot contracts; the security plugin contributes the components, gates them on the security license, and owns the dual-read between the new tactics_techniques field and the legacy sigma_rule_tag_* shape. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 5936055 commit 603146f

42 files changed

Lines changed: 678 additions & 75 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

graylog-storage-elasticsearch7/src/main/java/org/graylog/storage/elasticsearch7/MoreSearchAdapterES7.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import jakarta.inject.Named;
2323
import org.graylog.events.event.EventDto;
2424
import org.graylog.events.processor.EventProcessorException;
25+
import org.graylog.events.search.MitreBackwardsCompatibilityFilter;
2526
import org.graylog.events.search.MoreSearch;
2627
import org.graylog.events.search.MoreSearchAdapter;
2728
import org.graylog.events.search.SourceStreamFilter;
@@ -228,7 +229,17 @@ private QueryBuilder createQuery(String queryString, TimeRange timerange, Set<St
228229
.filter(termsQuery(EventDto.FIELD_STREAMS, eventStreams))
229230
.filter(requireNonNull(TimeRangeQueryFactory.create(timerange)));
230231

231-
extraFilters.forEach((field, values) -> {
232+
final BoolQueryBuilder mitreOr = boolQuery().minimumShouldMatch(1);
233+
if (MitreBackwardsCompatibilityFilter.emitShouldClauses(extraFilters,
234+
(k, v) -> mitreOr.should(buildExtraFilter(k, v)))) {
235+
filter.filter(mitreOr);
236+
}
237+
238+
extraFilters.entrySet().stream()
239+
.filter(e -> !MitreBackwardsCompatibilityFilter.isMitreKey(e.getKey()))
240+
.forEach(e -> {
241+
final var field = e.getKey();
242+
final var values = e.getValue();
232243
values.stream()
233244
.filter(MoreSearchAdapter::isRangeValue)
234245
.map(value -> buildExtraFilter(field, value))

graylog-storage-opensearch2/src/main/java/org/graylog/storage/opensearch2/MoreSearchAdapterOS2.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import jakarta.inject.Named;
2323
import org.graylog.events.event.EventDto;
2424
import org.graylog.events.processor.EventProcessorException;
25+
import org.graylog.events.search.MitreBackwardsCompatibilityFilter;
2526
import org.graylog.events.search.MoreSearch;
2627
import org.graylog.events.search.MoreSearchAdapter;
2728
import org.graylog.events.search.SourceStreamFilter;
@@ -229,7 +230,17 @@ private QueryBuilder createQuery(String queryString, TimeRange timerange, Set<St
229230
.filter(termsQuery(EventDto.FIELD_STREAMS, eventStreams))
230231
.filter(requireNonNull(TimeRangeQueryFactory.create(timerange)));
231232

232-
extraFilters.forEach((field, values) -> {
233+
final BoolQueryBuilder mitreOr = boolQuery().minimumShouldMatch(1);
234+
if (MitreBackwardsCompatibilityFilter.emitShouldClauses(extraFilters,
235+
(k, v) -> mitreOr.should(buildExtraFilter(k, v)))) {
236+
filter.filter(mitreOr);
237+
}
238+
239+
extraFilters.entrySet().stream()
240+
.filter(e -> !MitreBackwardsCompatibilityFilter.isMitreKey(e.getKey()))
241+
.forEach(e -> {
242+
final var field = e.getKey();
243+
final var values = e.getValue();
233244
values.stream()
234245
.filter(MoreSearchAdapter::isRangeValue)
235246
.map(value -> buildExtraFilter(field, value))

graylog-storage-opensearch3/src/main/java/org/graylog/storage/opensearch3/MoreSearchAdapterOS.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import jakarta.inject.Named;
2222
import org.graylog.events.event.EventDto;
2323
import org.graylog.events.processor.EventProcessorException;
24+
import org.graylog.events.search.MitreBackwardsCompatibilityFilter;
2425
import org.graylog.events.search.MoreSearch;
2526
import org.graylog.events.search.MoreSearchAdapter;
2627
import org.graylog.events.search.SourceStreamFilter;
@@ -164,7 +165,17 @@ private Query createQuery(String queryString, TimeRange timerange, Set<String> e
164165
boolQuery.filter(timerangeQuery(timerange));
165166

166167

167-
extraFilters.forEach((field, values) -> {
168+
final BoolQuery.Builder mitreOr = BoolQuery.builder().minimumShouldMatch("1");
169+
if (MitreBackwardsCompatibilityFilter.emitShouldClauses(extraFilters,
170+
(k, v) -> mitreOr.should(buildExtraFilter(k, v)))) {
171+
boolQuery.filter(Query.of(b -> b.bool(mitreOr.build())));
172+
}
173+
174+
extraFilters.entrySet().stream()
175+
.filter(e -> !MitreBackwardsCompatibilityFilter.isMitreKey(e.getKey()))
176+
.forEach(e -> {
177+
final var field = e.getKey();
178+
final var values = e.getValue();
168179
values.stream()
169180
.filter(MoreSearchAdapter::isRangeValue)
170181
.map(value -> buildExtraFilter(field, value))

graylog2-server/src/main/java/org/graylog/events/EventsModule.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import org.graylog.events.processor.EventProcessorExecutionJob;
5353
import org.graylog.events.processor.EventProcessorExecutionMetrics;
5454
import org.graylog.events.processor.EventResolver;
55+
import org.graylog.events.processor.TacticsTechniquesValidator;
5556
import org.graylog.events.processor.aggregation.AggregationEventProcessor;
5657
import org.graylog.events.processor.aggregation.AggregationEventProcessorConfig;
5758
import org.graylog.events.processor.aggregation.AggregationEventProcessorParameters;
@@ -99,6 +100,9 @@ protected void configure() {
99100
OptionalBinder.newOptionalBinder(binder(), EventResolver.class)
100101
.setDefault().to(DefaultEventResolver.class);
101102

103+
OptionalBinder.newOptionalBinder(binder(), TacticsTechniquesValidator.class)
104+
.setDefault().to(TacticsTechniquesValidator.NoOp.class);
105+
102106
addSystemRestResource(AvailableEntityTypesResource.class);
103107
addSystemRestResource(EventDefinitionsResource.class);
104108
addSystemRestResource(EventNotificationsResource.class);

graylog2-server/src/main/java/org/graylog/events/contentpack/entities/EventDefinitionEntity.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public abstract class EventDefinitionEntity extends ScopedContentPackEntity impl
6767
private static final String MATCHED_AT = "matched_at";
6868
private static final String FIELD_EVENT_PROCEDURE = "event_procedure";
6969
private static final String FIELD_EVENT_SUMMARY_TEMPLATE = "event_summary_template";
70+
private static final String FIELD_TACTICS_TECHNIQUES = EventDefinitionDto.FIELD_TACTICS_TECHNIQUES;
7071

7172
@JsonProperty(FIELD_TITLE)
7273
public abstract ValueReference title();
@@ -124,6 +125,9 @@ public abstract class EventDefinitionEntity extends ScopedContentPackEntity impl
124125
@JsonProperty(FIELD_EVENT_SUMMARY_TEMPLATE)
125126
public abstract ValueReference eventSummaryTemplate();
126127

128+
@JsonProperty(FIELD_TACTICS_TECHNIQUES)
129+
public abstract ImmutableList<String> tacticsTechniques();
130+
127131
public static Builder builder() {
128132
return Builder.create();
129133
}
@@ -136,6 +140,7 @@ public static abstract class Builder extends ScopedContentPackEntity.AbstractBui
136140
public static Builder create() {
137141
return new AutoValue_EventDefinitionEntity.Builder()
138142
.isScheduled(ValueReference.of(true))
143+
.tacticsTechniques(ImmutableList.of())
139144
.tags(ImmutableSet.of());
140145
}
141146

@@ -190,6 +195,9 @@ public static Builder create() {
190195
@JsonProperty(FIELD_EVENT_SUMMARY_TEMPLATE)
191196
public abstract Builder eventSummaryTemplate(ValueReference eventSummaryTemplate);
192197

198+
@JsonProperty(FIELD_TACTICS_TECHNIQUES)
199+
public abstract Builder tacticsTechniques(ImmutableList<String> tacticsTechniques);
200+
193201
public abstract EventDefinitionEntity build();
194202
}
195203

@@ -230,6 +238,7 @@ public EventDefinitionDto toNativeEntity(Map<String, ValueReference> parameters,
230238
.tags(tags())
231239
.eventProcedureId(procedureId)
232240
.eventSummaryTemplate(eventSummaryTemplate() != null ? eventSummaryTemplate().asString(parameters) : null)
241+
.tacticsTechniques(tacticsTechniques())
233242
.build();
234243
}
235244

graylog2-server/src/main/java/org/graylog/events/event/Event.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ public interface Event extends Indexable {
113113

114114
void addAssociatedAssets(Set<String> associatedAssets);
115115

116+
List<String> getTacticsTechniques();
117+
118+
void setTacticsTechniques(List<String> tacticsTechniques);
119+
116120
Set<String> getTags();
117121

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

140145
return event;

graylog2-server/src/main/java/org/graylog/events/event/EventDto.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.fasterxml.jackson.annotation.JsonProperty;
2222
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
2323
import com.google.auto.value.AutoValue;
24+
import com.google.common.collect.ImmutableList;
2425
import com.google.common.collect.ImmutableMap;
2526
import com.google.common.collect.ImmutableSet;
2627
import org.joda.time.DateTime;
@@ -57,6 +58,7 @@ public abstract class EventDto {
5758
public static final String FIELD_GROUP_BY_FIELDS = "group_by_fields";
5859
public static final String FIELD_AGGREGATION_CONDITIONS = "aggregation_conditions";
5960
public static final String FIELD_REPLAY_INFO = "replay_info";
61+
public static final String FIELD_TACTICS_TECHNIQUES = "tactics_techniques";
6062

6163
@JsonProperty(FIELD_ID)
6264
public abstract String id();
@@ -130,6 +132,9 @@ public abstract class EventDto {
130132
@JsonProperty(FIELD_REPLAY_INFO)
131133
public abstract Optional<EventReplayInfo> replayInfo();
132134

135+
@JsonProperty(FIELD_TACTICS_TECHNIQUES)
136+
public abstract List<String> tacticsTechniques();
137+
133138
public static Builder builder() {
134139
return Builder.create();
135140
}
@@ -149,6 +154,7 @@ public static Builder create() {
149154
.aggregationConditions(ImmutableMap.of())
150155
.scores(ImmutableMap.of())
151156
.associatedAssets(ImmutableSet.of())
157+
.tacticsTechniques(ImmutableList.of())
152158
.tags(ImmutableSet.of());
153159
}
154160

@@ -225,6 +231,9 @@ public static Builder create() {
225231
@JsonProperty(FIELD_REPLAY_INFO)
226232
public abstract Builder replayInfo(@Nullable EventReplayInfo replayInfo);
227233

234+
@JsonProperty(FIELD_TACTICS_TECHNIQUES)
235+
public abstract Builder tacticsTechniques(List<String> tacticsTechniques);
236+
228237
public abstract EventDto build();
229238
}
230239
}

graylog2-server/src/main/java/org/graylog/events/event/EventImpl.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public class EventImpl implements Event {
6767
private final Set<String> associatedAssets = new HashSet<>();
6868
private final Set<String> tags = new HashSet<>();
6969
private EventReplayInfo replayInfo;
70+
private List<String> tacticsTechniques = ImmutableList.of();
7071

7172
EventImpl(String eventId,
7273
DateTime eventTimestamp,
@@ -339,6 +340,16 @@ public void setReplayInfo(EventReplayInfo replayInfo) {
339340
this.replayInfo = replayInfo;
340341
}
341342

343+
@Override
344+
public List<String> getTacticsTechniques() {
345+
return tacticsTechniques;
346+
}
347+
348+
@Override
349+
public void setTacticsTechniques(List<String> tacticsTechniques) {
350+
this.tacticsTechniques = tacticsTechniques == null ? ImmutableList.of() : ImmutableList.copyOf(tacticsTechniques);
351+
}
352+
342353
@Override
343354
public EventDto toDto() {
344355
final Map<String, String> fields = this.fields.entrySet()
@@ -375,6 +386,7 @@ public EventDto toDto() {
375386
.groupByFields(ImmutableMap.copyOf(groupByFields))
376387
.aggregationConditions(ImmutableMap.copyOf(aggregationConditions))
377388
.replayInfo(getReplayInfo())
389+
.tacticsTechniques(getTacticsTechniques())
378390
.build();
379391
}
380392

@@ -431,6 +443,7 @@ public boolean equals(Object o) {
431443
Objects.equals(scores, event.scores) &&
432444
Objects.equals(associatedAssets, event.associatedAssets) &&
433445
Objects.equals(tags, event.tags) &&
446+
Objects.equals(tacticsTechniques, event.tacticsTechniques) &&
434447
Objects.equals(replayInfo, event.replayInfo);
435448
}
436449

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

445458
@Override
@@ -467,6 +480,7 @@ public String toString() {
467480
.add("scores", scores)
468481
.add("associatedAssets", associatedAssets)
469482
.add("tags", tags)
483+
.add("tacticsTechniques", tacticsTechniques)
470484
.toString();
471485
}
472486

graylog2-server/src/main/java/org/graylog/events/event/EventProcessorEventFactory.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ public Event createEvent(EventDefinition eventDefinition, DateTime eventTime, St
6565
eventDefinition.priority(),
6666
eventDefinition.alert()
6767
);
68+
event.setTacticsTechniques(eventDefinition.tacticsTechniques());
6869
event.setTags(eventDefinition.tags());
6970
return event;
7071
}

graylog2-server/src/main/java/org/graylog/events/processor/EventDefinition.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,8 @@ default String eventProcedureId() {
103103
default String eventSummaryTemplate() {
104104
return null;
105105
}
106+
107+
default ImmutableList<String> tacticsTechniques() {
108+
return ImmutableList.of();
109+
}
106110
}

0 commit comments

Comments
 (0)