From 70b6098cd19363af79d4bcc2898f5f2b27a680ca Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Sat, 14 Aug 2021 04:11:53 +0200 Subject: [PATCH] [TGrid] Alerts status update use RAC api (#108092) Co-authored-by: Devin Hurley --- .../src/alerts_as_data_rbac.ts | 2 +- .../server/alert_data_client/alerts_client.ts | 101 ++++++++---- .../alert_data_client/tests/get.test.ts | 10 +- .../alert_data_client/tests/update.test.ts | 14 +- .../server/routes/bulk_update_alerts.ts | 16 +- .../timeline_actions/alert_context_menu.tsx | 2 +- .../timeline_actions/use_alerts_actions.tsx | 10 +- .../components/take_action_dropdown/index.tsx | 3 + .../__snapshots__/index.test.tsx.snap | 2 + .../side_panel/event_details/footer.tsx | 1 + x-pack/plugins/timelines/common/constants.ts | 2 + .../components/t_grid/body/index.test.tsx | 2 + .../public/components/t_grid/body/index.tsx | 8 + .../public/components/t_grid/helpers.tsx | 84 ++++++++-- .../components/t_grid/integrated/index.tsx | 26 ++- .../components/t_grid/standalone/index.tsx | 33 +++- .../alert_status_bulk_actions.tsx | 8 +- .../public/container/use_update_alerts.ts | 29 ++-- .../hooks/use_status_bulk_action_items.tsx | 15 +- .../basic/tests/index.ts | 1 + .../basic/tests/update_rac_alerts.ts | 152 ++++++++++++++++++ .../rule_registry/alerts/data.json | 14 +- .../tests/basic/bulk_update_alerts.ts | 23 ++- 23 files changed, 465 insertions(+), 93 deletions(-) create mode 100644 x-pack/test/detection_engine_api_integration/basic/tests/update_rac_alerts.ts diff --git a/packages/kbn-rule-data-utils/src/alerts_as_data_rbac.ts b/packages/kbn-rule-data-utils/src/alerts_as_data_rbac.ts index 795d194ee8a92..476425487df1b 100644 --- a/packages/kbn-rule-data-utils/src/alerts_as_data_rbac.ts +++ b/packages/kbn-rule-data-utils/src/alerts_as_data_rbac.ts @@ -27,7 +27,7 @@ export const AlertConsumers = { SYNTHETICS: 'synthetics', } as const; export type AlertConsumers = typeof AlertConsumers[keyof typeof AlertConsumers]; -export type STATUS_VALUES = 'open' | 'acknowledged' | 'closed'; +export type STATUS_VALUES = 'open' | 'acknowledged' | 'closed' | 'in-progress'; // TODO: remove 'in-progress' after migration to 'acknowledged' export const mapConsumerToIndexName: Record = { apm: '.alerts-observability-apm', diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts index a67c03abe8b32..e78f5f6d51cd2 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts @@ -5,6 +5,7 @@ * 2.0. */ import Boom from '@hapi/boom'; +import { estypes } from '@elastic/elasticsearch'; import { PublicMethodsOf } from '@kbn/utility-types'; import { Filter, buildEsQuery, EsQueryConfig } from '@kbn/es-query'; import { decodeVersion, encodeHitVersion } from '@kbn/securitysolution-es-utils'; @@ -35,7 +36,7 @@ import { Logger, ElasticsearchClient, EcsEventOutcome } from '../../../../../src import { alertAuditEvent, operationAlertAuditActionMap } from './audit_events'; import { AuditLogger } from '../../../security/server'; import { - ALERT_STATUS, + ALERT_WORKFLOW_STATUS, ALERT_RULE_CONSUMER, ALERT_RULE_TYPE_ID, SPACE_IDS, @@ -50,16 +51,19 @@ const mapConsumerToIndexName: typeof mapConsumerToIndexNameTyped = mapConsumerTo // TODO: Fix typings https://github.com/elastic/kibana/issues/101776 type NonNullableProps = Omit & { [K in Props]-?: NonNullable }; -type AlertType = NonNullableProps< +type AlertType = { _index: string; _id: string } & NonNullableProps< ParsedTechnicalFields, typeof ALERT_RULE_TYPE_ID | typeof ALERT_RULE_CONSUMER | typeof SPACE_IDS >; -const isValidAlert = (source?: ParsedTechnicalFields): source is AlertType => { +const isValidAlert = (source?: estypes.SearchHit): source is AlertType => { return ( - source?.[ALERT_RULE_TYPE_ID] != null && - source?.[ALERT_RULE_CONSUMER] != null && - source?.[SPACE_IDS] != null + (source?._source?.[ALERT_RULE_TYPE_ID] != null && + source?._source?.[ALERT_RULE_CONSUMER] != null && + source?._source?.[SPACE_IDS] != null) || + (source?.fields?.[ALERT_RULE_TYPE_ID][0] != null && + source?.fields?.[ALERT_RULE_CONSUMER][0] != null && + source?.fields?.[SPACE_IDS][0] != null) ); }; export interface ConstructorOptions { @@ -80,7 +84,7 @@ export interface BulkUpdateOptions { ids: string[] | undefined | null; status: STATUS_VALUES; index: string; - query: string | undefined | null; + query: object | string | undefined | null; } interface GetAlertParams { @@ -90,7 +94,7 @@ interface GetAlertParams { interface SingleSearchAfterAndAudit { id: string | null | undefined; - query: string | null | undefined; + query: object | string | null | undefined; index?: string; operation: WriteOperations.Update | ReadOperations.Find | ReadOperations.Get; lastSortIds: Array | undefined; @@ -126,6 +130,15 @@ export class AlertsClient { }; } + private getAlertStatusFieldUpdate( + source: ParsedTechnicalFields | undefined, + status: STATUS_VALUES + ) { + return source?.[ALERT_WORKFLOW_STATUS] == null + ? { signal: { status } } + : { [ALERT_WORKFLOW_STATUS]: status }; + } + /** * Accepts an array of ES documents and executes ensureAuthorized for the given operation * @param items @@ -218,6 +231,7 @@ export class AlertsClient { const config = getEsQueryConfig(); let queryBody = { + fields: [ALERT_RULE_TYPE_ID, ALERT_RULE_CONSUMER, ALERT_WORKFLOW_STATUS, SPACE_IDS], query: await this.buildEsQueryWithAuthz(query, id, alertSpaceId, operation, config), sort: [ { @@ -245,7 +259,7 @@ export class AlertsClient { seq_no_primary_term: true, }); - if (!result?.body.hits.hits.every((hit) => isValidAlert(hit._source))) { + if (!result?.body.hits.hits.every((hit) => isValidAlert(hit))) { const errorMessage = `Invalid alert found with id of "${id}" or with query "${query}" and operation ${operation}`; this.logger.error(errorMessage); throw Boom.badData(errorMessage); @@ -307,19 +321,25 @@ export class AlertsClient { ); } - const bulkUpdateRequest = mgetRes.body.docs.flatMap((item) => [ - { - update: { - _index: item._index, - _id: item._id, + const bulkUpdateRequest = mgetRes.body.docs.flatMap((item) => { + const fieldToUpdate = this.getAlertStatusFieldUpdate(item?._source, status); + return [ + { + update: { + _index: item._index, + _id: item._id, + }, }, - }, - { - doc: { [ALERT_STATUS]: status }, - }, - ]); + { + doc: { + ...fieldToUpdate, + }, + }, + ]; + }); const bulkUpdateResponse = await this.esClient.bulk({ + refresh: 'wait_for', body: bulkUpdateRequest, }); return bulkUpdateResponse; @@ -330,7 +350,7 @@ export class AlertsClient { } private async buildEsQueryWithAuthz( - query: string | null | undefined, + query: object | string | null | undefined, id: string | null | undefined, alertSpaceId: string, operation: WriteOperations.Update | ReadOperations.Get | ReadOperations.Find, @@ -345,15 +365,33 @@ export class AlertsClient { }, operation ); - return buildEsQuery( + let esQuery; + if (id != null) { + esQuery = { query: `_id:${id}`, language: 'kuery' }; + } else if (typeof query === 'string') { + esQuery = { query, language: 'kuery' }; + } else if (query != null && typeof query === 'object') { + esQuery = []; + } + const builtQuery = buildEsQuery( undefined, - { query: query == null ? `_id:${id}` : query, language: 'kuery' }, + esQuery == null ? { query: ``, language: 'kuery' } : esQuery, [ (authzFilter as unknown) as Filter, ({ term: { [SPACE_IDS]: alertSpaceId } } as unknown) as Filter, ], config ); + if (query != null && typeof query === 'object') { + return { + ...builtQuery, + bool: { + ...builtQuery.bool, + must: [...builtQuery.bool.must, query], + }, + }; + } + return builtQuery; } catch (exc) { this.logger.error(exc); throw Boom.expectationFailed( @@ -373,7 +411,7 @@ export class AlertsClient { operation, }: { index: string; - query: string; + query: object | string; operation: WriteOperations.Update | ReadOperations.Find | ReadOperations.Get; }) { let lastSortIds; @@ -436,7 +474,7 @@ export class AlertsClient { // first search for the alert by id, then use the alert info to check if user has access to it const alert = await this.singleSearchAfterAndAudit({ id, - query: null, + query: undefined, index, operation: ReadOperations.Get, lastSortIds: undefined, @@ -476,14 +514,17 @@ export class AlertsClient { this.logger.error(errorMessage); throw Boom.notFound(errorMessage); } - + const fieldToUpdate = this.getAlertStatusFieldUpdate( + alert?.hits.hits[0]._source, + status as STATUS_VALUES + ); const { body: response } = await this.esClient.update({ ...decodeVersion(_version), id, index, body: { doc: { - [ALERT_STATUS]: status, + ...fieldToUpdate, }, }, refresh: 'wait_for', @@ -535,11 +576,11 @@ export class AlertsClient { refresh: true, body: { script: { - source: `if (ctx._source['${ALERT_STATUS}'] != null) { - ctx._source['${ALERT_STATUS}'] = '${status}' + source: `if (ctx._source['${ALERT_WORKFLOW_STATUS}'] != null) { + ctx._source['${ALERT_WORKFLOW_STATUS}'] = '${status}' } - if (ctx._source['signal.status'] != null) { - ctx._source['signal.status'] = '${status}' + if (ctx._source.signal != null && ctx._source.signal.status != null) { + ctx._source.signal.status = '${status}' }`, lang: 'painless', } as InlineScript, diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts index c8d0d18dfd37e..1a0628bf6e9a8 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts @@ -119,6 +119,12 @@ describe('get()', () => { Array [ Object { "body": Object { + "fields": Array [ + "kibana.alert.rule.rule_type_id", + "kibana.alert.rule.consumer", + "kibana.alert.workflow_status", + "kibana.space_ids", + ], "query": Object { "bool": Object { "filter": Array [ @@ -254,7 +260,7 @@ describe('get()', () => { await expect(alertsClient.get({ id: fakeAlertId, index: '.alerts-observability-apm' })).rejects .toThrowErrorMatchingInlineSnapshot(` - "Unable to retrieve alert details for alert with id of \\"myfakeid1\\" or with query \\"null\\" and operation get + "Unable to retrieve alert details for alert with id of \\"myfakeid1\\" or with query \\"undefined\\" and operation get Error: Error: Unauthorized for fake.rule and apm" `); @@ -281,7 +287,7 @@ describe('get()', () => { await expect( alertsClient.get({ id: 'NoxgpHkBqbdrfX07MqXV', index: '.alerts-observability-apm' }) ).rejects.toThrowErrorMatchingInlineSnapshot(` - "Unable to retrieve alert details for alert with id of \\"NoxgpHkBqbdrfX07MqXV\\" or with query \\"null\\" and operation get + "Unable to retrieve alert details for alert with id of \\"NoxgpHkBqbdrfX07MqXV\\" or with query \\"undefined\\" and operation get Error: Error: something went wrong" `); }); diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts index 0aaab20052716..4e084c2c028b1 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts @@ -7,7 +7,7 @@ import { ALERT_RULE_CONSUMER, - ALERT_STATUS, + ALERT_WORKFLOW_STATUS, SPACE_IDS, ALERT_RULE_TYPE_ID, } from '@kbn/rule-data-utils'; @@ -89,8 +89,8 @@ describe('update()', () => { _source: { [ALERT_RULE_TYPE_ID]: 'apm.error_rate', message: 'hello world 1', + [ALERT_WORKFLOW_STATUS]: 'open', [ALERT_RULE_CONSUMER]: 'apm', - [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, }, @@ -139,7 +139,7 @@ describe('update()', () => { Object { "body": Object { "doc": Object { - "${ALERT_STATUS}": "closed", + "${ALERT_WORKFLOW_STATUS}": "closed", }, }, "id": "1", @@ -175,8 +175,8 @@ describe('update()', () => { _source: { [ALERT_RULE_TYPE_ID]: 'apm.error_rate', message: 'hello world 1', + [ALERT_WORKFLOW_STATUS]: 'open', [ALERT_RULE_CONSUMER]: 'apm', - [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, }, @@ -249,7 +249,7 @@ describe('update()', () => { _source: { [ALERT_RULE_TYPE_ID]: fakeRuleTypeId, [ALERT_RULE_CONSUMER]: 'apm', - [ALERT_STATUS]: 'open', + [ALERT_WORKFLOW_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, }, @@ -330,8 +330,8 @@ describe('update()', () => { _source: { [ALERT_RULE_TYPE_ID]: 'apm.error_rate', message: 'hello world 1', + [ALERT_WORKFLOW_STATUS]: 'open', [ALERT_RULE_CONSUMER]: 'apm', - [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, }, @@ -391,7 +391,7 @@ describe('update()', () => { [ALERT_RULE_TYPE_ID]: 'apm.error_rate', message: 'hello world 1', [ALERT_RULE_CONSUMER]: 'apm', - [ALERT_STATUS]: 'open', + [ALERT_WORKFLOW_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, }, diff --git a/x-pack/plugins/rule_registry/server/routes/bulk_update_alerts.ts b/x-pack/plugins/rule_registry/server/routes/bulk_update_alerts.ts index 40f27f7eb7e23..afeddee5ff876 100644 --- a/x-pack/plugins/rule_registry/server/routes/bulk_update_alerts.ts +++ b/x-pack/plugins/rule_registry/server/routes/bulk_update_alerts.ts @@ -22,16 +22,26 @@ export const bulkUpdateAlertsRoute = (router: IRouter) body: buildRouteValidation( t.union([ t.strict({ - status: t.union([t.literal('open'), t.literal('closed')]), + status: t.union([ + t.literal('open'), + t.literal('closed'), + t.literal('in-progress'), // TODO: remove after migration to acknowledged + t.literal('acknowledged'), + ]), index: t.string, ids: t.array(t.string), query: t.undefined, }), t.strict({ - status: t.union([t.literal('open'), t.literal('closed')]), + status: t.union([ + t.literal('open'), + t.literal('closed'), + t.literal('in-progress'), // TODO: remove after migration to acknowledged + t.literal('acknowledged'), + ]), index: t.string, ids: t.undefined, - query: t.string, + query: t.union([t.object, t.string]), }), ]) ), diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index 2dae69fec43e1..ea451f424b430 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -53,7 +53,6 @@ const AlertContextMenuComponent: React.FC = ({ timelineId, }) => { const [isPopoverOpen, setPopover] = useState(false); - const ruleId = get(0, ecsRowData?.signal?.rule?.id); const ruleName = get(0, ecsRowData?.signal?.rule?.name); @@ -116,6 +115,7 @@ const AlertContextMenuComponent: React.FC = ({ const { actionItems } = useAlertsActions({ alertStatus, eventId: ecsRowData?._id, + indexName: ecsRowData?._index ?? '', timelineId, closePopover, }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx index 4fdebee6e1f4d..f06d671549357 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx @@ -27,9 +27,16 @@ interface Props { closePopover: () => void; eventId: string; timelineId: string; + indexName: string; } -export const useAlertsActions = ({ alertStatus, closePopover, eventId, timelineId }: Props) => { +export const useAlertsActions = ({ + alertStatus, + closePopover, + eventId, + timelineId, + indexName, +}: Props) => { const dispatch = useDispatch(); const [, dispatchToaster] = useStateToaster(); @@ -100,6 +107,7 @@ export const useAlertsActions = ({ alertStatus, closePopover, eventId, timelineI const actionItems = useStatusBulkActionItems({ eventIds: [eventId], currentStatus: alertStatus, + indexName, setEventsLoading, setEventsDeleted, onUpdateSuccess: onAlertStatusUpdateSuccess, diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx index bd0ec4e2e742e..f1187c007106a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx @@ -48,6 +48,7 @@ export const TakeActionDropdown = React.memo( onAddExceptionTypeClick, onAddIsolationStatusClick, refetch, + indexName, timelineId, }: { detailsData: TimelineEventsDetailsItem[] | null; @@ -57,6 +58,7 @@ export const TakeActionDropdown = React.memo( loadingEventDetails: boolean; nonEcsData?: TimelineNonEcsData[]; refetch: (() => void) | undefined; + indexName: string; onAddEventFilterClick: () => void; onAddExceptionTypeClick: (type: ExceptionListType) => void; onAddIsolationStatusClick: (action: 'isolateHost' | 'unisolateHost') => void; @@ -154,6 +156,7 @@ export const TakeActionDropdown = React.memo( const { actionItems } = useAlertsActions({ alertStatus: actionsData.alertStatus, eventId: actionsData.eventId, + indexName, timelineId, closePopover: closePopoverAndFlyout, }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap index 3b365306447b4..c8da44e648ff8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap @@ -487,6 +487,7 @@ Array [ diff --git a/x-pack/plugins/timelines/common/constants.ts b/x-pack/plugins/timelines/common/constants.ts index 8c4e1700a54dd..9ef20f3ef5a6f 100644 --- a/x-pack/plugins/timelines/common/constants.ts +++ b/x-pack/plugins/timelines/common/constants.ts @@ -13,3 +13,5 @@ export const DEFAULT_NUMBER_FORMAT = 'format:number:defaultPattern'; export const FILTER_OPEN: AlertStatus = 'open'; export const FILTER_CLOSED: AlertStatus = 'closed'; export const FILTER_IN_PROGRESS: AlertStatus = 'in-progress'; + +export const RAC_ALERTS_BULK_UPDATE_URL = '/internal/rac/alerts/bulk_update'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx index db5ec7646977d..c9a94eab0ff20 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx @@ -80,6 +80,8 @@ describe('Body', () => { leadingControlColumns: [], trailingControlColumns: [], filterStatus: 'open', + filterQuery: '', + indexNames: [''], refetch: jest.fn(), }; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx index 5fba7cff55e5c..e9051b72db5e5 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx @@ -74,6 +74,7 @@ interface OwnProps { activePage: number; additionalControls?: React.ReactNode; browserFields: BrowserFields; + filterQuery: string; data: TimelineItem[]; defaultCellActions?: TGridCellAction[]; id: string; @@ -90,6 +91,7 @@ interface OwnProps { filterStatus?: AlertStatus; unit?: (total: number) => React.ReactNode; onRuleChange?: () => void; + indexNames: string[]; refetch: Refetch; } @@ -225,6 +227,7 @@ export const BodyComponent = React.memo( activePage, additionalControls, browserFields, + filterQuery, columnHeaders, data, defaultCellActions, @@ -250,6 +253,7 @@ export const BodyComponent = React.memo( unit = basicUnit, leadingControlColumns = EMPTY_CONTROL_COLUMNS, trailingControlColumns = EMPTY_CONTROL_COLUMNS, + indexNames, refetch, }) => { const dispatch = useDispatch(); @@ -337,6 +341,8 @@ export const BodyComponent = React.memo( id={id} totalItems={totalItems} filterStatus={filterStatus} + query={filterQuery} + indexName={indexNames.join()} onActionSuccess={onAlertStatusActionSuccess} onActionFailure={onAlertStatusActionFailure} refetch={refetch} @@ -375,7 +381,9 @@ export const BodyComponent = React.memo( alertCountText, totalItems, filterStatus, + filterQuery, browserFields, + indexNames, columnHeaders, additionalControls, showBulkActions, diff --git a/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx index 58d92c5f47f29..5fe766077a74c 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx @@ -6,8 +6,10 @@ */ import type { Filter, EsQueryConfig, Query } from '@kbn/es-query'; +import { FilterStateStore } from '@kbn/es-query'; import { isEmpty, get } from 'lodash/fp'; import memoizeOne from 'memoize-one'; +import { ALERT_WORKFLOW_STATUS } from '@kbn/rule-data-utils'; import { elementOrChildrenHasFocus, getFocusedAriaColindexCell, @@ -15,7 +17,7 @@ import { handleSkipFocus, stopPropagationAndPreventDefault, } from '../../../common'; -import type { IIndexPattern } from '../../../../../../src/plugins/data/public'; +import { IIndexPattern } from '../../../../../../src/plugins/data/public'; import type { BrowserFields } from '../../../common/search_strategy/index_fields'; import { DataProviderType, EXISTS_OPERATOR } from '../../../common/types/timeline'; import type { DataProvider, DataProvidersAnd } from '../../../common/types/timeline'; @@ -133,6 +135,17 @@ export const buildGlobalQuery = (dataProviders: DataProvider[], browserFields: B return !index ? `(${queryMatch})` : `${globalQuery} or (${queryMatch})`; }, ''); +interface CombineQueries { + config: EsQueryConfig; + dataProviders: DataProvider[]; + indexPattern: IIndexPattern; + browserFields: BrowserFields; + filters: Filter[]; + kqlQuery: Query; + kqlMode: string; + isEventViewer?: boolean; +} + export const combineQueries = ({ config, dataProviders, @@ -142,16 +155,7 @@ export const combineQueries = ({ kqlQuery, kqlMode, isEventViewer, -}: { - config: EsQueryConfig; - dataProviders: DataProvider[]; - indexPattern: IIndexPattern; - browserFields: BrowserFields; - filters: Filter[]; - kqlQuery: Query; - kqlMode: string; - isEventViewer?: boolean; -}): { filterQuery: string } | null => { +}: CombineQueries): { filterQuery: string } | null => { const kuery: Query = { query: '', language: kqlQuery.language }; if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEmpty(filters) && !isEventViewer) { return null; @@ -184,6 +188,64 @@ export const combineQueries = ({ }; }; +export const buildCombinedQuery = (combineQueriesParams: CombineQueries) => { + const combinedQuery = combineQueries(combineQueriesParams); + return combinedQuery + ? { + filterQuery: replaceStatusField(combinedQuery!.filterQuery), + } + : null; +}; + +export const buildTimeRangeFilter = (from: string, to: string): Filter => + ({ + range: { + '@timestamp': { + gte: from, + lt: to, + format: 'strict_date_optional_time', + }, + }, + meta: { + type: 'range', + disabled: false, + negate: false, + alias: null, + key: '@timestamp', + params: { + gte: from, + lt: to, + format: 'strict_date_optional_time', + }, + }, + $state: { + store: FilterStateStore.APP_STATE, + }, + } as Filter); + +export const getCombinedFilterQuery = ({ + from, + to, + filters, + ...combineQueriesParams +}: CombineQueries & { from: string; to: string }): string => { + return replaceStatusField( + combineQueries({ + ...combineQueriesParams, + filters: [...filters, buildTimeRangeFilter(from, to)], + })!.filterQuery + ); +}; + +/** + * This function is a temporary patch to prevent queries using old `signal.status` field. + * @todo The `signal.status` field should not be queried anymore and + * must be replaced by `ALERT_WORKFLOW_STATUS` field name constant + * @deprecated + */ +const replaceStatusField = (query: string): string => + query.replaceAll('signal.status', ALERT_WORKFLOW_STATUS); + /** * The CSS class name of a "stateful event", which appears in both * the `Timeline` and the `Events Viewer` widget diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx index 8ce8427985213..7dc632c219bd8 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx @@ -37,7 +37,12 @@ import { } from '../../../../../../../src/plugins/data/public'; import { useDeepEqualSelector } from '../../../hooks/use_selector'; import { defaultHeaders } from '../body/column_headers/default_headers'; -import { calculateTotalPages, combineQueries, resolverIsShowing } from '../helpers'; +import { + calculateTotalPages, + buildCombinedQuery, + getCombinedFilterQuery, + resolverIsShowing, +} from '../helpers'; import { tGridActions, tGridSelectors } from '../../../store/t_grid'; import { useTimelineEvents } from '../../../container'; import { HeaderSection } from '../header_section'; @@ -203,7 +208,7 @@ const TGridIntegratedComponent: React.FC = ({ [globalFullScreen, justTitle, setGlobalFullScreen] ); - const combinedQueries = combineQueries({ + const combinedQueries = buildCombinedQuery({ config: esQuery.getEsQueryConfig(uiSettings), dataProviders, indexPattern, @@ -257,6 +262,21 @@ const TGridIntegratedComponent: React.FC = ({ data, }); + const filterQuery = useMemo(() => { + return getCombinedFilterQuery({ + config: esQuery.getEsQueryConfig(uiSettings), + dataProviders, + indexPattern, + browserFields, + filters, + kqlQuery: query, + kqlMode, + isEventViewer: true, + from: start, + to: end, + }); + }, [uiSettings, dataProviders, indexPattern, browserFields, filters, start, end, query, kqlMode]); + const totalCountMinusDeleted = useMemo( () => (totalCount > 0 ? totalCount - deletedEventIds.length : 0), [deletedEventIds.length, totalCount] @@ -315,6 +335,7 @@ const TGridIntegratedComponent: React.FC = ({ = ({ leadingControlColumns={leadingControlColumns} trailingControlColumns={trailingControlColumns} refetch={refetch} + indexNames={indexNames} />