From adc6348d53331d1e89f166d0afa0af9d49f96864 Mon Sep 17 00:00:00 2001 From: Ezequiel Lopez Date: Wed, 3 Jun 2026 08:25:04 -0400 Subject: [PATCH 1/3] feat(14351): New sigma summary and condition form --- .../EventConditionForm.tsx | 49 ++++++------ .../EventDefinitionActions.tsx | 20 +++-- .../pages/ViewEventDefinitionPage.test.tsx | 31 +++++--- .../src/pages/ViewEventDefinitionPage.tsx | 77 +++++++++---------- 4 files changed, 93 insertions(+), 84 deletions(-) diff --git a/graylog2-web-interface/src/components/event-definitions/event-definition-form/EventConditionForm.tsx b/graylog2-web-interface/src/components/event-definitions/event-definition-form/EventConditionForm.tsx index 283b438528f0..ab15a63b8a1e 100644 --- a/graylog2-web-interface/src/components/event-definitions/event-definition-form/EventConditionForm.tsx +++ b/graylog2-web-interface/src/components/event-definitions/event-definition-form/EventConditionForm.tsx @@ -76,6 +76,11 @@ const EventConditionForm = ({ const eventDefinitionTypes = usePluginEntities('eventDefinitionTypes'); const filteredDefinitionTypes = eventDefinitionTypes.filter((type) => type.useCondition()); + const currentConditionPlugin = useMemo( + () => eventDefinitionTypes.find((edt) => edt.type === eventDefinition.config.type), + [eventDefinitionTypes, eventDefinition.config.type], + ); + const getConditionPlugin = useCallback( (type: string) => { if (type === undefined) { @@ -109,10 +114,15 @@ const EventConditionForm = ({ [filteredDefinitionTypes], ); - const formattedEventDefinitionTypes = useMemo( - () => sortedEventDefinitionTypes.map((type) => ({ label: type.displayName, value: type.type })), - [sortedEventDefinitionTypes], - ); + const formattedEventDefinitionTypes = useMemo(() => { + const options = sortedEventDefinitionTypes.map((type) => ({ label: type.displayName, value: type.type })); + + if (currentConditionPlugin && !options.some((o) => o.value === currentConditionPlugin.type)) { + options.push({ label: currentConditionPlugin.displayName, value: currentConditionPlugin.type }); + } + + return options; + }, [sortedEventDefinitionTypes, currentConditionPlugin]); const handleEventDefinitionTypeChange = (nextType: string) => { sendTelemetry(TELEMETRY_EVENT_TYPE.EVENTDEFINITION_CONDITION.TYPE_SELECTED, { @@ -141,15 +151,11 @@ const EventConditionForm = ({ [action, eventDefinition.config.type], ); - const eventDefinitionType = useMemo( - () => getConditionPlugin(eventDefinition.config.type), - [eventDefinition.config.type, getConditionPlugin], - ); const isSystemEventDefinition = eventDefinition.config.type === SYSTEM_EVENT_DEFINITION_TYPE; const canEditCondition = canEdit && !isSystemEventDefinition; - const eventDefinitionTypeComponent = eventDefinitionType?.formComponent - ? React.createElement(eventDefinitionType.formComponent, { + const eventDefinitionTypeComponent = currentConditionPlugin?.formComponent + ? React.createElement(currentConditionPlugin.formComponent, { action, entityTypes, currentUser, @@ -192,20 +198,17 @@ const EventConditionForm = ({ {canEditCondition && !disabledSelect && ( + + + + + + )} + + {canEditCondition && eventDefinitionTypeComponent && ( <> - - - - - - - - {eventDefinitionTypeComponent && ( - <> -
- {eventDefinitionTypeComponent} - - )} +
+ {eventDefinitionTypeComponent} )} diff --git a/graylog2-web-interface/src/components/event-definitions/event-definitions/EventDefinitionActions.tsx b/graylog2-web-interface/src/components/event-definitions/event-definitions/EventDefinitionActions.tsx index 439bed246b2f..dc47bc7b1fa5 100644 --- a/graylog2-web-interface/src/components/event-definitions/event-definitions/EventDefinitionActions.tsx +++ b/graylog2-web-interface/src/components/event-definitions/event-definitions/EventDefinitionActions.tsx @@ -99,10 +99,6 @@ const EventDefinitionActions = ({ eventDefinition }: Props) => { return 'System Event Definition cannot be deleted'; } - if (isSigmaEventDefinition(eventDefinition)) { - return 'Sigma Rules must be deleted from the Sigma Rules page'; - } - return undefined; }; @@ -258,11 +254,13 @@ const EventDefinitionActions = ({ eventDefinition }: Props) => { bsSize="xsmall" /> - - - Edit - - + {(!isSigmaEventDefinition(eventDefinition) || showActions()) && ( + + + Edit + + + )} {!isSystemEventDefinition(eventDefinition) && !isSigmaEventDefinition(eventDefinition) && ( handleAction(DIALOG_TYPES.COPY, eventDefinition)}>Duplicate @@ -287,10 +285,10 @@ const EventDefinitionActions = ({ eventDefinition }: Props) => { handleAction(DIALOG_TYPES.DELETE, eventDefinition) } diff --git a/graylog2-web-interface/src/pages/ViewEventDefinitionPage.test.tsx b/graylog2-web-interface/src/pages/ViewEventDefinitionPage.test.tsx index 97584356d383..c710751c56f0 100644 --- a/graylog2-web-interface/src/pages/ViewEventDefinitionPage.test.tsx +++ b/graylog2-web-interface/src/pages/ViewEventDefinitionPage.test.tsx @@ -23,13 +23,15 @@ import type { Permission } from 'graylog-web-plugin/plugin'; import Routes from 'routing/Routes'; import usePluginEntities from 'hooks/usePluginEntities'; -import mockAction from 'helpers/mocking/MockAction'; import MockStore from 'helpers/mocking/StoreMock'; +import mockAction from 'helpers/mocking/MockAction'; import mockComponent from 'helpers/mocking/MockComponent'; import { simpleEventDefinition as mockEventDefinition } from 'fixtures/eventDefinition'; import { adminUser } from 'fixtures/users'; import { asMock } from 'helpers/mocking'; import useCurrentUser from 'hooks/useCurrentUser'; +import { useGetEventDefinition } from 'components/event-definitions/hooks/useEventDefinitions'; +import useGetPermissionsByScope from 'hooks/useScopePermissions'; import ViewEventDefinitionPage from './ViewEventDefinitionPage'; @@ -41,18 +43,12 @@ jest.mock('react-router-dom', () => ({ })); jest.mock('hooks/useCurrentUser'); +jest.mock('components/event-definitions/hooks/useEventDefinitions'); +jest.mock('hooks/useScopePermissions'); jest.mock('stores/event-definitions/EventDefinitionsStore', () => ({ EventDefinitionsActions: { - get: mockAction( - jest.fn(() => - Promise.resolve({ - event_definition: mockEventDefinition, - context: { scheduler: { is_scheduled: true } }, - is_mutable: true, - }), - ), - ), + copy: mockAction(jest.fn(() => Promise.resolve({ id: 'new-id' }))), }, })); @@ -71,6 +67,19 @@ jest.mock('hooks/usePluginEntities'); describe('', () => { beforeEach(() => { asMock(useCurrentUser).mockReturnValue(defaultUser); + asMock(useGetEventDefinition).mockReturnValue({ + data: { + eventDefinition: mockEventDefinition, + context: { scheduler: { is_scheduled: true } }, + is_mutable: true, + }, + isFetching: false, + }); + asMock(useGetPermissionsByScope).mockReturnValue({ + loadingScopePermissions: false, + scopePermissions: { is_mutable: true, is_deletable: true }, + checkPermissions: () => true, + }); asMock(usePluginEntities).mockImplementation( (entityKey) => ({ @@ -90,7 +99,7 @@ describe('', () => { it('should display the event definition page', async () => { render(); - await screen.findByText(/View Event Definition/); + await screen.findByText(/View "Event Definition 1" Event Definition/); }); it('should display event details when permitted', async () => { diff --git a/graylog2-web-interface/src/pages/ViewEventDefinitionPage.tsx b/graylog2-web-interface/src/pages/ViewEventDefinitionPage.tsx index 8cc534522461..830351cc1922 100644 --- a/graylog2-web-interface/src/pages/ViewEventDefinitionPage.tsx +++ b/graylog2-web-interface/src/pages/ViewEventDefinitionPage.tsx @@ -15,8 +15,9 @@ * . */ import * as React from 'react'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; +import { useQueryClient } from '@tanstack/react-query'; import { useStore } from 'stores/connect'; import { ButtonToolbar, Col, Row, Button } from 'components/bootstrap'; @@ -24,7 +25,6 @@ import Routes from 'routing/Routes'; import DocsHelper from 'util/DocsHelper'; import { DocumentTitle, IfPermitted, PageHeader, Spinner, ConfirmDialog } from 'components/common'; import useCurrentUser from 'hooks/useCurrentUser'; -import { isPermitted } from 'util/PermissionsMixin'; import EventDefinitionSummary from 'components/event-definitions/event-definition-form/EventDefinitionSummary'; import { EventDefinitionsActions } from 'stores/event-definitions/EventDefinitionsStore'; import { EventNotificationsActions, EventNotificationsStore } from 'stores/event-notifications/EventNotificationsStore'; @@ -32,9 +32,11 @@ import EventsPageNavigation from 'components/events/EventsPageNavigation'; import useHistory from 'routing/useHistory'; import useSendTelemetry from 'logic/telemetry/useSendTelemetry'; import type { EventDefinition } from 'components/event-definitions/event-definitions-types'; -import { isSystemEventDefinition } from 'components/event-definitions/event-definitions-types'; +import { isSystemEventDefinition, isSigmaEventDefinition } from 'components/event-definitions/event-definitions-types'; import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants'; import usePluginEntities from 'hooks/usePluginEntities'; +import { useGetEventDefinition } from 'components/event-definitions/hooks/useEventDefinitions'; +import useGetPermissionsByScope from 'hooks/useScopePermissions'; type SigmaEventDefinitionConfig = EventDefinition['config'] & { sigma_rule_id: string; @@ -43,14 +45,12 @@ type SigmaEventDefinitionConfig = EventDefinition['config'] & { const ViewEventDefinitionPage = () => { const params = useParams<{ definitionId?: string }>(); const currentUser = useCurrentUser(); - const [eventDefinition, setEventDefinition] = useState(); const [showDialog, setShowDialog] = useState(false); const { all: notifications } = useStore(EventNotificationsStore); const history = useHistory(); const sendTelemetry = useSendTelemetry(); const navigate = useNavigate(); const [showSigmaModal, setShowSigmaModal] = useState(false); - const [refetch, setRefetch] = useState(true); const pluggableSigmaModal = usePluginEntities('eventDefinitions.components.editSigmaModal').find( (entity: { key: string }) => entity.key === 'coreSigmaModal', @@ -60,35 +60,32 @@ const ViewEventDefinitionPage = () => { ? (pluggableSigmaModal.component as React.FC<{ ruleId: string; onCancel: () => void; onConfirm: () => void }>) : null; - useEffect(() => { - if ( - currentUser && - isPermitted(currentUser.permissions, `eventdefinitions:read:${params.definitionId}`) && - refetch - ) { - EventDefinitionsActions.get(params.definitionId).then( - (response: any) => { - const eventDefinitionResp = response.event_definition; - - // Inject an internal "_is_scheduled" field to indicate if the event definition should be scheduled in the - // backend. This field will be removed in the event definitions store before sending an event definition - // back to the server. - eventDefinitionResp.config._is_scheduled = response.context.scheduler.is_scheduled; - setEventDefinition(eventDefinitionResp); - }, - (error) => { - if (error.status === 404) { - history.push(Routes.ALERTS.DEFINITIONS.LIST); - } - }, - ); + const queryClient = useQueryClient(); + const { data, isFetching } = useGetEventDefinition(params.definitionId); + + const eventDefinition = useMemo(() => { + if (!data?.eventDefinition) return null; - EventNotificationsActions.listAll(); + return { + ...data.eventDefinition, + config: { + ...data.eventDefinition.config, + _is_scheduled: data.context?.scheduler?.is_scheduled, + }, + }; + }, [data]); - // eslint-disable-next-line react-hooks/set-state-in-effect - setRefetch(false); + const { scopePermissions } = useGetPermissionsByScope(eventDefinition); + + useEffect(() => { + EventNotificationsActions.listAll(); + }, []); + + useEffect(() => { + if (!isFetching && !eventDefinition) { + history.push(Routes.ALERTS.DEFINITIONS.LIST); } - }, [currentUser, history, params, refetch]); + }, [eventDefinition, history, isFetching]); const handleDuplicateEvent = () => { sendTelemetry(TELEMETRY_EVENT_TYPE.EVENTDEFINITION_DUPLICATED, { @@ -109,11 +106,11 @@ const ViewEventDefinitionPage = () => { }; const onSigmaModalClose = () => { - setRefetch(true); + queryClient.invalidateQueries({ queryKey: ['get-event-definition', params.definitionId] }); setShowSigmaModal(false); }; - if (!eventDefinition || !notifications) { + if (isFetching || !eventDefinition || !notifications) { return ( @@ -133,12 +130,14 @@ const ViewEventDefinitionPage = () => { title={`View "${eventDefinition.title}" Event Definition`} actions={ - - - - {!isSystemEventDefinition(eventDefinition) && ( + {(!isSigmaEventDefinition(eventDefinition) || scopePermissions?.is_mutable) && ( + + + + )} + {!isSystemEventDefinition(eventDefinition) && !isSigmaEventDefinition(eventDefinition) && ( From 083b6b3d582e9ea9cc8f2c2dccdc4a64c02643eb Mon Sep 17 00:00:00 2001 From: Ezequiel Lopez Date: Fri, 5 Jun 2026 13:05:17 -0400 Subject: [PATCH 2/3] feat(14351): Edit works with sigma event definitions --- .../EventDefinitionActions.tsx | 34 +------------------ .../src/pages/ViewEventDefinitionPage.tsx | 8 +---- 2 files changed, 2 insertions(+), 40 deletions(-) diff --git a/graylog2-web-interface/src/components/event-definitions/event-definitions/EventDefinitionActions.tsx b/graylog2-web-interface/src/components/event-definitions/event-definitions/EventDefinitionActions.tsx index dc47bc7b1fa5..65be89320d60 100644 --- a/graylog2-web-interface/src/components/event-definitions/event-definitions/EventDefinitionActions.tsx +++ b/graylog2-web-interface/src/components/event-definitions/event-definitions/EventDefinitionActions.tsx @@ -31,7 +31,6 @@ import useLocation from 'routing/useLocation'; import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants'; import useSelectedEntities from 'components/common/EntityDataTable/hooks/useSelectedEntities'; import { MoreActions } from 'components/common/EntityDataTable'; -import usePluginEntities from 'hooks/usePluginEntities'; import { useTableFetchContext } from 'components/common/PaginatedEntityTable'; import usePluggableEntitySharedActions from 'hooks/usePluggableEntitySharedActions'; @@ -42,10 +41,6 @@ import { isSigmaEventDefinition, } from '../event-definitions-types'; -type SigmaEventDefinitionConfig = EventDefinition['config'] & { - sigma_rule_id: string; -}; - type Props = { eventDefinition: EventDefinition; }; @@ -84,7 +79,6 @@ const EventDefinitionActions = ({ eventDefinition }: Props) => { const [showDialog, setShowDialog] = useState(false); const [dialogType, setDialogType] = useState(null); const [showEntityShareModal, setShowEntityShareModal] = useState(false); - const [showSigmaModal, setShowSigmaModal] = useState(false); const { pathname } = useLocation(); const sendTelemetry = useSendTelemetry(); const navigate = useNavigate(); @@ -102,14 +96,6 @@ const EventDefinitionActions = ({ eventDefinition }: Props) => { return undefined; }; - const pluggableSigmaModal = usePluginEntities('eventDefinitions.components.editSigmaModal').find( - (entity: { key: string }) => entity.key === 'coreSigmaModal', - ); - - const CoreSigmaModal = pluggableSigmaModal - ? (pluggableSigmaModal.component as React.FC<{ ruleId: string; onCancel: () => void; onConfirm: () => void }>) - : null; - const updateState = ({ show, type, definition }) => { setShowDialog(show); setDialogType(type); @@ -229,18 +215,7 @@ const EventDefinitionActions = ({ eventDefinition }: Props) => { } }; - const onEditEventDefinition = () => { - if (isSigmaEventDefinition(eventDefinition)) { - setShowSigmaModal(true); - } else { - navigate(Routes.ALERTS.DEFINITIONS.edit(eventDefinition.id)); - } - }; - - const onSigmaModalClose = () => { - refetchEventDefinitions(); - setShowSigmaModal(false); - }; + const onEditEventDefinition = () => navigate(Routes.ALERTS.DEFINITIONS.edit(eventDefinition.id)); const isEnabled = eventDefinition?.state === 'ENABLED'; @@ -333,13 +308,6 @@ const EventDefinitionActions = ({ eventDefinition }: Props) => { onClose={() => setShowEntityShareModal(false)} /> )} - {showSigmaModal && CoreSigmaModal && ( - - )} {pluggableActionModals} ); diff --git a/graylog2-web-interface/src/pages/ViewEventDefinitionPage.tsx b/graylog2-web-interface/src/pages/ViewEventDefinitionPage.tsx index 830351cc1922..d9fa8fd64d0a 100644 --- a/graylog2-web-interface/src/pages/ViewEventDefinitionPage.tsx +++ b/graylog2-web-interface/src/pages/ViewEventDefinitionPage.tsx @@ -97,13 +97,7 @@ const ViewEventDefinitionPage = () => { }); }; - const onEditEventDefinition = () => { - if (eventDefinition.config.type === 'sigma-v1') { - setShowSigmaModal(true); - } else { - navigate(Routes.ALERTS.DEFINITIONS.edit(params.definitionId)); - } - }; + const onEditEventDefinition = () => navigate(Routes.ALERTS.DEFINITIONS.edit(params.definitionId)); const onSigmaModalClose = () => { queryClient.invalidateQueries({ queryKey: ['get-event-definition', params.definitionId] }); From 991633acb78cd7bd2f94338355b6c3a021c44a26 Mon Sep 17 00:00:00 2001 From: Ezequiel Lopez Date: Fri, 5 Jun 2026 16:06:47 -0400 Subject: [PATCH 3/3] feat(14351): Allow edit sigma events --- .../EventDetailsForm.tsx | 5 +--- .../FilterAggregationSummary.tsx | 28 ++++++++++++++----- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/graylog2-web-interface/src/components/event-definitions/event-definition-form/EventDetailsForm.tsx b/graylog2-web-interface/src/components/event-definitions/event-definition-form/EventDetailsForm.tsx index 6b45527d6fd1..5c783021351a 100644 --- a/graylog2-web-interface/src/components/event-definitions/event-definition-form/EventDetailsForm.tsx +++ b/graylog2-web-interface/src/components/event-definitions/event-definition-form/EventDetailsForm.tsx @@ -95,10 +95,7 @@ const EventDetailsForm = ({ eventDefinition, eventDefinitionEventProcedure, vali data: { valid: validSecurityLicense }, } = usePluggableLicenseCheck('/license/security'); - const readOnly = useMemo( - () => !canEdit || isSystemEventDefinition(eventDefinition) || eventDefinition.config.type === 'sigma-v1', - [canEdit, eventDefinition], - ); + const readOnly = useMemo(() => !canEdit || isSystemEventDefinition(eventDefinition), [canEdit, eventDefinition]); const showEventProcedureSummary = useMemo( () => !!eventDefinitionEventProcedure && !showAddEventProcedureForm && validSecurityLicense, [eventDefinitionEventProcedure, showAddEventProcedureForm, validSecurityLicense], diff --git a/graylog2-web-interface/src/components/event-definitions/event-definition-types/FilterAggregationSummary.tsx b/graylog2-web-interface/src/components/event-definitions/event-definition-types/FilterAggregationSummary.tsx index 43926415a768..5558e893fd2a 100644 --- a/graylog2-web-interface/src/components/event-definitions/event-definition-types/FilterAggregationSummary.tsx +++ b/graylog2-web-interface/src/components/event-definitions/event-definition-types/FilterAggregationSummary.tsx @@ -18,10 +18,12 @@ import * as React from 'react'; import { useContext } from 'react'; import isEmpty from 'lodash/isEmpty'; import upperFirst from 'lodash/upperFirst'; +import styled from 'styled-components'; import { describeExpression } from 'util/CronUtils'; import { Link } from 'components/common'; import { Alert } from 'components/bootstrap'; +import { DataWell } from 'components/lookup-tables/layout-componets'; import { extractDurationAndUnit } from 'components/common/TimeUnitInput'; import { isPermitted } from 'util/PermissionsMixin'; import { naturalSortIgnoreCase } from 'util/SortUtils'; @@ -40,6 +42,16 @@ import styles from './FilterAggregationSummary.css'; import LinkToReplaySearch from '../replay-search/LinkToReplaySearch'; +const StyledDataWell = styled(DataWell)` + line-height: 1.8; + white-space: pre; + font-family: monospace; + font-size: medium; + color: ${({ theme }) => (theme.mode === 'light' ? 'darkslateblue' : 'lightsteelblue')}; + overflow: auto; + text-wrap: auto; +`; + const StreamOrId = ({ streamOrId }: { streamOrId: Stream | string }) => { if (typeof streamOrId === 'string') { return ( @@ -89,7 +101,7 @@ type Props = { }; const SearchFilters = ({ filters }: { filters: EventDefinition['config']['filters'] }) => { - if (!filters || filters.length === 0) { + if (!filters || filters?.length === 0) { return
No filters configured
; } @@ -112,12 +124,12 @@ type StreamsProps = { }; const Streams = ({ streams, streamIds, streamIdsWithMissingPermission }: StreamsProps) => { - if ((!streamIds || streamIds.length === 0) && streamIdsWithMissingPermission.length <= 0) { + if ((!streamIds || streamIds?.length === 0) && streamIdsWithMissingPermission?.length <= 0) { return <>No Streams selected, searches in all Streams; } const warning = - streamIdsWithMissingPermission.length > 0 ? ( + streamIdsWithMissingPermission?.length > 0 ? ( Missing Stream Permissions for:
@@ -178,7 +190,7 @@ const FilterAggregationSummary = ({ config, currentUser, definitionId = undefine }; const renderStreamCategories = () => { - if (!streamCategories || streamCategories.length === 0) return null; + if (!streamCategories || streamCategories?.length === 0) return null; const renderedCategories = streamCategories.map((s) => ); @@ -195,8 +207,10 @@ const FilterAggregationSummary = ({ config, currentUser, definitionId = undefine
Type
{upperFirst(conditionType)}
Search Query
-
{query || '*'}
- {queryParameters.length > 0 && } +
+ {query || '*'} +
+ {queryParameters?.length > 0 && }
Search Filters
Streams
@@ -242,7 +256,7 @@ const FilterAggregationSummary = ({ config, currentUser, definitionId = undefine {conditionType === 'aggregation' && ( <>
Group by Field(s)
-
{groupBy && groupBy.length > 0 ? groupBy.join(', ') : 'No Group by configured'}
+
{groupBy && groupBy?.length > 0 ? groupBy.join(', ') : 'No Group by configured'}
Create Events if
{validationResults.isValid ? (