diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx index f695c33a374471..4e850a0a11957f 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx @@ -45,7 +45,7 @@ export interface UtilityBarActionProps extends LinkIconProps { } export const UtilityBarAction = React.memo( - ({ children, color, href, iconSide, iconSize, iconType, onClick, popoverContent }) => ( + ({ children, color, disabled, href, iconSide, iconSize, iconType, onClick, popoverContent }) => ( {popoverContent ? ( ( ) : ( ( - ({ children, color, href, iconSide = 'left', iconSize = 's', iconType, onClick }) => ( - + ({ children, color, disabled, href, iconSide = 'left', iconSize = 's', iconType, onClick }) => ( + {children} diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts index b56fae320df1a0..2228ee4262400d 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts @@ -236,6 +236,7 @@ describe('helpers', () => { description: '', deletedEventIds: [], eventIdToNoteIds: {}, + eventType: 'raw', filters: [], highlightedDropAndProviderId: '', historyIds: [], @@ -329,6 +330,7 @@ describe('helpers', () => { description: '', deletedEventIds: [], eventIdToNoteIds: {}, + eventType: 'raw', filters: [], highlightedDropAndProviderId: '', historyIds: [], @@ -415,6 +417,7 @@ describe('helpers', () => { description: '', deletedEventIds: [], eventIdToNoteIds: {}, + eventType: 'raw', filters: [], highlightedDropAndProviderId: '', historyIds: [], @@ -536,6 +539,7 @@ describe('helpers', () => { description: '', deletedEventIds: [], eventIdToNoteIds: {}, + eventType: 'raw', filters: [ { $state: { diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts index 41e13408c1e01d..c6cc75d74cad7a 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts @@ -58,7 +58,7 @@ export const isUntitled = ({ title }: OpenTimelineResult): boolean => const omitTypename = (key: string, value: keyof TimelineModel) => key === '__typename' ? undefined : value; -const omitTypenameInTimeline = (timeline: TimelineResult): TimelineResult => +export const omitTypenameInTimeline = (timeline: TimelineResult): TimelineResult => JSON.parse(JSON.stringify(timeline), omitTypename); const parseString = (params: string) => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.tsx index 6cf14cd972d3ec..030e9be7703ed5 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.tsx @@ -14,15 +14,15 @@ import { EventsLoading, EventsTd, EventsTdContent, EventsTdGroupActions } from ' import { eventHasNotes, getPinTooltip } from '../helpers'; import * as i18n from '../translations'; import { OnRowSelected } from '../../events'; -import { TimelineNonEcsData } from '../../../../graphql/types'; +import { Ecs } from '../../../../graphql/types'; export interface TimelineActionProps { eventId: string; - data: TimelineNonEcsData[]; + ecsData: Ecs; } export interface TimelineAction { - getAction: ({ eventId, data }: TimelineActionProps) => JSX.Element; + getAction: ({ eventId, ecsData }: TimelineActionProps) => JSX.Element; width: number; id: string; } diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/event_column_view.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/event_column_view.tsx index 9af860072ec1cb..1036c6b53b4c1d 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/event_column_view.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/event_column_view.tsx @@ -7,7 +7,7 @@ import React, { useMemo } from 'react'; import uuid from 'uuid'; -import { TimelineNonEcsData } from '../../../../graphql/types'; +import { TimelineNonEcsData, Ecs } from '../../../../graphql/types'; import { Note } from '../../../../lib/note'; import { AssociateNote, UpdateNote } from '../../../notes/helpers'; import { OnColumnResized, OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events'; @@ -26,6 +26,7 @@ interface Props { columnHeaders: ColumnHeader[]; columnRenderers: ColumnRenderer[]; data: TimelineNonEcsData[]; + ecsData: Ecs; eventIdToNoteIds: Readonly>; expanded: boolean; getNotesByIds: (noteIds: string[]) => Note[]; @@ -58,6 +59,7 @@ export const EventColumnView = React.memo( columnHeaders, columnRenderers, data, + ecsData, eventIdToNoteIds, expanded, getNotesByIds, @@ -83,11 +85,11 @@ export const EventColumnView = React.memo( return ( timelineTypeContext.timelineActions?.map(action => ( - {action.getAction({ eventId: id, data })} + {action.getAction({ eventId: id, ecsData })} )) ?? [] ); - }, [data, timelineTypeContext.timelineActions]); + }, [ecsData, timelineTypeContext.timelineActions]); return ( diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx index b93b0531c740f1..6c43d9a63029c2 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx @@ -30,6 +30,7 @@ import { ColumnHeader } from '../column_headers/column_header'; import { ColumnRenderer } from '../renderers/column_renderer'; import { getRowRenderer } from '../renderers/get_row_renderer'; import { RowRenderer } from '../renderers/row_renderer'; +import { getEventType } from '../helpers'; import { StatefulEventChild } from './stateful_event_child'; interface Props { @@ -215,6 +216,8 @@ const StatefulEventComponent: React.FC = ({ { if (c != null) { divElement.current = c; @@ -232,6 +235,7 @@ const StatefulEventComponent: React.FC = ({ columnHeaders={columnHeaders} columnRenderers={columnRenderers} data={event.data} + ecsData={event.ecs} eventIdToNoteIds={eventIdToNoteIds} expanded={!!expanded[event._id]} getNotesByIds={getNotesByIds} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event_child.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event_child.tsx index a39c254c61126b..16f89ca916d816 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event_child.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event_child.tsx @@ -7,7 +7,7 @@ import React from 'react'; import uuid from 'uuid'; -import { TimelineNonEcsData } from '../../../../graphql/types'; +import { TimelineNonEcsData, Ecs } from '../../../../graphql/types'; import { Note } from '../../../../lib/note'; import { AddNoteToEvent, UpdateNote } from '../../../notes/helpers'; import { NoteCards } from '../../../notes/note_cards'; @@ -26,6 +26,7 @@ interface Props { columnHeaders: ColumnHeader[]; columnRenderers: ColumnRenderer[]; data: TimelineNonEcsData[]; + ecsData: Ecs; expanded: boolean; eventIdToNoteIds: Readonly>; isEventViewer?: boolean; @@ -61,6 +62,7 @@ export const StatefulEventChild = React.memo( columnRenderers, expanded, data, + ecsData, eventIdToNoteIds, getNotesByIds, isEventViewer = false, @@ -92,6 +94,7 @@ export const StatefulEventChild = React.memo( columnHeaders={columnHeaders} columnRenderers={columnRenderers} data={data} + ecsData={ecsData} expanded={expanded} eventIdToNoteIds={eventIdToNoteIds} getNotesByIds={getNotesByIds} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.ts b/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.ts index c11b884f8a80a7..4b1d72a3af7b05 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.ts @@ -7,6 +7,7 @@ import { get, isEmpty, noop } from 'lodash/fp'; import { BrowserFields } from '../../../containers/source'; import { Ecs, TimelineItem, TimelineNonEcsData } from '../../../graphql/types'; +import { EventType } from '../../../store/timeline/model'; import { OnPinEvent, OnUnPinEvent } from '../events'; import { ColumnHeader } from './column_headers/column_header'; import * as i18n from './translations'; @@ -126,3 +127,11 @@ export const getEventIdToDataMapping = ( }; }, {}); }; + +/** Return eventType raw or signal */ +export const getEventType = (event: Ecs): Omit => { + if (!isEmpty(event.signal?.rule?.id)) { + return 'signal'; + } + return 'raw'; +}; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx index 8fe6759acf52d8..edf0613ac26932 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx @@ -281,6 +281,7 @@ const makeMapStateToProps = () => { const { columns, eventIdToNoteIds, + eventType, isSelectAllChecked, loadingEventIds, pinnedEventIds, @@ -292,6 +293,7 @@ const makeMapStateToProps = () => { return { columnHeaders: memoizedColumnHeaders(columns, browserFields), eventIdToNoteIds, + eventType, isSelectAllChecked, loadingEventIds, notesById: getNotesByIds(state), diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx index 4156c67e9b8419..bb8b04f6e304ef 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx @@ -5,7 +5,7 @@ */ import { isEqual } from 'lodash/fp'; -import React, { useEffect, useCallback } from 'react'; +import React, { useEffect, useCallback, useMemo } from 'react'; import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; @@ -14,7 +14,8 @@ import { esFilters } from '../../../../../../../src/plugins/data/public'; import { WithSource } from '../../containers/source'; import { inputsModel, inputsSelectors, State, timelineSelectors } from '../../store'; import { timelineActions } from '../../store/actions'; -import { KqlMode, timelineDefaults, TimelineModel } from '../../store/timeline/model'; +import { EventType, KqlMode, timelineDefaults, TimelineModel } from '../../store/timeline/model'; +import { useSignalIndex } from '../../containers/detection_engine/signals/use_signal_index'; import { ColumnHeader } from './body/column_headers/column_header'; import { DataProvider, QueryOperator } from './data_providers/data_provider'; @@ -41,6 +42,7 @@ interface StateReduxProps { activePage?: number; columns: ColumnHeader[]; dataProviders?: DataProvider[]; + eventType: EventType; end: number; filters: esFilters.Filter[]; isLive: boolean; @@ -139,6 +141,7 @@ const StatefulTimelineComponent = React.memo( columns, createTimeline, dataProviders, + eventType, end, filters, flyoutHeaderHeight, @@ -163,6 +166,15 @@ const StatefulTimelineComponent = React.memo( updateItemsPerPage, upsertColumn, }) => { + const [loading, signalIndexExists, signalIndexName] = useSignalIndex(); + + const indexToAdd = useMemo(() => { + if (signalIndexExists && signalIndexName != null && ['signal', 'all'].includes(eventType)) { + return [signalIndexName]; + } + return []; + }, [eventType, signalIndexExists, signalIndexName]); + const onDataProviderRemoved: OnDataProviderRemoved = useCallback( (providerId: string, andProviderId?: string) => removeProvider!({ id, providerId, andProviderId }), @@ -249,7 +261,7 @@ const StatefulTimelineComponent = React.memo( }, []); return ( - + {({ indexPattern, browserFields }) => ( ( flyoutHeight={flyoutHeight} id={id} indexPattern={indexPattern} + indexToAdd={indexToAdd} isLive={isLive} itemsPerPage={itemsPerPage!} itemsPerPageOptions={itemsPerPageOptions!} kqlMode={kqlMode} kqlQueryExpression={kqlQueryExpression} + loadingIndexName={loading} onChangeDataProviderKqlQuery={onChangeDataProviderKqlQuery} onChangeDroppableAndProvider={onChangeDroppableAndProvider} onChangeItemsPerPage={onChangeItemsPerPage} @@ -286,6 +300,7 @@ const StatefulTimelineComponent = React.memo( (prevProps, nextProps) => { return ( prevProps.activePage === nextProps.activePage && + prevProps.eventType === nextProps.eventType && prevProps.end === nextProps.end && prevProps.flyoutHeaderHeight === nextProps.flyoutHeaderHeight && prevProps.flyoutHeight === nextProps.flyoutHeight && @@ -320,6 +335,7 @@ const makeMapStateToProps = () => { const { columns, dataProviders, + eventType, filters, itemsPerPage, itemsPerPageOptions, @@ -334,6 +350,7 @@ const makeMapStateToProps = () => { return { columns, dataProviders, + eventType, end: input.timerange.to, filters: timelineFilter, id, diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx index 31d1002f161792..d25ebe8e80ad59 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx @@ -21,7 +21,7 @@ import { inputsSelectors, } from '../../../store'; import { timelineActions } from '../../../store/actions'; -import { KqlMode, timelineDefaults, TimelineModel } from '../../../store/timeline/model'; +import { KqlMode, timelineDefaults, TimelineModel, EventType } from '../../../store/timeline/model'; import { DispatchUpdateReduxTime, dispatchUpdateReduxTime } from '../../super_date_picker'; import { DataProvider } from '../data_providers/data_provider'; import { SearchOrFilter } from './search_or_filter'; @@ -34,6 +34,7 @@ interface OwnProps { interface StateReduxProps { dataProviders: DataProvider[]; + eventType: EventType; filters: esFilters.Filter[]; filterQuery: KueryFilterQuery; filterQueryDraft: KueryFilterQuery; @@ -55,6 +56,7 @@ interface DispatchProps { id: string; filterQuery: SerializedFilterQuery; }) => void; + updateEventType: ({ id, eventType }: { id: string; eventType: EventType }) => void; updateKqlMode: ({ id, kqlMode }: { id: string; kqlMode: KqlMode }) => void; setKqlFilterQueryDraft: ({ id, @@ -75,6 +77,7 @@ const StatefulSearchOrFilterComponent = React.memo( applyKqlFilterQuery, browserFields, dataProviders, + eventType, filters, filterQuery, filterQueryDraft, @@ -91,6 +94,7 @@ const StatefulSearchOrFilterComponent = React.memo( timelineId, to, toStr, + updateEventType, updateKqlMode, updateReduxTime, }) => { @@ -139,11 +143,21 @@ const StatefulSearchOrFilterComponent = React.memo( [timelineId] ); + const handleUpdateEventType = useCallback( + (newEventType: EventType) => + updateEventType({ + id: timelineId, + eventType: newEventType, + }), + [timelineId] + ); + return ( ( timelineId={timelineId} to={to} toStr={toStr} + updateEventType={handleUpdateEventType} updateKqlMode={updateKqlMode!} updateReduxTime={updateReduxTime} /> @@ -167,6 +182,7 @@ const StatefulSearchOrFilterComponent = React.memo( }, (prevProps, nextProps) => { return ( + prevProps.eventType === nextProps.eventType && prevProps.from === nextProps.from && prevProps.fromStr === nextProps.fromStr && prevProps.to === nextProps.to && @@ -200,6 +216,7 @@ const makeMapStateToProps = () => { const policy: inputsModel.Policy = getInputsPolicy(state); return { dataProviders: timeline.dataProviders, + eventType: timeline.eventType ?? 'raw', filterQuery: getKqlFilterQuery(state, timelineId), filterQueryDraft: getKqlFilterQueryDraft(state, timelineId), filters: timeline.filters, @@ -224,6 +241,8 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ filterQuery, }) ), + updateEventType: ({ id, eventType }: { id: string; eventType: EventType }) => + dispatch(timelineActions.updateEventType({ id, eventType })), updateKqlMode: ({ id, kqlMode }: { id: string; kqlMode: KqlMode }) => dispatch(timelineActions.updateKqlMode({ id, kqlMode })), setKqlFilterQueryDraft: ({ diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/pick_events.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/pick_events.tsx new file mode 100644 index 00000000000000..76f9e6fe3673a2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/pick_events.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiHealth, EuiSuperSelect } from '@elastic/eui'; +import React, { memo } from 'react'; +import styled from 'styled-components'; + +import { EventType } from '../../../store/timeline/model'; +import * as i18n from './translations'; + +interface EventTypeOptionItem { + value: EventType; + inputDisplay: React.ReactElement; +} + +const AllEuiHealth = styled(EuiHealth)` + margin-left: -2px; + svg { + stroke: #fff; + stroke-width: 1px; + stroke-linejoin: round; + width: 19px; + height: 19px; + margin-top: 1px; + z-index: 1; + } +`; + +const WarningEuiHealth = styled(EuiHealth)` + margin-left: -17px; + svg { + z-index: 0; + } +`; + +const PickEventContainer = styled.div` + .euiSuperSelect { + width: 155px; + max-width: 155px; + button.euiSuperSelectControl { + padding-top: 3px; + } + } +`; + +export const eventTypeOptions: EventTypeOptionItem[] = [ + { + value: 'all', + inputDisplay: ( + + {i18n.ALL_EVENT} + + ), + }, + { + value: 'raw', + inputDisplay: {i18n.RAW_EVENT}, + }, + { + value: 'signal', + inputDisplay: {i18n.SIGNAL_EVENT}, + }, +]; + +interface PickEventTypeProps { + eventType: EventType; + onChangeEventType: (value: EventType) => void; +} + +const PickEventTypeComponents: React.FC = ({ + eventType, + onChangeEventType, +}) => { + return ( + + + + ); +}; + +export const PickEventType = memo(PickEventTypeComponents); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx index 45eb7f85c809f5..881540485fcfb3 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx @@ -11,13 +11,14 @@ import styled, { createGlobalStyle } from 'styled-components'; import { esFilters, IIndexPattern } from '../../../../../../../../src/plugins/data/public'; import { BrowserFields } from '../../../containers/source'; import { KueryFilterQuery, KueryFilterQueryKind } from '../../../store'; -import { KqlMode } from '../../../store/timeline/model'; +import { KqlMode, EventType } from '../../../store/timeline/model'; +import { DispatchUpdateReduxTime } from '../../super_date_picker'; import { DataProvider } from '../data_providers/data_provider'; import { QueryBarTimeline } from '../query_bar'; import { options } from './helpers'; import * as i18n from './translations'; -import { DispatchUpdateReduxTime } from '../../super_date_picker'; +import { PickEventType } from './pick_events'; const timelineSelectModeItemsClassName = 'timelineSelectModeItemsClassName'; const searchOrFilterPopoverClassName = 'searchOrFilterPopover'; @@ -42,6 +43,7 @@ interface Props { applyKqlFilterQuery: (expression: string, kind: KueryFilterQueryKind) => void; browserFields: BrowserFields; dataProviders: DataProvider[]; + eventType: EventType; filterQuery: KueryFilterQuery; filterQueryDraft: KueryFilterQuery; from: number; @@ -59,6 +61,7 @@ interface Props { savedQueryId: string | null; to: number; toStr: string; + updateEventType: (eventType: EventType) => void; updateReduxTime: DispatchUpdateReduxTime; } @@ -88,6 +91,7 @@ export const SearchOrFilter = React.memo( applyKqlFilterQuery, browserFields, dataProviders, + eventType, indexPattern, isRefreshPaused, filters, @@ -104,6 +108,7 @@ export const SearchOrFilter = React.memo( setSavedQueryId, to, toStr, + updateEventType, updateKqlMode, updateReduxTime, }) => ( @@ -148,6 +153,9 @@ export const SearchOrFilter = React.memo( updateReduxTime={updateReduxTime} /> + + + diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/translations.ts b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/translations.ts index 5072e5bd02cc34..c34b6b08ffd390 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/translations.ts +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/translations.ts @@ -69,3 +69,18 @@ export const FILTER_OR_SEARCH_WITH_KQL = i18n.translate( defaultMessage: 'Filter or Search with KQL', } ); + +export const ALL_EVENT = i18n.translate('xpack.siem.timeline.searchOrFilter.eventTypeAllEvent', { + defaultMessage: 'All events', +}); + +export const RAW_EVENT = i18n.translate('xpack.siem.timeline.searchOrFilter.eventTypeRawEvent', { + defaultMessage: 'Raw events', +}); + +export const SIGNAL_EVENT = i18n.translate( + 'xpack.siem.timeline.searchOrFilter.eventTypeSignalEvent', + { + defaultMessage: 'Signal events', + } +); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx index c4361bbc8990d5..c7259edbdc5937 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx @@ -74,6 +74,7 @@ const basicSuperSelectOptions = [ const getBasicSelectableOptions = (timelineId: string) => [ { description: i18n.DEFAULT_TIMELINE_DESCRIPTION, + favorite: [], label: i18n.DEFAULT_TIMELINE_TITLE, id: null, title: i18n.DEFAULT_TIMELINE_TITLE, @@ -143,7 +144,11 @@ const SearchTimelineSuperSelectComponent: React.FC - + ); @@ -293,7 +298,7 @@ const SearchTimelineSuperSelectComponent: React.FC ({ description: t.description, - favorite: !isEmpty(t.favorite), + favorite: t.favorite, label: t.title, id: t.savedObjectId, key: `${t.title}-${index}`, diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx index b6fdc1b2973aaf..d5e5d15eb8ad26 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx @@ -8,6 +8,7 @@ import { EuiLoadingSpinner } from '@elastic/eui'; import { rgba } from 'polished'; import styled, { createGlobalStyle } from 'styled-components'; +import { EventType } from '../../store/timeline/model'; import { IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME } from '../drag_and_drop/helpers'; /** @@ -155,9 +156,14 @@ export const EventsTbody = styled.div.attrs(({ className = '' }) => ({ export const EventsTrGroup = styled.div.attrs(({ className = '' }) => ({ className: `siemEventsTable__trGroup ${className}`, -}))<{ className?: string }>` +}))<{ className?: string; eventType: Omit; showLeftBorder: boolean }>` border-bottom: ${({ theme }) => theme.eui.euiBorderWidthThin} solid ${({ theme }) => theme.eui.euiColorLightShade}; + ${({ theme, eventType, showLeftBorder }) => + showLeftBorder + ? `border-left: 4px solid + ${eventType === 'raw' ? theme.eui.euiColorLightShade : theme.eui.euiColorWarning}` + : ''}; &:hover { background-color: ${({ theme }) => theme.eui.euiTableHoverColor}; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx index 2971053bc5252b..0be5e69abea382 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx @@ -58,11 +58,13 @@ describe('Timeline', () => { flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} + indexToAdd={[]} isLive={false} itemsPerPage={5} itemsPerPageOptions={[5, 10, 20]} kqlMode="search" kqlQueryExpression="" + loadingIndexName={false} onChangeDataProviderKqlQuery={jest.fn()} onChangeDroppableAndProvider={jest.fn()} onChangeItemsPerPage={jest.fn()} @@ -94,11 +96,13 @@ describe('Timeline', () => { flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} + indexToAdd={[]} isLive={false} itemsPerPage={5} itemsPerPageOptions={[5, 10, 20]} kqlMode="search" kqlQueryExpression="" + loadingIndexName={false} onChangeDataProviderKqlQuery={jest.fn()} onChangeDroppableAndProvider={jest.fn()} onChangeItemsPerPage={jest.fn()} @@ -133,11 +137,13 @@ describe('Timeline', () => { flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} + indexToAdd={[]} isLive={false} itemsPerPage={5} itemsPerPageOptions={[5, 10, 20]} kqlMode="search" kqlQueryExpression="" + loadingIndexName={false} onChangeDataProviderKqlQuery={jest.fn()} onChangeDroppableAndProvider={jest.fn()} onChangeItemsPerPage={jest.fn()} @@ -172,11 +178,13 @@ describe('Timeline', () => { flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} + indexToAdd={[]} isLive={false} itemsPerPage={5} itemsPerPageOptions={[5, 10, 20]} kqlMode="search" kqlQueryExpression="" + loadingIndexName={false} onChangeDataProviderKqlQuery={jest.fn()} onChangeDroppableAndProvider={jest.fn()} onChangeItemsPerPage={jest.fn()} @@ -216,11 +224,13 @@ describe('Timeline', () => { flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} + indexToAdd={[]} isLive={false} itemsPerPage={5} itemsPerPageOptions={[5, 10, 20]} kqlMode="search" kqlQueryExpression="" + loadingIndexName={false} onChangeDataProviderKqlQuery={jest.fn()} onChangeDroppableAndProvider={jest.fn()} onChangeItemsPerPage={jest.fn()} @@ -262,11 +272,13 @@ describe('Timeline', () => { flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} + indexToAdd={[]} isLive={false} itemsPerPage={5} itemsPerPageOptions={[5, 10, 20]} kqlMode="search" kqlQueryExpression="" + loadingIndexName={false} onChangeDataProviderKqlQuery={jest.fn()} onChangeDroppableAndProvider={jest.fn()} onChangeItemsPerPage={jest.fn()} @@ -316,11 +328,13 @@ describe('Timeline', () => { flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} + indexToAdd={[]} isLive={false} itemsPerPage={5} itemsPerPageOptions={[5, 10, 20]} kqlMode="search" kqlQueryExpression="" + loadingIndexName={false} onChangeDataProviderKqlQuery={jest.fn()} onChangeDroppableAndProvider={jest.fn()} onChangeItemsPerPage={jest.fn()} @@ -374,11 +388,13 @@ describe('Timeline', () => { flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} + indexToAdd={[]} isLive={false} itemsPerPage={5} itemsPerPageOptions={[5, 10, 20]} kqlMode="search" kqlQueryExpression="" + loadingIndexName={false} onChangeDataProviderKqlQuery={jest.fn()} onChangeDroppableAndProvider={jest.fn()} onChangeItemsPerPage={jest.fn()} @@ -435,11 +451,13 @@ describe('Timeline', () => { flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} + indexToAdd={[]} isLive={false} itemsPerPage={5} itemsPerPageOptions={[5, 10, 20]} kqlMode="search" kqlQueryExpression="" + loadingIndexName={false} onChangeDataProviderKqlQuery={jest.fn()} onChangeDroppableAndProvider={jest.fn()} onChangeItemsPerPage={jest.fn()} @@ -486,11 +504,13 @@ describe('Timeline', () => { flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} + indexToAdd={[]} isLive={false} itemsPerPage={5} itemsPerPageOptions={[5, 10, 20]} kqlMode="search" kqlQueryExpression="" + loadingIndexName={false} onChangeDataProviderKqlQuery={jest.fn()} onChangeDroppableAndProvider={jest.fn()} onChangeItemsPerPage={jest.fn()} @@ -543,11 +563,13 @@ describe('Timeline', () => { flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} + indexToAdd={[]} isLive={false} itemsPerPage={5} itemsPerPageOptions={[5, 10, 20]} kqlMode="search" kqlQueryExpression="" + loadingIndexName={false} onChangeDataProviderKqlQuery={jest.fn()} onChangeDroppableAndProvider={jest.fn()} onChangeItemsPerPage={jest.fn()} @@ -604,11 +626,13 @@ describe('Timeline', () => { flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} + indexToAdd={[]} isLive={false} itemsPerPage={5} itemsPerPageOptions={[5, 10, 20]} kqlMode="search" kqlQueryExpression="" + loadingIndexName={false} onChangeDataProviderKqlQuery={jest.fn()} onChangeDroppableAndProvider={jest.fn()} onChangeItemsPerPage={jest.fn()} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx index e15c58d32425a8..ece5b4fa18d1c4 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx @@ -65,11 +65,13 @@ interface Props { flyoutHeight: number; id: string; indexPattern: IIndexPattern; + indexToAdd: string[]; isLive: boolean; itemsPerPage: number; itemsPerPageOptions: number[]; kqlMode: KqlMode; kqlQueryExpression: string; + loadingIndexName: boolean; onChangeDataProviderKqlQuery: OnChangeDataProviderKqlQuery; onChangeDroppableAndProvider: OnChangeDroppableAndProvider; onChangeItemsPerPage: OnChangeItemsPerPage; @@ -95,11 +97,13 @@ export const TimelineComponent = ({ flyoutHeight, id, indexPattern, + indexToAdd, isLive, itemsPerPage, itemsPerPageOptions, kqlMode, kqlQueryExpression, + loadingIndexName, onChangeDataProviderKqlQuery, onChangeDroppableAndProvider, onChangeItemsPerPage, @@ -156,6 +160,7 @@ export const TimelineComponent = ({ {combinedQueries != null ? ( c.id)} sourceId="default" limit={itemsPerPage} @@ -175,7 +180,7 @@ export const TimelineComponent = ({ getUpdatedAt, refetch, }) => ( - + = { - 'detection-engine': [], + 'detection-engine': [ + CONSTANTS.appQuery, + CONSTANTS.filters, + CONSTANTS.savedQuery, + CONSTANTS.timerange, + CONSTANTS.timeline, + ], host: [ CONSTANTS.appQuery, CONSTANTS.filters, diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx index 88a1333c82a45f..f7a30766ad7d8c 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx @@ -75,7 +75,9 @@ export const useFetchIndexPatterns = (defaultIndices: string[] = []): Return => setIndexPatterns( getIndexFields(indices.join(), get('data.source.status.indexFields', result)) ); - setBrowserFields(getBrowserFields(get('data.source.status.indexFields', result))); + setBrowserFields( + getBrowserFields(indices.join(), get('data.source.status.indexFields', result)) + ); } }, error => { diff --git a/x-pack/legacy/plugins/siem/public/containers/source/index.tsx b/x-pack/legacy/plugins/siem/public/containers/source/index.tsx index 94524dedbcd596..e995d123b1b44c 100644 --- a/x-pack/legacy/plugins/siem/public/containers/source/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/source/index.tsx @@ -5,9 +5,9 @@ */ import { isUndefined } from 'lodash'; -import { get, keyBy, pick, set } from 'lodash/fp'; +import { get, keyBy, pick, set, isEmpty } from 'lodash/fp'; import { Query } from 'react-apollo'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import memoizeOne from 'memoize-one'; import { IIndexPattern } from 'src/plugins/data/public'; import { useUiSetting$ } from '../../lib/kibana'; @@ -57,6 +57,7 @@ interface WithSourceArgs { interface WithSourceProps { children: (args: WithSourceArgs) => React.ReactNode; + indexToAdd?: string[] | null; sourceId: string; } @@ -71,7 +72,7 @@ export const getIndexFields = memoizeOne( ); export const getBrowserFields = memoizeOne( - (fields: IndexField[]): BrowserFields => + (title: string, fields: IndexField[]): BrowserFields => fields && fields.length > 0 ? fields.reduce( (accumulator: BrowserFields, field: IndexField) => @@ -81,8 +82,14 @@ export const getBrowserFields = memoizeOne( : {} ); -export const WithSource = React.memo(({ children, sourceId }) => { - const [defaultIndex] = useUiSetting$(DEFAULT_INDEX_KEY); +export const WithSource = React.memo(({ children, indexToAdd, sourceId }) => { + const [configIndex] = useUiSetting$(DEFAULT_INDEX_KEY); + const defaultIndex = useMemo(() => { + if (indexToAdd != null && !isEmpty(indexToAdd)) { + return [...configIndex, ...indexToAdd]; + } + return configIndex; + }, [configIndex, DEFAULT_INDEX_KEY, indexToAdd]); return ( query={sourceQuery} @@ -96,7 +103,10 @@ export const WithSource = React.memo(({ children, sourceId }) = {({ data }) => children({ indicesExist: get('source.status.indicesExist', data), - browserFields: getBrowserFields(get('source.status.indexFields', data)), + browserFields: getBrowserFields( + defaultIndex.join(), + get('source.status.indexFields', data) + ), indexPattern: getIndexFields(defaultIndex.join(), get('source.status.indexFields', data)), }) } @@ -139,7 +149,9 @@ export const useWithSource = (sourceId: string, indices: string[]) => { updateLoading(false); updateErrorMessage(null); setIndicesExist(get('data.source.status.indicesExist', result)); - setBrowserFields(getBrowserFields(get('data.source.status.indexFields', result))); + setBrowserFields( + getBrowserFields(indices.join(), get('data.source.status.indexFields', result)) + ); setIndexPattern( getIndexFields(indices.join(), get('data.source.status.indexFields', result)) ); diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/index.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/timeline/index.gql_query.ts index 90b94be5d7e370..9bd580f8322303 100644 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/index.gql_query.ts +++ b/x-pack/legacy/plugins/siem/public/containers/timeline/index.gql_query.ts @@ -189,6 +189,22 @@ export const timelineQuery = gql` region_name country_iso_code } + signal { + original_time + rule { + id + saved_id + timeline_id + timeline_title + output_index + from + index + language + query + to + filters + } + } suricata { eve { proto diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx b/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx index 3d4b878d9bf632..c585e04d2cfd7b 100644 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEmpty, getOr } from 'lodash/fp'; +import { getOr, isEmpty } from 'lodash/fp'; import memoizeOne from 'memoize-one'; import React from 'react'; import { Query } from 'react-apollo'; @@ -47,6 +47,7 @@ export interface OwnProps extends QueryTemplateProps { children?: (args: TimelineArgs) => React.ReactNode; id: string; indexPattern?: IIndexPattern; + indexToAdd?: string[]; limit: number; sortField: SortField; fields: string[]; @@ -71,6 +72,7 @@ class TimelineQueryComponent extends QueryTemplate< children, id, indexPattern, + indexToAdd = [], isInspected, kibana, limit, @@ -79,10 +81,11 @@ class TimelineQueryComponent extends QueryTemplate< sourceId, sortField, } = this.props; - const defaultIndex = - indexPattern == null || isEmpty(indexPattern) - ? kibana.services.uiSettings.get(DEFAULT_INDEX_KEY) - : indexPattern?.title.split(','); + // I needed to do that to avoid test to yell at me since there is no good way yet to mock withKibana + const defaultKibanaIndex = kibana.services.uiSettings.get(DEFAULT_INDEX_KEY) ?? []; + const defaultIndex = isEmpty(indexPattern) + ? [...defaultKibanaIndex, ...indexToAdd] + : indexPattern?.title.split(',') ?? []; const variables: GetTimelineQuery.Variables = { fieldRequested: fields, filterQuery: createFilter(filterQuery), diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/one/index.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/timeline/one/index.gql_query.ts index eebb4a349dab43..e68db445a5cbbf 100644 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/one/index.gql_query.ts +++ b/x-pack/legacy/plugins/siem/public/containers/timeline/one/index.gql_query.ts @@ -55,6 +55,7 @@ export const oneTimelineQuery = gql` end } description + eventType eventIdToNoteIds { eventId note diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/persist.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/timeline/persist.gql_query.ts index 68b749064dc0cf..6a0609f9158f3b 100644 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/persist.gql_query.ts +++ b/x-pack/legacy/plugins/siem/public/containers/timeline/persist.gql_query.ts @@ -55,6 +55,7 @@ export const persistTimelineMutation = gql` } } description + eventType favorite { fullName userName diff --git a/x-pack/legacy/plugins/siem/public/graphql/introspection.json b/x-pack/legacy/plugins/siem/public/graphql/introspection.json index c48b5ab9a27a4d..d73755fb92185c 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/introspection.json +++ b/x-pack/legacy/plugins/siem/public/graphql/introspection.json @@ -3932,6 +3932,14 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "signal", + "description": "", + "args": [], + "type": { "kind": "OBJECT", "name": "SignalField", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "source", "description": "", @@ -4682,6 +4690,316 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "SignalField", + "description": "", + "fields": [ + { + "name": "rule", + "description": "", + "args": [], + "type": { "kind": "OBJECT", "name": "RuleField", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "original_time", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RuleField", + "description": "", + "fields": [ + { + "name": "id", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rule_id", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "false_positives", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "saved_id", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timeline_id", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timeline_title", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "max_signals", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToNumberArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "risk_score", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "output_index", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "from", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "immutable", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToBooleanArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "index", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "interval", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "language", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "query", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "references", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "severity", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tags", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "threats", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToAny", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "size", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "to", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enabled", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToBooleanArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filters", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToAny", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "created_at", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updated_at", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "created_by", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updated_by", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "version", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "ToBooleanArray", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "ToAny", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "SuricataEcsFields", @@ -5011,16 +5329,6 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "SCALAR", - "name": "ToBooleanArray", - "description": "", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, { "kind": "OBJECT", "name": "ZeekNoticeData", @@ -9425,6 +9733,14 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "eventType", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "favorite", "description": "", @@ -10666,6 +10982,12 @@ "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "defaultValue": null }, + { + "name": "eventType", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, { "name": "filters", "description": "", diff --git a/x-pack/legacy/plugins/siem/public/graphql/types.ts b/x-pack/legacy/plugins/siem/public/graphql/types.ts index e35ddedafc7c84..73049e26f15815 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/public/graphql/types.ts @@ -122,6 +122,8 @@ export interface TimelineInput { description?: Maybe; + eventType?: Maybe; + filters?: Maybe; kqlMode?: Maybe; @@ -367,6 +369,8 @@ export type ToDateArray = string[]; export type ToBooleanArray = boolean[]; +export type ToAny = any; + export type EsValue = any; // ==================================================== @@ -785,6 +789,8 @@ export interface Ecs { network?: Maybe; + signal?: Maybe; + source?: Maybe; suricata?: Maybe; @@ -962,6 +968,74 @@ export interface NetworkEcsField { transport?: Maybe; } +export interface SignalField { + rule?: Maybe; + + original_time?: Maybe; +} + +export interface RuleField { + id?: Maybe; + + rule_id?: Maybe; + + false_positives: string[]; + + saved_id?: Maybe; + + timeline_id?: Maybe; + + timeline_title?: Maybe; + + max_signals?: Maybe; + + risk_score?: Maybe; + + output_index?: Maybe; + + description?: Maybe; + + from?: Maybe; + + immutable?: Maybe; + + index?: Maybe; + + interval?: Maybe; + + language?: Maybe; + + query?: Maybe; + + references?: Maybe; + + severity?: Maybe; + + tags?: Maybe; + + threats?: Maybe; + + type?: Maybe; + + size?: Maybe; + + to?: Maybe; + + enabled?: Maybe; + + filters?: Maybe; + + created_at?: Maybe; + + updated_at?: Maybe; + + created_by?: Maybe; + + updated_by?: Maybe; + + version?: Maybe; +} + export interface SuricataEcsFields { eve?: Maybe; } @@ -1848,6 +1922,8 @@ export interface TimelineResult { eventIdToNoteIds?: Maybe; + eventType?: Maybe; + favorite?: Maybe; filters?: Maybe; @@ -4349,6 +4425,8 @@ export namespace GetTimelineQuery { geo: Maybe<__Geo>; + signal: Maybe; + suricata: Maybe; network: Maybe; @@ -4668,6 +4746,40 @@ export namespace GetTimelineQuery { country_iso_code: Maybe; }; + export type Signal = { + __typename?: 'SignalField'; + + original_time: Maybe; + + rule: Maybe; + }; + + export type Rule = { + __typename?: 'RuleField'; + + id: Maybe; + + saved_id: Maybe; + + timeline_id: Maybe; + + timeline_title: Maybe; + + output_index: Maybe; + + from: Maybe; + + index: Maybe; + + language: Maybe; + + query: Maybe; + + to: Maybe; + + filters: Maybe; + }; + export type Suricata = { __typename?: 'SuricataEcsFields'; @@ -5069,6 +5181,8 @@ export namespace GetOneTimeline { description: Maybe; + eventType: Maybe; + eventIdToNoteIds: Maybe; favorite: Maybe; @@ -5387,6 +5501,8 @@ export namespace PersistTimelineMutation { description: Maybe; + eventType: Maybe; + favorite: Maybe; filters: Maybe; diff --git a/x-pack/legacy/plugins/siem/public/lib/keury/index.ts b/x-pack/legacy/plugins/siem/public/lib/keury/index.ts index d82079dd05d31e..acd8b2d25f2ae5 100644 --- a/x-pack/legacy/plugins/siem/public/lib/keury/index.ts +++ b/x-pack/legacy/plugins/siem/public/lib/keury/index.ts @@ -17,7 +17,7 @@ import { KueryFilterQuery } from '../../store'; export const convertKueryToElasticSearchQuery = ( kueryExpression: string, - indexPattern: IIndexPattern + indexPattern?: IIndexPattern ) => { try { return kueryExpression diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx index d08e282a4c399b..4701ed93dc4f0a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx @@ -4,11 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ +import dateMath from '@elastic/datemath'; +import { getOr } from 'lodash/fp'; import moment from 'moment'; import { updateSignalStatus } from '../../../../containers/detection_engine/signals/api'; -import { SendSignalsToTimelineActionProps, UpdateSignalStatusActionProps } from './types'; -import { TimelineNonEcsData } from '../../../../graphql/types'; +import { SendSignalToTimelineActionProps, UpdateSignalStatusActionProps } from './types'; +import { TimelineNonEcsData, GetOneTimeline, TimelineResult } from '../../../../graphql/types'; +import { oneTimelineQuery } from '../../../../containers/timeline/one/index.gql_query'; +import { + omitTypenameInTimeline, + formatTimelineResultToModel, +} from '../../../../components/open_timeline/helpers'; +import { convertKueryToElasticSearchQuery } from '../../../../lib/keury'; +import { timelineDefaults } from '../../../../store/timeline/model'; +import { + replaceTemplateFieldFromQuery, + replaceTemplateFieldFromMatchFilters, + replaceTemplateFieldFromDataProviders, +} from './helpers'; export const getUpdateSignalsQuery = (eventIds: Readonly) => { return { @@ -58,19 +72,111 @@ export const updateSignalStatusAction = async ({ } }; -export const sendSignalsToTimelineAction = async ({ +export const sendSignalToTimelineAction = async ({ + apolloClient, createTimeline, - data, -}: SendSignalsToTimelineActionProps) => { - const stringFilter = data[0].filter(d => d.field === 'signal.rule.filters')?.[0]?.value ?? []; + ecsData, + updateTimelineIsLoading, +}: SendSignalToTimelineActionProps) => { + const timelineId = + ecsData.signal?.rule?.timeline_id != null ? ecsData.signal?.rule?.timeline_id[0] : ''; - // TODO: Switch to using from/to when adding dateRange - // const [stringFilters, from, to] = getFilterAndRuleBounds(data); - const parsedFilter = stringFilter.map(sf => JSON.parse(sf)); - createTimeline({ - id: 'timeline-1', - filters: parsedFilter, - dateRange: undefined, // TODO - kqlQuery: undefined, // TODO - }); + const ellapsedTimeRule = moment.duration( + moment().diff( + dateMath.parse(ecsData.signal?.rule?.from != null ? ecsData.signal?.rule?.from[0] : 'now-0s') + ) + ); + + const from = moment(ecsData.timestamp ?? new Date()) + .subtract(ellapsedTimeRule) + .valueOf(); + const to = moment(ecsData.timestamp ?? new Date()).valueOf(); + + if (timelineId !== '' && apolloClient != null) { + try { + updateTimelineIsLoading({ id: 'timeline-1', isLoading: true }); + const responseTimeline = await apolloClient.query< + GetOneTimeline.Query, + GetOneTimeline.Variables + >({ + query: oneTimelineQuery, + fetchPolicy: 'no-cache', + variables: { + id: timelineId, + }, + }); + + const timelineTemplate: TimelineResult = omitTypenameInTimeline( + getOr({}, 'data.getOneTimeline', responseTimeline) + ); + const { timeline } = formatTimelineResultToModel(timelineTemplate, true); + const query = replaceTemplateFieldFromQuery( + timeline.kqlQuery?.filterQuery?.kuery?.expression ?? '', + ecsData + ); + const filters = replaceTemplateFieldFromMatchFilters(timeline.filters ?? [], ecsData); + const dataProviders = replaceTemplateFieldFromDataProviders( + timeline.dataProviders ?? [], + ecsData + ); + createTimeline({ + from, + timeline: { + ...timeline, + dataProviders, + eventType: 'all', + filters, + dateRange: { + start: from, + end: to, + }, + kqlQuery: { + filterQuery: { + kuery: { + kind: timeline.kqlQuery?.filterQuery?.kuery?.kind ?? 'kuery', + expression: query, + }, + serializedQuery: convertKueryToElasticSearchQuery(query), + }, + filterQueryDraft: { + kind: timeline.kqlQuery?.filterQuery?.kuery?.kind ?? 'kuery', + expression: query, + }, + }, + show: true, + }, + to, + }); + } catch { + updateTimelineIsLoading({ id: 'timeline-1', isLoading: false }); + } + } else { + const query = `_id: ${ecsData._id}`; + createTimeline({ + from, + timeline: { + ...timelineDefaults, + id: 'timeline-1', + dateRange: { + start: from, + end: to, + }, + eventType: 'all', + kqlQuery: { + filterQuery: { + kuery: { + kind: 'kuery', + expression: query, + }, + serializedQuery: convertKueryToElasticSearchQuery(query), + }, + filterQueryDraft: { + kind: 'kuery', + expression: query, + }, + }, + }, + to, + }); + } }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx index 83b6ba690ec5bf..5c4795a8192753 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx @@ -7,6 +7,7 @@ /* eslint-disable react/display-name */ import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import ApolloClient from 'apollo-client'; import React from 'react'; import { esFilters } from '../../../../../../../../../src/plugins/data/common/es_query'; @@ -20,7 +21,7 @@ import { import { SubsetTimelineModel, timelineDefaults } from '../../../../store/timeline/model'; import { FILTER_OPEN } from './signals_filter_group'; -import { sendSignalsToTimelineAction, updateSignalStatusAction } from './actions'; +import { sendSignalToTimelineAction, updateSignalStatusAction } from './actions'; import * as i18n from './translations'; import { CreateTimeline, SetEventsDeletedProps, SetEventsLoadingProps } from './types'; @@ -168,66 +169,71 @@ export const requiredFieldsForActions = [ ]; export const getSignalsActions = ({ + apolloClient, canUserCRUD, hasIndexWrite, setEventsLoading, setEventsDeleted, createTimeline, status, + updateTimelineIsLoading, }: { + apolloClient?: ApolloClient<{}>; canUserCRUD: boolean; hasIndexWrite: boolean; setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void; setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void; createTimeline: CreateTimeline; status: 'open' | 'closed'; -}): TimelineAction[] => { - const actions = [ - { - getAction: ({ eventId, data }: TimelineActionProps): JSX.Element => ( - - sendSignalsToTimelineAction({ createTimeline, data: [data] })} - iconType="tableDensityNormal" - aria-label="Next" - /> - - ), - id: 'sendSignalToTimeline', - width: 26, - }, - ]; - return canUserCRUD && hasIndexWrite - ? [ - ...actions, - { - getAction: ({ eventId, data }: TimelineActionProps): JSX.Element => ( - - - updateSignalStatusAction({ - signalIds: [eventId], - status, - setEventsLoading, - setEventsDeleted, - }) - } - iconType={status === FILTER_OPEN ? 'indexOpen' : 'indexClose'} - aria-label="Next" - /> - - ), - id: 'updateSignalStatus', - width: 26, - }, - ] - : actions; -}; + updateTimelineIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => void; +}): TimelineAction[] => [ + { + getAction: ({ eventId, ecsData }: TimelineActionProps): JSX.Element => ( + + + sendSignalToTimelineAction({ + apolloClient, + createTimeline, + ecsData, + updateTimelineIsLoading, + }) + } + iconType="tableDensityNormal" + aria-label="Next" + /> + + ), + id: 'sendSignalToTimeline', + width: 26, + }, + { + getAction: ({ eventId }: TimelineActionProps): JSX.Element => ( + + + updateSignalStatusAction({ + signalIds: [eventId], + status, + setEventsLoading, + setEventsDeleted, + }) + } + isDisabled={!canUserCRUD || !hasIndexWrite} + iconType={status === FILTER_OPEN ? 'indexOpen' : 'indexClose'} + aria-label="Next" + /> + + ), + id: 'updateSignalStatus', + width: 26, + }, +]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/helpers.ts new file mode 100644 index 00000000000000..653f4978db305a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/helpers.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get, isEmpty } from 'lodash/fp'; +import { esKuery } from '../../../../../../../../../src/plugins/data/common'; +import { esFilters } from '../../../../../../../../../src/plugins/data/public'; +import { + DataProvider, + DataProvidersAnd, +} from '../../../../components/timeline/data_providers/data_provider'; +import { Ecs } from '../../../../graphql/types'; + +interface FindValueToChangeInQuery { + field: string; + valueToChange: string; +} + +const templateFields = [ + 'host.name', + 'host.hostname', + 'host.domain', + 'host.id', + 'host.ip', + 'client.ip', + 'destination.ip', + 'server.ip', + 'source.ip', + 'network.community_id', + 'user.name', + 'process.name', +]; + +export const findValueToChangeInQuery = ( + keuryNode: esKuery.KueryNode, + valueToChange: FindValueToChangeInQuery[] = [] +): FindValueToChangeInQuery[] => { + let localValueToChange = valueToChange; + if (keuryNode.function === 'is' && templateFields.includes(keuryNode.arguments[0].value)) { + localValueToChange = [ + ...localValueToChange, + { + field: keuryNode.arguments[0].value, + valueToChange: keuryNode.arguments[1].value, + }, + ]; + } + return keuryNode.arguments.reduce( + (addValueToChange: FindValueToChangeInQuery[], ast: esKuery.KueryNode) => { + if (ast.function === 'is' && templateFields.includes(ast.arguments[0].value)) { + return [ + ...addValueToChange, + { + field: ast.arguments[0].value, + valueToChange: ast.arguments[1].value, + }, + ]; + } + if (ast.arguments) { + return findValueToChangeInQuery(ast, addValueToChange); + } + return addValueToChange; + }, + localValueToChange + ); +}; + +export const replaceTemplateFieldFromQuery = (query: string, ecsData: Ecs) => { + if (query.trim() !== '') { + const valueToChange = findValueToChangeInQuery(esKuery.fromKueryExpression(query)); + return valueToChange.reduce((newQuery, vtc) => { + const newValue = get(vtc.field, ecsData); + if (newValue != null) { + return newQuery.replace(vtc.valueToChange, newValue); + } + return newQuery; + }, query); + } + return ''; +}; + +export const replaceTemplateFieldFromMatchFilters = (filters: esFilters.Filter[], ecsData: Ecs) => + filters.map(filter => { + if ( + filter.meta.type === 'phrase' && + filter.meta.key != null && + templateFields.includes(filter.meta.key) + ) { + const newValue = get(filter.meta.key, ecsData); + if (newValue != null) { + filter.meta.params = { query: newValue }; + filter.query = { match_phrase: { [filter.meta.key]: newValue } }; + } + } + return filter; + }); + +export const reformatDataProviderWithNewValue = ( + dataProvider: T, + ecsData: Ecs +): T => { + if (templateFields.includes(dataProvider.queryMatch.field)) { + const newValue = get(dataProvider.queryMatch.field, ecsData); + if (newValue != null) { + dataProvider.id = dataProvider.id.replace(dataProvider.name, newValue); + dataProvider.name = newValue; + dataProvider.queryMatch.value = newValue; + dataProvider.queryMatch.displayField = undefined; + dataProvider.queryMatch.displayValue = undefined; + } + } + return dataProvider; +}; + +export const replaceTemplateFieldFromDataProviders = ( + dataProviders: DataProvider[], + ecsData: Ecs +) => + dataProviders.map((dataProvider: DataProvider) => { + const newDataProvider = reformatDataProviderWithNewValue(dataProvider, ecsData); + if (newDataProvider.and != null && !isEmpty(newDataProvider.and)) { + newDataProvider.and = newDataProvider.and.map(andDataProvider => + reformatDataProviderWithNewValue(andDataProvider, ecsData) + ); + } + return newDataProvider; + }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx index 66003cf55fc383..6fd37215b9259b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx @@ -8,10 +8,25 @@ import { EuiPanel, EuiLoadingContent } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; import { ActionCreator } from 'typescript-fsa'; -import { SignalsUtilityBar } from './signals_utility_bar'; + +import { esFilters, esQuery } from '../../../../../../../../../src/plugins/data/common/es_query'; +import { Query } from '../../../../../../../../../src/plugins/data/common/query'; +import { useFetchIndexPatterns } from '../../../../containers/detection_engine/rules/fetch_index_patterns'; import { StatefulEventsViewer } from '../../../../components/events_viewer'; -import * as i18n from './translations'; +import { HeaderSection } from '../../../../components/header_section'; +import { DispatchUpdateTimeline } from '../../../../components/open_timeline/types'; +import { combineQueries } from '../../../../components/timeline/helpers'; +import { TimelineNonEcsData } from '../../../../graphql/types'; +import { useKibana } from '../../../../lib/kibana'; +import { inputsSelectors, State } from '../../../../store'; +import { InputsRange } from '../../../../store/inputs/model'; +import { timelineActions, timelineSelectors } from '../../../../store/timeline'; +import { timelineDefaults, TimelineModel } from '../../../../store/timeline/model'; +import { useApolloClient } from '../../../../utils/apollo_context'; + +import { updateSignalStatusAction } from './actions'; import { getSignalsActions, requiredFieldsForActions, @@ -19,36 +34,22 @@ import { signalsDefaultModel, signalsOpenFilters, } from './default_config'; -import { timelineActions, timelineSelectors } from '../../../../store/timeline'; -import { timelineDefaults, TimelineModel } from '../../../../store/timeline/model'; import { FILTER_CLOSED, FILTER_OPEN, SignalFilterOption, SignalsTableFilterGroup, } from './signals_filter_group'; -import { useKibana } from '../../../../lib/kibana'; -import { defaultHeaders } from '../../../../components/timeline/body/column_headers/default_headers'; -import { ColumnHeader } from '../../../../components/timeline/body/column_headers/column_header'; -import { esFilters, esQuery } from '../../../../../../../../../src/plugins/data/common/es_query'; -import { TimelineNonEcsData } from '../../../../graphql/types'; -import { inputsSelectors, SerializedFilterQuery, State } from '../../../../store'; -import { sendSignalsToTimelineAction, updateSignalStatusAction } from './actions'; +import { SignalsUtilityBar } from './signals_utility_bar'; +import * as i18n from './translations'; import { CreateTimelineProps, - SendSignalsToTimeline, SetEventsDeletedProps, SetEventsLoadingProps, UpdateSignalsStatus, UpdateSignalsStatusProps, } from './types'; -import { inputsActions } from '../../../../store/inputs'; -import { combineQueries } from '../../../../components/timeline/helpers'; -import { useFetchIndexPatterns } from '../../../../containers/detection_engine/rules/fetch_index_patterns'; -import { InputsRange } from '../../../../store/inputs/model'; -import { Query } from '../../../../../../../../../src/plugins/data/common/query'; - -import { HeaderSection } from '../../../../components/header_section'; +import { dispatchUpdateTimeline } from '../../../../components/open_timeline/helpers'; const SIGNALS_PAGE_TIMELINE_ID = 'signals-page'; @@ -62,23 +63,9 @@ interface ReduxProps { } interface DispatchProps { - createTimeline: ActionCreator<{ - dateRange?: { - start: number; - end: number; - }; - filters?: esFilters.Filter[]; - id: string; - kqlQuery?: { - filterQuery: SerializedFilterQuery | null; - }; - columns: ColumnHeader[]; - show?: boolean; - }>; clearEventsDeleted?: ActionCreator<{ id: string }>; clearEventsLoading?: ActionCreator<{ id: string }>; clearSelected?: ActionCreator<{ id: string }>; - removeTimelineLinkTo: ActionCreator<{}>; setEventsDeleted?: ActionCreator<{ id: string; eventIds: string[]; @@ -89,6 +76,8 @@ interface DispatchProps { eventIds: string[]; isLoading: boolean; }>; + updateTimelineIsLoading: ActionCreator<{ id: string; isLoading: boolean }>; + updateTimeline: DispatchUpdateTimeline; } interface OwnProps { @@ -106,7 +95,6 @@ type SignalsTableComponentProps = OwnProps & ReduxProps & DispatchProps; export const SignalsTableComponent = React.memo( ({ canUserCRUD, - createTimeline, clearEventsDeleted, clearEventsLoading, clearSelected, @@ -118,14 +106,16 @@ export const SignalsTableComponent = React.memo( isSelectAllChecked, loading, loadingEventIds, - removeTimelineLinkTo, selectedEventIds, setEventsDeleted, setEventsLoading, signalsIndex, to, + updateTimeline, + updateTimelineIsLoading, }) => { const [selectAll, setSelectAll] = useState(false); + const apolloClient = useApolloClient(); const [showClearSelectionAction, setShowClearSelectionAction] = useState(false); const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); @@ -148,15 +138,25 @@ export const SignalsTableComponent = React.memo( }); } return null; - }, [browserFields, globalFilters, globalQuery, indexPatterns, to, from]); + }, [browserFields, globalFilters, globalQuery, indexPatterns, kibana, to, from]); // Callback for creating a new timeline -- utilized by row/batch actions const createTimelineCallback = useCallback( - ({ id, kqlQuery, filters, dateRange }: CreateTimelineProps) => { - removeTimelineLinkTo({}); - createTimeline({ id, columns: defaultHeaders, show: true, filters, dateRange, kqlQuery }); + ({ from: fromTimeline, timeline, to: toTimeline }: CreateTimelineProps) => { + updateTimelineIsLoading({ id: 'timeline-1', isLoading: false }); + updateTimeline({ + duplicate: true, + from: fromTimeline, + id: 'timeline-1', + notes: [], + timeline: { + ...timeline, + show: true, + }, + to: toTimeline, + })(); }, - [createTimeline, removeTimelineLinkTo] + [updateTimeline, updateTimelineIsLoading] ); const setEventsLoadingCallback = useCallback( @@ -190,7 +190,7 @@ export const SignalsTableComponent = React.memo( clearSelected!({ id: SIGNALS_PAGE_TIMELINE_ID }); setFilterGroup(newFilterGroup); }, - [setFilterGroup] + [clearEventsLoading, clearEventsDeleted, clearSelected, setFilterGroup] ); // Callback for clearing entire selection from utility bar @@ -198,7 +198,7 @@ export const SignalsTableComponent = React.memo( clearSelected!({ id: SIGNALS_PAGE_TIMELINE_ID }); setSelectAll(false); setShowClearSelectionAction(false); - }, [clearSelected, setShowClearSelectionAction]); + }, [clearSelected, setSelectAll, setShowClearSelectionAction]); // Callback for selecting all events on all pages from utility bar // Dispatches to stateful_body's selectAll via TimelineTypeContext props @@ -206,7 +206,7 @@ export const SignalsTableComponent = React.memo( const selectAllCallback = useCallback(() => { setSelectAll(true); setShowClearSelectionAction(true); - }, [setShowClearSelectionAction]); + }, [setSelectAll, setShowClearSelectionAction]); const updateSignalsStatusCallback: UpdateSignalsStatus = useCallback( async ({ signalIds, status }: UpdateSignalsStatusProps) => { @@ -226,12 +226,6 @@ export const SignalsTableComponent = React.memo( showClearSelectionAction, ] ); - const sendSignalsToTimelineCallback: SendSignalsToTimeline = useCallback(async () => { - await sendSignalsToTimelineAction({ - createTimeline: createTimelineCallback, - data: Object.values(selectedEventIds), - }); - }, [selectedEventIds, setEventsDeletedCallback, setEventsLoadingCallback]); // Callback for creating the SignalUtilityBar which receives totalCount from EventsViewer component const utilityBarCallback = useCallback( @@ -245,7 +239,6 @@ export const SignalsTableComponent = React.memo( isFilteredToOpen={filterGroup === FILTER_OPEN} selectAll={selectAllCallback} selectedEventIds={selectedEventIds} - sendSignalsToTimeline={sendSignalsToTimelineCallback} showClearSelection={showClearSelectionAction} totalCount={totalCount} updateSignalsStatus={updateSignalsStatusCallback} @@ -261,6 +254,7 @@ export const SignalsTableComponent = React.memo( selectAllCallback, selectedEventIds, showClearSelectionAction, + updateSignalsStatusCallback, ] ); @@ -268,14 +262,25 @@ export const SignalsTableComponent = React.memo( const additionalActions = useMemo( () => getSignalsActions({ + apolloClient, canUserCRUD, hasIndexWrite, createTimeline: createTimelineCallback, setEventsLoading: setEventsLoadingCallback, setEventsDeleted: setEventsDeletedCallback, status: filterGroup === FILTER_OPEN ? FILTER_CLOSED : FILTER_OPEN, + updateTimelineIsLoading, }), - [canUserCRUD, createTimelineCallback, filterGroup] + [ + apolloClient, + canUserCRUD, + createTimelineCallback, + hasIndexWrite, + filterGroup, + setEventsLoadingCallback, + setEventsDeletedCallback, + updateTimelineIsLoading, + ] ); const defaultIndices = useMemo(() => [signalsIndex], [signalsIndex]); @@ -352,12 +357,33 @@ const makeMapStateToProps = () => { return mapStateToProps; }; -export const SignalsTable = connect(makeMapStateToProps, { - removeTimelineLinkTo: inputsActions.removeTimelineLinkTo, - clearSelected: timelineActions.clearSelected, - setEventsLoading: timelineActions.setEventsLoading, - clearEventsLoading: timelineActions.clearEventsLoading, - setEventsDeleted: timelineActions.setEventsDeleted, - clearEventsDeleted: timelineActions.clearEventsDeleted, - createTimeline: timelineActions.createTimeline, -})(SignalsTableComponent); +const mapDispatchToProps = (dispatch: Dispatch) => ({ + clearSelected: ({ id }: { id: string }) => dispatch(timelineActions.clearSelected({ id })), + setEventsLoading: ({ + id, + eventIds, + isLoading, + }: { + id: string; + eventIds: string[]; + isLoading: boolean; + }) => dispatch(timelineActions.setEventsLoading({ id, eventIds, isLoading })), + clearEventsLoading: ({ id }: { id: string }) => + dispatch(timelineActions.clearEventsLoading({ id })), + setEventsDeleted: ({ + id, + eventIds, + isDeleted, + }: { + id: string; + eventIds: string[]; + isDeleted: boolean; + }) => dispatch(timelineActions.setEventsDeleted({ id, eventIds, isDeleted })), + clearEventsDeleted: ({ id }: { id: string }) => + dispatch(timelineActions.clearEventsDeleted({ id })), + updateTimelineIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => + dispatch(timelineActions.updateIsLoading({ id, isLoading })), + updateTimeline: dispatchUpdateTimeline(dispatch), +}); + +export const SignalsTable = connect(makeMapStateToProps, mapDispatchToProps)(SignalsTableComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx index e28fb3e06870ee..0af3635d4c473b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { isEmpty } from 'lodash/fp'; import React, { useCallback } from 'react'; -import { EuiContextMenuPanel } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { UtilityBar, @@ -15,11 +15,11 @@ import { UtilityBarText, } from '../../../../../components/detection_engine/utility_bar'; import * as i18n from './translations'; -import { getBatchItems } from './batch_actions'; import { useUiSetting$ } from '../../../../../lib/kibana'; import { DEFAULT_NUMBER_FORMAT } from '../../../../../../common/constants'; import { TimelineNonEcsData } from '../../../../../graphql/types'; -import { SendSignalsToTimeline, UpdateSignalsStatus } from '../types'; +import { UpdateSignalsStatus } from '../types'; +import { FILTER_CLOSED, FILTER_OPEN } from '../signals_filter_group'; interface SignalsUtilityBarProps { canUserCRUD: boolean; @@ -29,7 +29,6 @@ interface SignalsUtilityBarProps { isFilteredToOpen: boolean; selectAll: () => void; selectedEventIds: Readonly>; - sendSignalsToTimeline: SendSignalsToTimeline; showClearSelection: boolean; totalCount: number; updateSignalsStatus: UpdateSignalsStatus; @@ -46,33 +45,15 @@ const SignalsUtilityBarComponent: React.FC = ({ selectAll, showClearSelection, updateSignalsStatus, - sendSignalsToTimeline, }) => { const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); - const getBatchItemsPopoverContent = useCallback( - (closePopover: () => void) => ( - - ), - [ - areEventsLoading, - selectedEventIds, - updateSignalsStatus, - sendSignalsToTimeline, - isFilteredToOpen, - hasIndexWrite, - ] - ); + const handleUpdateStatus = useCallback(async () => { + await updateSignalsStatus({ + signalIds: Object.keys(selectedEventIds), + status: isFilteredToOpen ? FILTER_CLOSED : FILTER_OPEN, + }); + }, [selectedEventIds, updateSignalsStatus, isFilteredToOpen]); const formattedTotalCount = numeral(totalCount).format(defaultNumberFormat); const formattedSelectedEventsCount = numeral(Object.keys(selectedEventIds).length).format( @@ -98,11 +79,13 @@ const SignalsUtilityBarComponent: React.FC = ({ - {i18n.BATCH_ACTIONS} + {isFilteredToOpen + ? i18n.BATCH_ACTION_CLOSE_SELECTED + : i18n.BATCH_ACTION_OPEN_SELECTED} void; -export interface SendSignalsToTimelineActionProps { +export interface SendSignalToTimelineActionProps { + apolloClient?: ApolloClient<{}>; createTimeline: CreateTimeline; - data: TimelineNonEcsData[][]; + ecsData: Ecs; + updateTimelineIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => void; } export interface CreateTimelineProps { - id: string; - kqlQuery?: { - filterQuery: SerializedFilterQuery | null; - filterQueryDraft: KueryFilterQuery | null; - }; - filters?: esFilters.Filter[]; - dateRange?: { start: number; end: number }; + from: number; + timeline: TimelineModel; + to: number; } -export type CreateTimeline = ({ id, kqlQuery, filters, dateRange }: CreateTimelineProps) => void; +export type CreateTimeline = ({ from, timeline, to }: CreateTimelineProps) => void; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx index e638cf89e77bbe..388f667f47fe12 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx @@ -128,9 +128,7 @@ const DetectionEngineComponent = React.memo( to={to} updateDateRange={updateDateRangeCallback} /> - - , truncateText: true, + width: '16%', }, { - field: 'lastCompletedRun', + field: 'statusDate', name: i18n.COLUMN_LAST_COMPLETE_RUN, - render: (value: TableData['lastCompletedRun']) => { + render: (value: TableData['statusDate']) => { return value == null ? ( getEmptyTagValue() ) : ( - + ); }, sortable: true, truncateText: true, - width: '16%', + width: '20%', }, { - field: 'lastResponse', + field: 'status', name: i18n.COLUMN_LAST_RESPONSE, - render: (value: TableData['lastResponse']) => { - return value == null ? ( - getEmptyTagValue() - ) : ( + render: (value: TableData['status']) => { + const color = + value == null + ? 'subdued' + : value === 'succeeded' + ? 'success' + : value === 'failed' + ? 'danger' + : value === 'executing' + ? 'warning' + : 'subdued'; + return ( <> - {value.type === 'Fail' ? ( - - {value.type} - - ) : ( - {value.type} - )} + {value ?? getEmptyTagValue()} ); }, + width: '16%', truncateText: true, }, { field: 'tags', name: i18n.COLUMN_TAGS, render: (value: TableData['tags']) => ( -
- <> - {value.map((tag, i) => ( - - {tag} - - ))} - -
+ <> + {value.map((tag, i) => ( + + {tag} + + ))} + ), truncateText: true, width: '20%', diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts index b18938920082d7..9666b7a5688cf7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts @@ -36,6 +36,8 @@ export const formatRules = (rules: Rule[], selectedIds?: string[]): TableData[] }, tags: rule.tags ?? [], activate: rule.enabled, + status: rule.status ?? null, + statusDate: rule.status_date ?? null, sourceRule: rule, isLoading: selectedIds?.includes(rule.id) ?? false, })); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx index fa4bea319f8590..0ef104e6891df0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx @@ -25,6 +25,7 @@ interface ScheduleItemProps { dataTestSubj: string; idAria: string; isDisabled: boolean; + minimumValue?: number; } const timeTypeOptions = [ @@ -61,7 +62,13 @@ const MyEuiSelect = styled(EuiSelect)` width: auto; `; -export const ScheduleItem = ({ dataTestSubj, field, idAria, isDisabled }: ScheduleItemProps) => { +export const ScheduleItem = ({ + dataTestSubj, + field, + idAria, + isDisabled, + minimumValue = 0, +}: ScheduleItemProps) => { const [timeType, setTimeType] = useState('s'); const [timeVal, setTimeVal] = useState(0); const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx index b99201abe87777..92ca83b4a89d92 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx @@ -108,6 +108,7 @@ const StepScheduleRuleComponent: FC = ({ idAria: 'detectionEngineStepScheduleRuleFrom', isDisabled: isLoading, dataTestSubj: 'detectionEngineStepScheduleRuleFrom', + minimumValue: 1, }} /> diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts index 1e47d1a57facc2..725c4b31ea2a8e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts @@ -220,9 +220,9 @@ export const COLUMN_SEVERITY = i18n.translate( ); export const COLUMN_LAST_COMPLETE_RUN = i18n.translate( - 'xpack.siem.detectionEngine.rules.allRules.columns.lastCompletedRunTitle', + 'xpack.siem.detectionEngine.rules.allRules.columns.lastRunTitle', { - defaultMessage: 'Last completed run', + defaultMessage: 'Last run', } ); @@ -247,6 +247,19 @@ export const COLUMN_ACTIVATE = i18n.translate( } ); +export const COLUMN_STATUS = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.columns.currentStatusTitle', + { + defaultMessage: 'Current status', + } +); +export const NO_STATUS = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.columns.unknownStatusDescription', + { + defaultMessage: 'Unknown', + } +); + export const DEFINE_RULE = i18n.translate('xpack.siem.detectionEngine.rules.defineRuleTitle', { defaultMessage: 'Define rule', }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts index 3da294fc9b8454..5ae516dda5b389 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts @@ -43,6 +43,8 @@ export interface TableData { activate: boolean; isLoading: boolean; sourceRule: Rule; + status?: string | null; + statusDate?: string | null; } export enum RuleStep { diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/actions.ts b/x-pack/legacy/plugins/siem/public/store/timeline/actions.ts index f8d23ac72e202c..7af17a40da312e 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/actions.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/actions.ts @@ -15,7 +15,7 @@ import { } from '../../components/timeline/data_providers/data_provider'; import { KueryFilterQuery, SerializedFilterQuery } from '../model'; -import { KqlMode, TimelineModel } from './model'; +import { EventType, KqlMode, TimelineModel } from './model'; import { TimelineNonEcsData } from '../../graphql/types'; const actionCreator = actionCreatorFactory('x-pack/siem/local/timeline'); @@ -50,6 +50,7 @@ export const applyDeltaToColumnWidth = actionCreator<{ export const createTimeline = actionCreator<{ id: string; + dataProviders?: DataProvider[]; dateRange?: { start: number; end: number; @@ -241,3 +242,7 @@ export const setEventsDeleted = actionCreator<{ export const clearEventsDeleted = actionCreator<{ id: string; }>('CLEAR_TIMELINE_EVENTS_DELETED'); + +export const updateEventType = actionCreator<{ id: string; eventType: EventType }>( + 'UPDATE_EVENT_TYPE' +); diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic.test.ts b/x-pack/legacy/plugins/siem/public/store/timeline/epic.test.ts index 6e62ce8cb8b06c..1633f7320a18ba 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/epic.test.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/epic.test.ts @@ -88,6 +88,7 @@ describe('Epic Timeline', () => { deletedEventIds: [], description: '', eventIdToNoteIds: {}, + eventType: 'all', highlightedDropAndProviderId: '', historyIds: [], filters: [ @@ -227,6 +228,7 @@ describe('Epic Timeline', () => { start: 1572469587644, }, description: '', + eventType: 'all', filters: [ { exists: null, diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts b/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts index a9cf7cff812adc..7d8bb7591c04fd 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts @@ -49,6 +49,7 @@ import { removeColumn, removeProvider, updateColumns, + updateEventType, updateDataProviderEnabled, updateDataProviderExcluded, updateDataProviderKqlQuery, @@ -99,6 +100,7 @@ const timelineActionsType = [ updateDataProviderExcluded.type, updateDataProviderKqlQuery.type, updateDescription.type, + updateEventType.type, updateKqlMode.type, updateProviders.type, updateSort.type, @@ -248,6 +250,7 @@ const timelineInput: TimelineInput = { columns: null, dataProviders: null, description: null, + eventType: null, filters: null, kqlMode: null, kqlQuery: null, diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts b/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts index 1f79a38b9f5b75..d3dacb68d4cde6 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts @@ -17,7 +17,7 @@ import { } from '../../components/timeline/data_providers/data_provider'; import { KueryFilterQuery, SerializedFilterQuery } from '../model'; -import { KqlMode, timelineDefaults, TimelineModel } from './model'; +import { KqlMode, timelineDefaults, TimelineModel, EventType } from './model'; import { TimelineById, TimelineState } from './types'; import { TimelineNonEcsData } from '../../graphql/types'; @@ -130,6 +130,7 @@ export const addTimelineToStore = ({ interface AddNewTimelineParams { columns: ColumnHeader[]; + dataProviders?: DataProvider[]; dateRange?: { start: number; end: number; @@ -151,6 +152,7 @@ interface AddNewTimelineParams { /** Adds a new `Timeline` to the provided collection of `TimelineById` */ export const addNewTimeline = ({ columns, + dataProviders = [], dateRange = { start: 0, end: 0 }, filters = timelineDefaults.filters, id, @@ -167,6 +169,7 @@ export const addNewTimeline = ({ id, ...timelineDefaults, columns, + dataProviders, dateRange, filters, itemsPerPage, @@ -627,6 +630,28 @@ export const updateTimelineTitle = ({ }; }; +interface UpdateTimelineEventTypeParams { + id: string; + eventType: EventType; + timelineById: TimelineById; +} + +export const updateTimelineEventType = ({ + id, + eventType, + timelineById, +}: UpdateTimelineEventTypeParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + eventType, + }, + }; +}; + interface UpdateTimelineIsFavoriteParams { id: string; isFavorite: boolean; diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/model.ts b/x-pack/legacy/plugins/siem/public/store/timeline/model.ts index 517392857e4a67..d9f1bab1e0033c 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/model.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/model.ts @@ -15,7 +15,7 @@ import { KueryFilterQuery, SerializedFilterQuery } from '../model'; export const DEFAULT_PAGE_COUNT = 2; // Eui Pager will not render unless this is a minimum of 2 pages export type KqlMode = 'filter' | 'search'; - +export type EventType = 'all' | 'raw' | 'signal'; export interface TimelineModel { /** The columns displayed in the timeline */ columns: ColumnHeader[]; @@ -25,6 +25,8 @@ export interface TimelineModel { deletedEventIds: string[]; /** A summary of the events and notes in this timeline */ description: string; + /** Typoe of event you want to see in this timeline */ + eventType?: EventType; /** A map of events in this timeline to the chronologically ordered notes (in this timeline) associated with the event */ eventIdToNoteIds: Record; filters?: esFilters.Filter[]; @@ -92,6 +94,7 @@ export type SubsetTimelineModel = Readonly< | 'dataProviders' | 'deletedEventIds' | 'description' + | 'eventType' | 'eventIdToNoteIds' | 'highlightedDropAndProviderId' | 'historyIds' @@ -126,6 +129,7 @@ export const timelineDefaults: SubsetTimelineModel & Pick ({ + ...state, + timelineById: updateTimelineEventType({ id, eventType, timelineById: state.timelineById }), + })) .case(updateIsFavorite, (state, { id, isFavorite }) => ({ ...state, timelineById: updateTimelineIsFavorite({ id, isFavorite, timelineById: state.timelineById }), diff --git a/x-pack/legacy/plugins/siem/server/graphql/ecs/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/ecs/schema.gql.ts index 6ab6e41212f836..9f57155d4d1890 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/ecs/schema.gql.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/ecs/schema.gql.ts @@ -379,6 +379,44 @@ export const ecsSchema = gql` auth: AuthEcsFields } + type RuleField { + id: ToStringArray + rule_id: ToStringArray + false_positives: [String!]! + saved_id: ToStringArray + timeline_id: ToStringArray + timeline_title: ToStringArray + max_signals: ToNumberArray + risk_score: ToStringArray + output_index: ToStringArray + description: ToStringArray + from: ToStringArray + immutable: ToBooleanArray + index: ToStringArray + interval: ToStringArray + language: ToStringArray + query: ToStringArray + references: ToStringArray + severity: ToStringArray + tags: ToStringArray + threats: ToAny + type: ToStringArray + size: ToStringArray + to: ToStringArray + enabled: ToBooleanArray + filters: ToAny + created_at: ToStringArray + updated_at: ToStringArray + created_by: ToStringArray + updated_by: ToStringArray + version: ToStringArray + } + + type SignalField { + rule: RuleField + original_time: ToStringArray + } + type ECS { _id: String! _index: String @@ -390,6 +428,7 @@ export const ecsSchema = gql` geo: GeoEcsFields host: HostEcsFields network: NetworkEcsField + signal: SignalField source: SourceEcsFields suricata: SuricataEcsFields tls: TlsEcsFields diff --git a/x-pack/legacy/plugins/siem/server/graphql/index.ts b/x-pack/legacy/plugins/siem/server/graphql/index.ts index 762b9002a466da..60853e2ce7bed4 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/index.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/index.ts @@ -20,6 +20,7 @@ import { overviewSchema } from './overview'; import { dateSchema } from './scalar_date'; import { noteSchema } from './note'; import { pinnedEventSchema } from './pinned_event'; +import { toAnySchema } from './scalar_to_any'; import { toBooleanSchema } from './scalar_to_boolean_array'; import { toDateSchema } from './scalar_to_date_array'; import { toNumberSchema } from './scalar_to_number_array'; @@ -37,6 +38,7 @@ export const schemas = [ ecsSchema, eventsSchema, dateSchema, + toAnySchema, toNumberSchema, toDateSchema, toBooleanSchema, diff --git a/x-pack/legacy/plugins/siem/server/graphql/scalar_to_any/index.ts b/x-pack/legacy/plugins/siem/server/graphql/scalar_to_any/index.ts new file mode 100644 index 00000000000000..cad5aa53ff5335 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/graphql/scalar_to_any/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { createScalarToAnyValueResolvers } from './resolvers'; +export { toAnySchema } from './schema.gql'; diff --git a/x-pack/legacy/plugins/siem/server/graphql/scalar_to_any/resolvers.ts b/x-pack/legacy/plugins/siem/server/graphql/scalar_to_any/resolvers.ts new file mode 100644 index 00000000000000..1cbcfb5dd08dff --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/graphql/scalar_to_any/resolvers.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isObject } from 'lodash/fp'; +import { GraphQLScalarType, Kind } from 'graphql'; + +/* + * serialize: gets invoked when serializing the result to send it back to a client. + * + * parseValue: gets invoked to parse client input that was passed through variables. + * + * parseLiteral: gets invoked to parse client input that was passed inline in the query. + */ + +export const toAnyScalar = new GraphQLScalarType({ + name: 'Any', + description: 'Represents any type', + serialize(value): unknown { + if (value == null) { + return null; + } + try { + const maybeObj = JSON.parse(value); + if (isObject(maybeObj)) { + return maybeObj; + } else { + return value; + } + } catch (e) { + return value; + } + }, + parseValue(value) { + return value; + }, + parseLiteral(ast) { + switch (ast.kind) { + case Kind.BOOLEAN: + return ast.value; + case Kind.INT: + return ast.value; + case Kind.FLOAT: + return ast.value; + case Kind.STRING: + return ast.value; + case Kind.LIST: + return ast.values; + case Kind.OBJECT: + return ast.fields; + } + return null; + }, +}); + +export const createScalarToAnyValueResolvers = () => ({ + ToAny: toAnyScalar, +}); diff --git a/x-pack/legacy/plugins/siem/server/graphql/scalar_to_any/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/scalar_to_any/schema.gql.ts new file mode 100644 index 00000000000000..f0adde0945b5f0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/graphql/scalar_to_any/schema.gql.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import gql from 'graphql-tag'; + +export const toAnySchema = gql` + scalar ToAny +`; diff --git a/x-pack/legacy/plugins/siem/server/graphql/timeline/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/timeline/schema.gql.ts index f05c26de7f75c6..8b24cea0d6af92 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/timeline/schema.gql.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/timeline/schema.gql.ts @@ -129,6 +129,7 @@ export const timelineSchema = gql` columns: [ColumnHeaderInput!] dataProviders: [DataProviderInput!] description: String + eventType: String filters: [FilterTimelineInput!] kqlMode: String kqlQuery: SerializedFilterQueryInput @@ -223,6 +224,7 @@ export const timelineSchema = gql` dateRange: DateRangePickerResult description: String eventIdToNoteIds: [NoteResult!] + eventType: String favorite: [FavoriteTimelineResult!] filters: [FilterTimelineResult!] kqlMode: String diff --git a/x-pack/legacy/plugins/siem/server/graphql/types.ts b/x-pack/legacy/plugins/siem/server/graphql/types.ts index c610be2e0b4779..48ca32874dda24 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/types.ts @@ -124,6 +124,8 @@ export interface TimelineInput { description?: Maybe; + eventType?: Maybe; + filters?: Maybe; kqlMode?: Maybe; @@ -369,6 +371,8 @@ export type ToDateArray = string[] | string; export type ToBooleanArray = boolean[] | boolean; +export type ToAny = any; + export type EsValue = any; // ==================================================== @@ -787,6 +791,8 @@ export interface Ecs { network?: Maybe; + signal?: Maybe; + source?: Maybe; suricata?: Maybe; @@ -964,6 +970,74 @@ export interface NetworkEcsField { transport?: Maybe; } +export interface SignalField { + rule?: Maybe; + + original_time?: Maybe; +} + +export interface RuleField { + id?: Maybe; + + rule_id?: Maybe; + + false_positives: string[]; + + saved_id?: Maybe; + + timeline_id?: Maybe; + + timeline_title?: Maybe; + + max_signals?: Maybe; + + risk_score?: Maybe; + + output_index?: Maybe; + + description?: Maybe; + + from?: Maybe; + + immutable?: Maybe; + + index?: Maybe; + + interval?: Maybe; + + language?: Maybe; + + query?: Maybe; + + references?: Maybe; + + severity?: Maybe; + + tags?: Maybe; + + threats?: Maybe; + + type?: Maybe; + + size?: Maybe; + + to?: Maybe; + + enabled?: Maybe; + + filters?: Maybe; + + created_at?: Maybe; + + updated_at?: Maybe; + + created_by?: Maybe; + + updated_by?: Maybe; + + version?: Maybe; +} + export interface SuricataEcsFields { eve?: Maybe; } @@ -1850,6 +1924,8 @@ export interface TimelineResult { eventIdToNoteIds?: Maybe; + eventType?: Maybe; + favorite?: Maybe; filters?: Maybe; @@ -4165,6 +4241,8 @@ export namespace EcsResolvers { network?: NetworkResolver, TypeParent, TContext>; + signal?: SignalResolver, TypeParent, TContext>; + source?: SourceResolver, TypeParent, TContext>; suricata?: SuricataResolver, TypeParent, TContext>; @@ -4242,6 +4320,11 @@ export namespace EcsResolvers { Parent = Ecs, TContext = SiemContext > = Resolver; + export type SignalResolver< + R = Maybe, + Parent = Ecs, + TContext = SiemContext + > = Resolver; export type SourceResolver< R = Maybe, Parent = Ecs, @@ -4814,6 +4897,240 @@ export namespace NetworkEcsFieldResolvers { > = Resolver; } +export namespace SignalFieldResolvers { + export interface Resolvers { + rule?: RuleResolver, TypeParent, TContext>; + + original_time?: OriginalTimeResolver, TypeParent, TContext>; + } + + export type RuleResolver< + R = Maybe, + Parent = SignalField, + TContext = SiemContext + > = Resolver; + export type OriginalTimeResolver< + R = Maybe, + Parent = SignalField, + TContext = SiemContext + > = Resolver; +} + +export namespace RuleFieldResolvers { + export interface Resolvers { + id?: IdResolver, TypeParent, TContext>; + + rule_id?: RuleIdResolver, TypeParent, TContext>; + + false_positives?: FalsePositivesResolver; + + saved_id?: SavedIdResolver, TypeParent, TContext>; + + timeline_id?: TimelineIdResolver, TypeParent, TContext>; + + timeline_title?: TimelineTitleResolver, TypeParent, TContext>; + + max_signals?: MaxSignalsResolver, TypeParent, TContext>; + + risk_score?: RiskScoreResolver, TypeParent, TContext>; + + output_index?: OutputIndexResolver, TypeParent, TContext>; + + description?: DescriptionResolver, TypeParent, TContext>; + + from?: FromResolver, TypeParent, TContext>; + + immutable?: ImmutableResolver, TypeParent, TContext>; + + index?: IndexResolver, TypeParent, TContext>; + + interval?: IntervalResolver, TypeParent, TContext>; + + language?: LanguageResolver, TypeParent, TContext>; + + query?: QueryResolver, TypeParent, TContext>; + + references?: ReferencesResolver, TypeParent, TContext>; + + severity?: SeverityResolver, TypeParent, TContext>; + + tags?: TagsResolver, TypeParent, TContext>; + + threats?: ThreatsResolver, TypeParent, TContext>; + + type?: TypeResolver, TypeParent, TContext>; + + size?: SizeResolver, TypeParent, TContext>; + + to?: ToResolver, TypeParent, TContext>; + + enabled?: EnabledResolver, TypeParent, TContext>; + + filters?: FiltersResolver, TypeParent, TContext>; + + created_at?: CreatedAtResolver, TypeParent, TContext>; + + updated_at?: UpdatedAtResolver, TypeParent, TContext>; + + created_by?: CreatedByResolver, TypeParent, TContext>; + + updated_by?: UpdatedByResolver, TypeParent, TContext>; + + version?: VersionResolver, TypeParent, TContext>; + } + + export type IdResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; + export type RuleIdResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; + export type FalsePositivesResolver< + R = string[], + Parent = RuleField, + TContext = SiemContext + > = Resolver; + export type SavedIdResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; + export type TimelineIdResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; + export type TimelineTitleResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; + export type MaxSignalsResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; + export type RiskScoreResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; + export type OutputIndexResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; + export type DescriptionResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; + export type FromResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; + export type ImmutableResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; + export type IndexResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; + export type IntervalResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; + export type LanguageResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; + export type QueryResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; + export type ReferencesResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; + export type SeverityResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; + export type TagsResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; + export type ThreatsResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; + export type TypeResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; + export type SizeResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; + export type ToResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; + export type EnabledResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; + export type FiltersResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; + export type CreatedAtResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; + export type UpdatedAtResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; + export type CreatedByResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; + export type UpdatedByResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; + export type VersionResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; +} + export namespace SuricataEcsFieldsResolvers { export interface Resolvers { eve?: EveResolver, TypeParent, TContext>; @@ -7762,6 +8079,8 @@ export namespace TimelineResultResolvers { eventIdToNoteIds?: EventIdToNoteIdsResolver, TypeParent, TContext>; + eventType?: EventTypeResolver, TypeParent, TContext>; + favorite?: FavoriteResolver, TypeParent, TContext>; filters?: FiltersResolver, TypeParent, TContext>; @@ -7832,6 +8151,11 @@ export namespace TimelineResultResolvers { Parent = TimelineResult, TContext = SiemContext > = Resolver; + export type EventTypeResolver< + R = Maybe, + Parent = TimelineResult, + TContext = SiemContext + > = Resolver; export type FavoriteResolver< R = Maybe, Parent = TimelineResult, @@ -8779,6 +9103,9 @@ export interface ToDateArrayScalarConfig extends GraphQLScalarTypeConfig { name: 'ToBooleanArray'; } +export interface ToAnyScalarConfig extends GraphQLScalarTypeConfig { + name: 'ToAny'; +} export interface EsValueScalarConfig extends GraphQLScalarTypeConfig { name: 'EsValue'; } @@ -8825,6 +9152,8 @@ export type IResolvers = { EndgameEcsFields?: EndgameEcsFieldsResolvers.Resolvers; EventEcsFields?: EventEcsFieldsResolvers.Resolvers; NetworkEcsField?: NetworkEcsFieldResolvers.Resolvers; + SignalField?: SignalFieldResolvers.Resolvers; + RuleField?: RuleFieldResolvers.Resolvers; SuricataEcsFields?: SuricataEcsFieldsResolvers.Resolvers; SuricataEveData?: SuricataEveDataResolvers.Resolvers; SuricataAlertData?: SuricataAlertDataResolvers.Resolvers; @@ -8936,6 +9265,7 @@ export type IResolvers = { ToNumberArray?: GraphQLScalarType; ToDateArray?: GraphQLScalarType; ToBooleanArray?: GraphQLScalarType; + ToAny?: GraphQLScalarType; EsValue?: GraphQLScalarType; } & { [typeName: string]: never }; diff --git a/x-pack/legacy/plugins/siem/server/init_server.ts b/x-pack/legacy/plugins/siem/server/init_server.ts index 5ecbb51c6770dd..1f4f1b176497fd 100644 --- a/x-pack/legacy/plugins/siem/server/init_server.ts +++ b/x-pack/legacy/plugins/siem/server/init_server.ts @@ -19,6 +19,7 @@ import { createNoteResolvers } from './graphql/note'; import { createPinnedEventResolvers } from './graphql/pinned_event'; import { createOverviewResolvers } from './graphql/overview'; import { createScalarDateResolvers } from './graphql/scalar_date'; +import { createScalarToAnyValueResolvers } from './graphql/scalar_to_any'; import { createScalarToBooleanArrayValueResolvers } from './graphql/scalar_to_boolean_array'; import { createScalarToDateArrayValueResolvers } from './graphql/scalar_to_date_array'; import { createScalarToNumberArrayValueResolvers } from './graphql/scalar_to_number_array'; @@ -50,6 +51,7 @@ export const initServer = (libs: AppBackendLibs) => { createNetworkResolvers(libs) as IResolvers, createScalarDateResolvers() as IResolvers, createScalarToDateArrayValueResolvers() as IResolvers, + createScalarToAnyValueResolvers() as IResolvers, createScalarToBooleanArrayValueResolvers() as IResolvers, createScalarToNumberArrayValueResolvers() as IResolvers, createSourcesResolvers(libs) as IResolvers, diff --git a/x-pack/legacy/plugins/siem/server/lib/ecs_fields/index.ts b/x-pack/legacy/plugins/siem/server/lib/ecs_fields/index.ts index e0a9ab6f3c71fe..237e4193683766 100644 --- a/x-pack/legacy/plugins/siem/server/lib/ecs_fields/index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/ecs_fields/index.ts @@ -284,6 +284,40 @@ export const systemFieldsMap: Readonly> = { 'system.auth.ssh.method': 'system.auth.ssh.method', }; +export const signalFieldsMap: Readonly> = { + 'signal.original_time': 'signal.original_time', + 'signal.rule.id': 'signal.rule.id', + 'signal.rule.saved_id': 'signal.rule.saved_id', + 'signal.rule.timeline_id': 'signal.rule.timeline_id', + 'signal.rule.timeline_title': 'signal.rule.timeline_title', + 'signal.rule.output_index': 'signal.rule.output_index', + 'signal.rule.from': 'signal.rule.from', + 'signal.rule.index': 'signal.rule.index', + 'signal.rule.language': 'signal.rule.language', + 'signal.rule.query': 'signal.rule.query', + 'signal.rule.to': 'signal.rule.to', + 'signal.rule.filters': 'signal.rule.filters', + 'signal.rule.rule_id': 'signal.rule.rule_id', + 'signal.rule.false_positives': 'signal.rule.false_positives', + 'signal.rule.max_signals': 'signal.rule.max_signals', + 'signal.rule.risk_score': 'signal.rule.risk_score', + 'signal.rule.description': 'signal.rule.description', + 'signal.rule.name': 'signal.rule.name', + 'signal.rule.immutable': 'signal.rule.immutable', + 'signal.rule.references': 'signal.rule.references', + 'signal.rule.severity': 'signal.rule.severity', + 'signal.rule.tags': 'signal.rule.tags', + 'signal.rule.threats': 'signal.rule.threats', + 'signal.rule.type': 'signal.rule.type', + 'signal.rule.size': 'signal.rule.size', + 'signal.rule.enabled': 'signal.rule.enabled', + 'signal.rule.created_at': 'signal.rule.created_at', + 'signal.rule.updated_at': 'signal.rule.updated_at', + 'signal.rule.created_by': 'signal.rule.created_by', + 'signal.rule.updated_by': 'signal.rule.updated_by', + 'signal.rule.version': 'signal.rule.version', +}; + export const eventFieldsMap: Readonly> = { timestamp: '@timestamp', '@timestamp': '@timestamp', @@ -293,9 +327,11 @@ export const eventFieldsMap: Readonly> = { ...{ ...dnsFieldsMap }, ...{ ...endgameFieldsMap }, ...{ ...eventBaseFieldsMap }, + ...{ ...fileMap }, ...{ ...geoFieldsMap }, ...{ ...hostFieldsMap }, ...{ ...networkFieldsMap }, + ...{ ...signalFieldsMap }, ...{ ...sourceFieldsMap }, ...{ ...suricataFieldsMap }, ...{ ...systemFieldsMap }, @@ -305,5 +341,4 @@ export const eventFieldsMap: Readonly> = { ...{ ...userFieldsMap }, ...{ ...winlogFieldsMap }, ...{ ...processFieldsMap }, - ...{ ...fileMap }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.ts index d06f6c030b380a..38b95cc5772f2e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.ts @@ -103,6 +103,7 @@ export class ElasticsearchEventsAdapter implements EventsAdapter { response: [inspectStringifyObject(searchResponse)], }; const data = getDataFromHits(merge(sourceData, hitsData)); + return { data, inspect, diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/saved_object_mappings.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/saved_object_mappings.ts index 8c7275a86911bf..8fc12fd56a8f60 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/saved_object_mappings.ts +++ b/x-pack/legacy/plugins/siem/server/lib/timeline/saved_object_mappings.ts @@ -130,6 +130,9 @@ export const timelineSavedObjectMappings: { description: { type: 'text', }, + eventType: { + type: 'keyword', + }, favorite: { properties: { keySearch: { diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/types.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/types.ts index 72e5cd50af394e..d757ea8049bc18 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/timeline/types.ts @@ -136,6 +136,7 @@ export const SavedTimelineRuntimeType = runtimeTypes.partial({ columns: unionWithNullType(runtimeTypes.array(SavedColumnHeaderRuntimeType)), dataProviders: unionWithNullType(runtimeTypes.array(SavedDataProviderRuntimeType)), description: unionWithNullType(runtimeTypes.string), + eventType: unionWithNullType(runtimeTypes.string), favorite: unionWithNullType(runtimeTypes.array(SavedFavoriteRuntimeType)), filters: unionWithNullType(runtimeTypes.array(SavedFilterRuntimeType)), kqlMode: unionWithNullType(runtimeTypes.string),