diff --git a/x-pack/plugins/cases/server/client/alerts/get.test.ts b/x-pack/plugins/cases/server/client/alerts/get.test.ts new file mode 100644 index 0000000000000..be675ed20a5cb --- /dev/null +++ b/x-pack/plugins/cases/server/client/alerts/get.test.ts @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; +import { AlertService } from '../../services'; +import { CasesClientArgs } from '../types'; +import { getAlerts } from './get'; + +describe('getAlerts', () => { + const esClient = elasticsearchServiceMock.createElasticsearchClient(); + const logger = loggingSystemMock.create().get('case'); + let alertsService: AlertService; + + beforeEach(async () => { + alertsService = new AlertService(esClient, logger); + jest.clearAllMocks(); + }); + + const docs = [ + { + _index: '.internal.alerts-security.alerts-default-000001', + _id: 'c3869d546717e8c581add9cbf7d24578f34cd3e72cbc8d8b8e9a9330a899f70f', + _version: 2, + _seq_no: 255, + _primary_term: 1, + found: true, + _source: { + destination: { mac: 'ff:ff:ff:ff:ff:ff' }, + source: { bytes: 444, mac: '11:1f:1e:13:15:14', packets: 6 }, + ecs: { version: '8.0.0' }, + }, + }, + ]; + + esClient.mget.mockResolvedValue({ docs }); + + it('returns an empty array if the alert info are empty', async () => { + const clientArgs = { alertsService } as unknown as CasesClientArgs; + const res = await getAlerts([], clientArgs); + + expect(res).toEqual([]); + }); + + it('returns the alerts correctly', async () => { + const clientArgs = { alertsService } as unknown as CasesClientArgs; + const res = await getAlerts( + [ + { + index: '.internal.alerts-security.alerts-default-000001', + id: 'c3869d546717e8c581add9cbf7d24578f34cd3e72cbc8d8b8e9a9330a899f70f', + }, + ], + clientArgs + ); + + expect(res).toEqual([ + { + index: '.internal.alerts-security.alerts-default-000001', + id: 'c3869d546717e8c581add9cbf7d24578f34cd3e72cbc8d8b8e9a9330a899f70f', + destination: { mac: 'ff:ff:ff:ff:ff:ff' }, + source: { bytes: 444, mac: '11:1f:1e:13:15:14', packets: 6 }, + ecs: { version: '8.0.0' }, + }, + ]); + }); + + it('filters mget errors correctly', async () => { + esClient.mget.mockResolvedValue({ + docs: [ + ...docs, + { + error: { type: 'not-found', reason: 'an error' }, + _index: '.internal.alerts-security.alerts-default-000002', + _id: 'd3869d546717e8c581add9cbf7d24578f34cd3e72cbc8d8b8e9a9330a899f70f', + }, + ], + }); + const clientArgs = { alertsService } as unknown as CasesClientArgs; + + const res = await getAlerts( + [ + { + index: '.internal.alerts-security.alerts-default-000001', + id: 'c3869d546717e8c581add9cbf7d24578f34cd3e72cbc8d8b8e9a9330a899f70f', + }, + ], + clientArgs + ); + + expect(res).toEqual([ + { + index: '.internal.alerts-security.alerts-default-000001', + id: 'c3869d546717e8c581add9cbf7d24578f34cd3e72cbc8d8b8e9a9330a899f70f', + destination: { mac: 'ff:ff:ff:ff:ff:ff' }, + source: { bytes: 444, mac: '11:1f:1e:13:15:14', packets: 6 }, + ecs: { version: '8.0.0' }, + }, + ]); + }); + + it('filters docs without _source correctly', async () => { + esClient.mget.mockResolvedValue({ + docs: [ + ...docs, + { + _index: '.internal.alerts-security.alerts-default-000002', + _id: 'd3869d546717e8c581add9cbf7d24578f34cd3e72cbc8d8b8e9a9330a899f70f', + found: true, + }, + ], + }); + const clientArgs = { alertsService } as unknown as CasesClientArgs; + + const res = await getAlerts( + [ + { + index: '.internal.alerts-security.alerts-default-000001', + id: 'c3869d546717e8c581add9cbf7d24578f34cd3e72cbc8d8b8e9a9330a899f70f', + }, + ], + clientArgs + ); + + expect(res).toEqual([ + { + index: '.internal.alerts-security.alerts-default-000001', + id: 'c3869d546717e8c581add9cbf7d24578f34cd3e72cbc8d8b8e9a9330a899f70f', + destination: { mac: 'ff:ff:ff:ff:ff:ff' }, + source: { bytes: 444, mac: '11:1f:1e:13:15:14', packets: 6 }, + ecs: { version: '8.0.0' }, + }, + ]); + }); +}); diff --git a/x-pack/plugins/cases/server/client/alerts/get.ts b/x-pack/plugins/cases/server/client/alerts/get.ts index 831ab0b05ed17..6a3f09cf30a89 100644 --- a/x-pack/plugins/cases/server/client/alerts/get.ts +++ b/x-pack/plugins/cases/server/client/alerts/get.ts @@ -5,9 +5,17 @@ * 2.0. */ +import { MgetResponseItem, GetGetResult } from '@elastic/elasticsearch/lib/api/types'; import { CasesClientGetAlertsResponse } from './types'; import { CasesClientArgs } from '..'; import { AlertInfo } from '../../common/types'; +import { Alert } from '../../services/alerts'; + +function isAlert( + doc?: MgetResponseItem +): doc is Omit, '_source'> & { _source: Alert } { + return Boolean(doc && !('error' in doc) && '_source' in doc); +} export const getAlerts = async ( alertsInfo: AlertInfo[], @@ -23,7 +31,7 @@ export const getAlerts = async ( return []; } - return alerts.docs.map((alert) => ({ + return alerts.docs.filter(isAlert).map((alert) => ({ id: alert._id, index: alert._index, ...alert._source, diff --git a/x-pack/plugins/cases/server/services/alerts/index.test.ts b/x-pack/plugins/cases/server/services/alerts/index.test.ts index 02e58b3a4e5ac..24eef0a88fcad 100644 --- a/x-pack/plugins/cases/server/services/alerts/index.test.ts +++ b/x-pack/plugins/cases/server/services/alerts/index.test.ts @@ -12,15 +12,14 @@ import { elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mo describe('updateAlertsStatus', () => { const esClient = elasticsearchServiceMock.createElasticsearchClient(); const logger = loggingSystemMock.create().get('case'); + let alertService: AlertService; - describe('happy path', () => { - let alertService: AlertService; - - beforeEach(async () => { - alertService = new AlertService(esClient, logger); - jest.resetAllMocks(); - }); + beforeEach(async () => { + alertService = new AlertService(esClient, logger); + jest.clearAllMocks(); + }); + describe('happy path', () => { it('updates the status of the alert correctly', async () => { const args = [{ id: 'alert-id-1', index: '.siem-signals', status: CaseStatuses.closed }]; @@ -273,4 +272,68 @@ describe('updateAlertsStatus', () => { expect(esClient.updateByQuery).not.toHaveBeenCalled(); }); }); + + describe('getAlerts', () => { + const docs = [ + { + _index: '.internal.alerts-security.alerts-default-000001', + _id: 'c3869d546717e8c581add9cbf7d24578f34cd3e72cbc8d8b8e9a9330a899f70f', + _version: 2, + _seq_no: 255, + _primary_term: 1, + found: true, + _source: { + destination: { mac: 'ff:ff:ff:ff:ff:ff' }, + source: { bytes: 444, mac: '11:1f:1e:13:15:14', packets: 6 }, + ecs: { version: '8.0.0' }, + }, + }, + ]; + + esClient.mget.mockResolvedValue({ docs }); + + it('returns the alerts correctly', async () => { + const res = await alertService.getAlerts([ + { + index: '.internal.alerts-security.alerts-default-000001', + id: 'c3869d546717e8c581add9cbf7d24578f34cd3e72cbc8d8b8e9a9330a899f70f', + }, + ]); + + expect(esClient.mget).toHaveBeenCalledWith({ + body: { + docs: [ + { + _id: 'c3869d546717e8c581add9cbf7d24578f34cd3e72cbc8d8b8e9a9330a899f70f', + _index: '.internal.alerts-security.alerts-default-000001', + }, + ], + }, + }); + + expect(res).toEqual({ docs }); + }); + + it('returns undefined if the id is empty', async () => { + const res = await alertService.getAlerts([ + { + index: '.internal.alerts-security.alerts-default-000001', + id: '', + }, + ]); + + expect(res).toBe(undefined); + }); + + it('returns undefined if the index is empty', async () => { + const res = await alertService.getAlerts([ + { + index: '', + id: 'c3869d546717e8c581add9cbf7d24578f34cd3e72cbc8d8b8e9a9330a899f70f', + }, + ]); + + expect(res).toBe(undefined); + }); + }); }); diff --git a/x-pack/plugins/cases/server/services/alerts/index.ts b/x-pack/plugins/cases/server/services/alerts/index.ts index b219c50964d39..a83528d026fd0 100644 --- a/x-pack/plugins/cases/server/services/alerts/index.ts +++ b/x-pack/plugins/cases/server/services/alerts/index.ts @@ -13,6 +13,7 @@ import { ALERT_WORKFLOW_STATUS, STATUS_VALUES, } from '@kbn/rule-registry-plugin/common/technical_rule_data_field_names'; +import { MgetResponse } from '@elastic/elasticsearch/lib/api/types'; import { CaseStatuses } from '../../../common/api'; import { MAX_ALERTS_PER_CASE, MAX_CONCURRENT_SEARCHES } from '../../../common/constants'; import { createCaseError } from '../../common/error'; @@ -180,7 +181,7 @@ export class AlertService { ); } - public async getAlerts(alertsInfo: AlertInfo[]): Promise { + public async getAlerts(alertsInfo: AlertInfo[]): Promise | undefined> { try { const docs = alertsInfo .filter((alert) => !AlertService.isEmptyAlert(alert)) @@ -193,8 +194,7 @@ export class AlertService { const results = await this.scopedClusterClient.mget({ body: { docs } }); - // @ts-expect-error @elastic/elasticsearch _source is optional - return results.body; + return results; } catch (error) { throw createCaseError({ message: `Failed to retrieve alerts ids: ${JSON.stringify(alertsInfo)}: ${error}`, @@ -230,16 +230,12 @@ function updateIndexEntryWithStatus( } } -interface Alert { +export interface Alert { _id: string; _index: string; _source: Record; } -interface AlertsResponse { - docs: Alert[]; -} - interface AlertIdIndex { id: string; index: string;