diff --git a/x-pack/legacy/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts b/x-pack/legacy/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts index 0baa1ef7cdd05..ad59d3dc436a7 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts +++ b/x-pack/legacy/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts @@ -10,8 +10,7 @@ import { InfluencerInput, Anomalies, CriteriaFields } from '../types'; import { hasMlUserPermissions } from '../permissions/has_ml_user_permissions'; import { MlCapabilitiesContext } from '../permissions/ml_capabilities_provider'; import { useSiemJobs } from '../../ml_popover/hooks/use_siem_jobs'; -import { useStateToaster } from '../../toasters'; -import { errorToToaster } from '../api/error_to_toaster'; +import { useStateToaster, errorToToaster } from '../../toasters'; import * as i18n from './translations'; import { useTimeZone, useUiSetting$ } from '../../../lib/kibana'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts b/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts index 35dbbf012272e..b3876b28655b3 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts +++ b/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts @@ -5,7 +5,6 @@ */ import { Anomalies, InfluencerInput, CriteriaFields } from '../types'; -import { throwIfNotOk } from '../../../hooks/api/api'; import { KibanaServices } from '../../../lib/kibana'; export interface Body { @@ -22,17 +21,10 @@ export interface Body { } export const anomaliesTableData = async (body: Body, signal: AbortSignal): Promise => { - const response = await KibanaServices.get().http.fetch( - '/api/ml/results/anomalies_table_data', - { - method: 'POST', - body: JSON.stringify(body), - asResponse: true, - asSystemRequest: true, - signal, - } - ); - - await throwIfNotOk(response.response); - return response.body!; + return KibanaServices.get().http.fetch('/api/ml/results/anomalies_table_data', { + method: 'POST', + body: JSON.stringify(body), + asSystemRequest: true, + signal, + }); }; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/error_to_toaster.ts b/x-pack/legacy/plugins/siem/public/components/ml/api/error_to_toaster.ts deleted file mode 100644 index b341016fff6ef..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/ml/api/error_to_toaster.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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 { isError } from 'lodash/fp'; -import uuid from 'uuid'; -import { ActionToaster, AppToast } from '../../toasters'; -import { ToasterErrorsType, ToasterErrors } from '../../../hooks/api/throw_if_not_ok'; - -export type ErrorToToasterArgs = Partial & { - error: unknown; - dispatchToaster: React.Dispatch; -}; - -export const errorToToaster = ({ - id = uuid.v4(), - title, - error, - color = 'danger', - iconType = 'alert', - dispatchToaster, -}: ErrorToToasterArgs) => { - if (isToasterError(error)) { - const toast: AppToast = { - id, - title, - color, - iconType, - errors: error.messages, - }; - dispatchToaster({ - type: 'addToaster', - toast, - }); - } else if (isAnError(error)) { - const toast: AppToast = { - id, - title, - color, - iconType, - errors: [error.message], - }; - dispatchToaster({ - type: 'addToaster', - toast, - }); - } else { - const toast: AppToast = { - id, - title, - color, - iconType, - errors: ['Network Error'], - }; - dispatchToaster({ - type: 'addToaster', - toast, - }); - } -}; - -export const isAnError = (error: unknown): error is Error => isError(error); - -export const isToasterError = (error: unknown): error is ToasterErrorsType => - error instanceof ToasterErrors; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/errors.ts b/x-pack/legacy/plugins/siem/public/components/ml/api/errors.ts new file mode 100644 index 0000000000000..f117b92c7106e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/ml/api/errors.ts @@ -0,0 +1,20 @@ +/* + * 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 { has } from 'lodash/fp'; + +import { MlError } from '../types'; + +export interface MlStartJobError { + error: MlError; + started: boolean; +} + +// use the "in operator" and regular type guards to do a narrow once this issue is fixed below: +// https://github.com/microsoft/TypeScript/issues/21732 +// Otherwise for now, has will work ok even though it casts 'unknown' to 'any' +export const isMlStartJobError = (value: unknown): value is MlStartJobError => + has('error.msg', value) && has('error.response', value) && has('error.statusCode', value); diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts b/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts index feafbba2024dc..e69abc1a86e0e 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts +++ b/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts @@ -5,7 +5,6 @@ */ import { InfluencerInput, MlCapabilities } from '../types'; -import { throwIfNotOk } from '../../../hooks/api/api'; import { KibanaServices } from '../../../lib/kibana'; export interface Body { @@ -22,16 +21,9 @@ export interface Body { } export const getMlCapabilities = async (signal: AbortSignal): Promise => { - const response = await KibanaServices.get().http.fetch( - '/api/ml/ml_capabilities', - { - method: 'GET', - asResponse: true, - asSystemRequest: true, - signal, - } - ); - - await throwIfNotOk(response.response); - return response.body!; + return KibanaServices.get().http.fetch('/api/ml/ml_capabilities', { + method: 'GET', + asSystemRequest: true, + signal, + }); }; diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/throw_if_not_ok.test.ts b/x-pack/legacy/plugins/siem/public/components/ml/api/throw_if_not_ok.test.ts similarity index 96% rename from x-pack/legacy/plugins/siem/public/hooks/api/throw_if_not_ok.test.ts rename to x-pack/legacy/plugins/siem/public/components/ml/api/throw_if_not_ok.test.ts index bc0c765d6f2df..486b0f7e77412 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/api/throw_if_not_ok.test.ts +++ b/x-pack/legacy/plugins/siem/public/components/ml/api/throw_if_not_ok.test.ts @@ -5,48 +5,21 @@ */ import fetchMock from 'fetch-mock'; + +import { ToasterError } from '../../toasters'; +import { SetupMlResponse } from '../../ml_popover/types'; +import { isMlStartJobError } from './errors'; import { - isMlStartJobError, - MessageBody, - parseJsonFromBody, throwIfErrorAttached, throwIfErrorAttachedToSetup, - ToasterErrors, tryParseResponse, } from './throw_if_not_ok'; -import { SetupMlResponse } from '../../components/ml_popover/types'; describe('throw_if_not_ok', () => { afterEach(() => { fetchMock.reset(); }); - describe('#parseJsonFromBody', () => { - test('parses a json from the body correctly', async () => { - fetchMock.mock('http://example.com', { - status: 500, - body: { - error: 'some error', - statusCode: 500, - message: 'I am a custom message', - }, - }); - const response = await fetch('http://example.com'); - const expected: MessageBody = { - error: 'some error', - statusCode: 500, - message: 'I am a custom message', - }; - await expect(parseJsonFromBody(response)).resolves.toEqual(expected); - }); - - test('returns null if the body does not exist', async () => { - fetchMock.mock('http://example.com', { status: 500, body: 'some text' }); - const response = await fetch('http://example.com'); - await expect(parseJsonFromBody(response)).resolves.toEqual(null); - }); - }); - describe('#tryParseResponse', () => { test('It formats a JSON object', () => { const parsed = tryParseResponse(JSON.stringify({ hello: 'how are you?' })); @@ -119,7 +92,7 @@ describe('throw_if_not_ok', () => { }, }; expect(() => throwIfErrorAttached(json, ['some-id'])).toThrow( - new ToasterErrors(['some message']) + new ToasterError(['some message']) ); }); diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/throw_if_not_ok.ts b/x-pack/legacy/plugins/siem/public/components/ml/api/throw_if_not_ok.ts similarity index 63% rename from x-pack/legacy/plugins/siem/public/hooks/api/throw_if_not_ok.ts rename to x-pack/legacy/plugins/siem/public/components/ml/api/throw_if_not_ok.ts index 7d70106b0e562..4e92cbc76c933 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/api/throw_if_not_ok.ts +++ b/x-pack/legacy/plugins/siem/public/components/ml/api/throw_if_not_ok.ts @@ -4,32 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { has } from 'lodash/fp'; - -import * as i18n from '../../components/ml/api/translations'; -import { MlError } from '../../components/ml/types'; -import { SetupMlResponse } from '../../components/ml_popover/types'; - -export { MessageBody, parseJsonFromBody } from '../../utils/api'; - -export interface MlStartJobError { - error: MlError; - started: boolean; -} - -export type ToasterErrorsType = Error & { - messages: string[]; -}; - -export class ToasterErrors extends Error implements ToasterErrorsType { - public messages: string[]; - - constructor(messages: string[]) { - super(messages[0]); - this.name = 'ToasterErrors'; - this.messages = messages; - } -} +import * as i18n from './translations'; +import { ToasterError } from '../../toasters'; +import { SetupMlResponse } from '../../ml_popover/types'; +import { isMlStartJobError } from './errors'; export const tryParseResponse = (response: string): string => { try { @@ -71,7 +49,7 @@ export const throwIfErrorAttachedToSetup = ( const errors = [...jobErrors, ...dataFeedErrors]; if (errors.length > 0) { - throw new ToasterErrors(errors); + throw new ToasterError(errors); } }; @@ -93,12 +71,6 @@ export const throwIfErrorAttached = ( } }, []); if (errors.length > 0) { - throw new ToasterErrors(errors); + throw new ToasterError(errors); } }; - -// use the "in operator" and regular type guards to do a narrow once this issue is fixed below: -// https://github.com/microsoft/TypeScript/issues/21732 -// Otherwise for now, has will work ok even though it casts 'unknown' to 'any' -export const isMlStartJobError = (value: unknown): value is MlStartJobError => - has('error.msg', value) && has('error.response', value) && has('error.statusCode', value); diff --git a/x-pack/legacy/plugins/siem/public/components/ml/permissions/ml_capabilities_provider.tsx b/x-pack/legacy/plugins/siem/public/components/ml/permissions/ml_capabilities_provider.tsx index cae05e26b115b..eee44abb44204 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/permissions/ml_capabilities_provider.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/permissions/ml_capabilities_provider.tsx @@ -9,8 +9,7 @@ import React, { useState, useEffect } from 'react'; import { MlCapabilities } from '../types'; import { getMlCapabilities } from '../api/get_ml_capabilities'; import { emptyMlCapabilities } from '../empty_ml_capabilities'; -import { errorToToaster } from '../api/error_to_toaster'; -import { useStateToaster } from '../../toasters'; +import { errorToToaster, useStateToaster } from '../../toasters'; import * as i18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx index 1ab996f88515b..f2bc7ca64565b 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx @@ -17,8 +17,7 @@ import { StartDatafeedResponse, StopDatafeedResponse, } from './types'; -import { throwIfErrorAttached, throwIfErrorAttachedToSetup } from '../../hooks/api/throw_if_not_ok'; -import { throwIfNotOk } from '../../hooks/api/api'; +import { throwIfErrorAttached, throwIfErrorAttachedToSetup } from '../ml/api/throw_if_not_ok'; import { KibanaServices } from '../../lib/kibana'; /** @@ -26,45 +25,36 @@ import { KibanaServices } from '../../lib/kibana'; * * @param indexPatternName ES index pattern to check for compatible modules * @param signal to cancel request + * + * @throws An error if response is not OK */ export const checkRecognizer = async ({ indexPatternName, signal, -}: CheckRecognizerProps): Promise => { - const response = await KibanaServices.get().http.fetch( +}: CheckRecognizerProps): Promise => + KibanaServices.get().http.fetch( `/api/ml/modules/recognize/${indexPatternName}`, { method: 'GET', - asResponse: true, asSystemRequest: true, signal, } ); - await throwIfNotOk(response.response); - return response.body!; -}; - /** * Returns ML Module for given moduleId. Returns all modules if no moduleId specified * * @param moduleId id of the module to retrieve * @param signal to cancel request + * + * @throws An error if response is not OK */ -export const getModules = async ({ moduleId = '', signal }: GetModulesProps): Promise => { - const response = await KibanaServices.get().http.fetch( - `/api/ml/modules/get_module/${moduleId}`, - { - method: 'GET', - asResponse: true, - asSystemRequest: true, - signal, - } - ); - - await throwIfNotOk(response.response); - return response.body!; -}; +export const getModules = async ({ moduleId = '', signal }: GetModulesProps): Promise => + KibanaServices.get().http.fetch(`/api/ml/modules/get_module/${moduleId}`, { + method: 'GET', + asSystemRequest: true, + signal, + }); /** * Creates ML Jobs + Datafeeds for the given configTemplate + indexPatternName @@ -74,6 +64,8 @@ export const getModules = async ({ moduleId = '', signal }: GetModulesProps): Pr * @param jobIdErrorFilter - if provided, filters all errors except for given jobIds * @param groups - list of groups to add to jobs being installed * @param prefix - prefix to be added to job name + * + * @throws An error if response is not OK */ export const setupMlJob = async ({ configTemplate, @@ -93,16 +85,12 @@ export const setupMlJob = async ({ startDatafeed: false, useDedicatedIndex: true, }), - asResponse: true, asSystemRequest: true, } ); - await throwIfNotOk(response.response); - const json = response.body!; - throwIfErrorAttachedToSetup(json, jobIdErrorFilter); - - return json; + throwIfErrorAttachedToSetup(response, jobIdErrorFilter); + return response; }; /** @@ -110,6 +98,8 @@ export const setupMlJob = async ({ * * @param datafeedIds * @param start + * + * @throws An error if response is not OK */ export const startDatafeeds = async ({ datafeedIds, @@ -126,22 +116,20 @@ export const startDatafeeds = async ({ datafeedIds, ...(start !== 0 && { start }), }), - asResponse: true, asSystemRequest: true, } ); - await throwIfNotOk(response.response); - const json = response.body!; - throwIfErrorAttached(json, datafeedIds); - - return json; + throwIfErrorAttached(response, datafeedIds); + return response; }; /** * Stops the given dataFeedIds and sets the corresponding Job's jobState to closed * * @param datafeedIds + * + * @throws An error if response is not OK */ export const stopDatafeeds = async ({ datafeedIds, @@ -155,14 +143,10 @@ export const stopDatafeeds = async ({ body: JSON.stringify({ datafeedIds, }), - asResponse: true, asSystemRequest: true, } ); - await throwIfNotOk(stopDatafeedsResponse.response); - const stopDatafeedsResponseJson = stopDatafeedsResponse.body!; - const datafeedPrefix = 'datafeed-'; const closeJobsResponse = await KibanaServices.get().http.fetch( '/api/ml/jobs/close_jobs', @@ -175,13 +159,11 @@ export const stopDatafeeds = async ({ : dataFeedId ), }), - asResponse: true, asSystemRequest: true, } ); - await throwIfNotOk(closeJobsResponse.response); - return [stopDatafeedsResponseJson, closeJobsResponse.body!]; + return [stopDatafeedsResponse, closeJobsResponse]; }; /** @@ -191,19 +173,13 @@ export const stopDatafeeds = async ({ * return a 500 * * @param signal to cancel request + * + * @throws An error if response is not OK */ -export const getJobsSummary = async (signal: AbortSignal): Promise => { - const response = await KibanaServices.get().http.fetch( - '/api/ml/jobs/jobs_summary', - { - method: 'POST', - body: JSON.stringify({}), - asResponse: true, - asSystemRequest: true, - signal, - } - ); - - await throwIfNotOk(response.response); - return response.body!; -}; +export const getJobsSummary = async (signal: AbortSignal): Promise => + KibanaServices.get().http.fetch('/api/ml/jobs/jobs_summary', { + method: 'POST', + body: JSON.stringify({}), + asSystemRequest: true, + signal, + }); diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx index 9df93d087e166..4e4cdbfc109a9 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx @@ -10,8 +10,7 @@ import { checkRecognizer, getJobsSummary, getModules } from '../api'; import { SiemJob } from '../types'; import { hasMlUserPermissions } from '../../ml/permissions/has_ml_user_permissions'; import { MlCapabilitiesContext } from '../../ml/permissions/ml_capabilities_provider'; -import { useStateToaster } from '../../toasters'; -import { errorToToaster } from '../../ml/api/error_to_toaster'; +import { errorToToaster, useStateToaster } from '../../toasters'; import { useUiSetting$ } from '../../../lib/kibana'; import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.tsx index a41e84c163663..ec924889f93f1 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.tsx @@ -12,10 +12,9 @@ import styled from 'styled-components'; import { useKibana } from '../../lib/kibana'; import { METRIC_TYPE, TELEMETRY_EVENT, track } from '../../lib/telemetry'; -import { errorToToaster } from '../ml/api/error_to_toaster'; import { hasMlAdminPermissions } from '../ml/permissions/has_ml_admin_permissions'; import { MlCapabilitiesContext } from '../ml/permissions/ml_capabilities_provider'; -import { useStateToaster } from '../toasters'; +import { errorToToaster, useStateToaster } from '../toasters'; import { setupMlJob, startDatafeeds, stopDatafeeds } from './api'; import { filterJobs } from './helpers'; import { useSiemJobs } from './hooks/use_siem_jobs'; diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/helpers.test.ts b/x-pack/legacy/plugins/siem/public/components/news_feed/helpers.test.ts index 0b0e0298a0c66..e7cd03d098da8 100644 --- a/x-pack/legacy/plugins/siem/public/components/news_feed/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/components/news_feed/helpers.test.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { KibanaServices } from '../../lib/kibana'; import { NEWS_FEED_URL_SETTING_DEFAULT } from '../../../common/constants'; import { rawNewsApiResponse } from '../../mock/news'; import { rawNewsJSON } from '../../mock/raw_news'; @@ -18,7 +19,7 @@ import { } from './helpers'; import { NewsItem, RawNewsApiResponse } from './types'; -type GlobalWithFetch = NodeJS.Global & { fetch: jest.Mock }; +jest.mock('../../lib/kibana'); describe('helpers', () => { describe('removeSnapshotFromVersion', () => { @@ -390,37 +391,19 @@ describe('helpers', () => { }); describe('fetchNews', () => { - const newsFeedUrl = 'https://feeds.elastic.co/security-solution/v8.0.0.json'; + const mockKibanaServices = KibanaServices.get as jest.Mock; + const fetchMock = jest.fn(); + mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); - afterAll(() => { - delete (global as GlobalWithFetch).fetch; + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(rawNewsApiResponse); }); test('it returns the raw API response from the news feed', async () => { - (global as GlobalWithFetch).fetch = jest.fn().mockImplementationOnce(() => - Promise.resolve({ - ok: true, - json: () => { - return rawNewsApiResponse; - }, - }) - ); - + const newsFeedUrl = 'https://feeds.elastic.co/security-solution/v8.0.0.json'; expect(await fetchNews({ newsFeedUrl })).toEqual(rawNewsApiResponse); }); - - test('it throws if the response from the news feed is not ok', async () => { - (global as GlobalWithFetch).fetch = jest.fn().mockImplementationOnce(() => - Promise.resolve({ - ok: false, - json: () => { - return rawNewsApiResponse; - }, - }) - ); - - await expect(fetchNews({ newsFeedUrl })).rejects.toThrow('Network Error: undefined'); - }); }); describe('showNewsItem', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/helpers.ts b/x-pack/legacy/plugins/siem/public/components/news_feed/helpers.ts index 0cb5015872dec..838a8a8c41262 100644 --- a/x-pack/legacy/plugins/siem/public/components/news_feed/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/news_feed/helpers.ts @@ -9,7 +9,7 @@ import moment from 'moment'; import uuid from 'uuid'; import { NewsItem, RawNewsApiItem, RawNewsApiResponse } from './types'; -import { throwIfNotOk } from '../../hooks/api/api'; +import { KibanaServices } from '../../lib/kibana'; /** * Removes the `-SNAPSHOT` that is sometimes appended to the Kibana version, @@ -90,15 +90,11 @@ export const fetchNews = async ({ }: { newsFeedUrl: string; }): Promise => { - const response = await fetch(newsFeedUrl, { - credentials: 'omit', + return KibanaServices.get().http.fetch(newsFeedUrl, { method: 'GET', + credentials: 'omit', mode: 'cors', }); - - await throwIfNotOk(response); - - return response.json(); }; /** diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/helpers.ts b/x-pack/legacy/plugins/siem/public/components/recent_timelines/helpers.ts deleted file mode 100644 index 41fa90f1776e6..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/recent_timelines/helpers.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 { throwIfNotOk } from '../../hooks/api/api'; -import { KibanaServices } from '../../lib/kibana'; -import { MeApiResponse } from './recent_timelines'; - -export const fetchUsername = async () => { - const response = await KibanaServices.get().http.fetch('/internal/security/me', { - method: 'GET', - credentials: 'same-origin', - asResponse: true, - }); - - await throwIfNotOk(response.response); - return response.body!.username; -}; diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/index.tsx b/x-pack/legacy/plugins/siem/public/components/recent_timelines/index.tsx index 41eb137742963..6f22287774d7e 100644 --- a/x-pack/legacy/plugins/siem/public/components/recent_timelines/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/recent_timelines/index.tsx @@ -22,10 +22,6 @@ import { RecentTimelines } from './recent_timelines'; import * as i18n from './translations'; import { FilterMode } from './types'; -export interface MeApiResponse { - username: string; -} - interface OwnProps { apolloClient: ApolloClient<{}>; filterBy: FilterMode; diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/recent_timelines.tsx b/x-pack/legacy/plugins/siem/public/components/recent_timelines/recent_timelines.tsx index a01cc0fe277aa..fdd206a33ff7e 100644 --- a/x-pack/legacy/plugins/siem/public/components/recent_timelines/recent_timelines.tsx +++ b/x-pack/legacy/plugins/siem/public/components/recent_timelines/recent_timelines.tsx @@ -21,10 +21,6 @@ import { WithHoverActions } from '../with_hover_actions'; import { RecentTimelineCounts } from './counts'; import * as i18n from './translations'; -export interface MeApiResponse { - username: string; -} - export const RecentTimelines = React.memo<{ noTimelinesMessage: string; onOpenTimeline: OnOpenTimeline; diff --git a/x-pack/legacy/plugins/siem/public/components/toasters/errors.ts b/x-pack/legacy/plugins/siem/public/components/toasters/errors.ts new file mode 100644 index 0000000000000..7633d289c1f1d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/toasters/errors.ts @@ -0,0 +1,18 @@ +/* + * 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 class ToasterError extends Error { + public readonly messages: string[]; + + constructor(messages: string[]) { + super(messages[0]); + this.name = 'ToasterError'; + this.messages = messages; + } +} + +export const isToasterError = (error: unknown): error is ToasterError => + error instanceof ToasterError; diff --git a/x-pack/legacy/plugins/siem/public/components/toasters/index.tsx b/x-pack/legacy/plugins/siem/public/components/toasters/index.tsx index 6d13bbd778f53..a24b52d8eef4d 100644 --- a/x-pack/legacy/plugins/siem/public/components/toasters/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/toasters/index.tsx @@ -8,11 +8,13 @@ import { EuiButton, EuiGlobalToastList, EuiGlobalToastListToast as Toast } from import { noop } from 'lodash/fp'; import React, { createContext, Dispatch, useContext, useReducer, useState } from 'react'; import styled from 'styled-components'; -import uuid from 'uuid'; import { ModalAllErrors } from './modal_all_errors'; import * as i18n from './translations'; +export * from './utils'; +export * from './errors'; + export interface AppToast extends Toast { errors?: string[]; } @@ -131,50 +133,3 @@ const ErrorToastContainer = styled.div` `; ErrorToastContainer.displayName = 'ErrorToastContainer'; - -/** - * Displays an error toast for the provided title and message - * - * @param errorTitle Title of error to display in toaster and modal - * @param errorMessages Message to display in error modal when clicked - * @param dispatchToaster provided by useStateToaster() - */ -export const displayErrorToast = ( - errorTitle: string, - errorMessages: string[], - dispatchToaster: React.Dispatch -): void => { - const toast: AppToast = { - id: uuid.v4(), - title: errorTitle, - color: 'danger', - iconType: 'alert', - errors: errorMessages, - }; - dispatchToaster({ - type: 'addToaster', - toast, - }); -}; - -/** - * Displays a success toast for the provided title and message - * - * @param title success message to display in toaster and modal - * @param dispatchToaster provided by useStateToaster() - */ -export const displaySuccessToast = ( - title: string, - dispatchToaster: React.Dispatch -): void => { - const toast: AppToast = { - id: uuid.v4(), - title, - color: 'success', - iconType: 'check', - }; - dispatchToaster({ - type: 'addToaster', - toast, - }); -}; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/error_to_toaster.test.ts b/x-pack/legacy/plugins/siem/public/components/toasters/utils.test.ts similarity index 57% rename from x-pack/legacy/plugins/siem/public/components/ml/api/error_to_toaster.test.ts rename to x-pack/legacy/plugins/siem/public/components/toasters/utils.test.ts index d4f38d817bd6b..26cfc4bc1fca1 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/api/error_to_toaster.test.ts +++ b/x-pack/legacy/plugins/siem/public/components/toasters/utils.test.ts @@ -4,8 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isAnError, isToasterError, errorToToaster } from './error_to_toaster'; -import { ToasterErrors } from '../../../hooks/api/throw_if_not_ok'; +import { errorToToaster } from './utils'; +import { ToasterError } from './errors'; + +const ApiError = class extends Error { + public body: {} = {}; +}; describe('error_to_toaster', () => { let dispatchToaster = jest.fn(); @@ -15,8 +19,8 @@ describe('error_to_toaster', () => { }); describe('#errorToToaster', () => { - test('adds a ToastError given multiple toaster errors', () => { - const error = new ToasterErrors(['some error 1', 'some error 2']); + test('dispatches an error toast given a ToasterError with multiple error messages', () => { + const error = new ToasterError(['some error 1', 'some error 2']); errorToToaster({ id: 'some-made-up-id', title: 'some title', error, dispatchToaster }); expect(dispatchToaster.mock.calls[0]).toEqual([ { @@ -32,8 +36,8 @@ describe('error_to_toaster', () => { ]); }); - test('adds a ToastError given a single toaster errors', () => { - const error = new ToasterErrors(['some error 1']); + test('dispatches an error toast given a ToasterError with a single error message', () => { + const error = new ToasterError(['some error 1']); errorToToaster({ id: 'some-made-up-id', title: 'some title', error, dispatchToaster }); expect(dispatchToaster.mock.calls[0]).toEqual([ { @@ -49,7 +53,44 @@ describe('error_to_toaster', () => { ]); }); - test('adds a regular Error given a single error', () => { + test('dispatches an error toast given an ApiError with a message', () => { + const error = new ApiError('Internal Server Error'); + error.body = { message: 'something bad happened', status_code: 500 }; + + errorToToaster({ id: 'some-made-up-id', title: 'some title', error, dispatchToaster }); + expect(dispatchToaster.mock.calls[0]).toEqual([ + { + toast: { + color: 'danger', + errors: ['something bad happened'], + iconType: 'alert', + id: 'some-made-up-id', + title: 'some title', + }, + type: 'addToaster', + }, + ]); + }); + + test('dispatches an error toast given an ApiError with no message', () => { + const error = new ApiError('Internal Server Error'); + + errorToToaster({ id: 'some-made-up-id', title: 'some title', error, dispatchToaster }); + expect(dispatchToaster.mock.calls[0]).toEqual([ + { + toast: { + color: 'danger', + errors: ['Internal Server Error'], + iconType: 'alert', + id: 'some-made-up-id', + title: 'some title', + }, + type: 'addToaster', + }, + ]); + }); + + test('dispatches an error toast given a standard Error', () => { const error = new Error('some error 1'); errorToToaster({ id: 'some-made-up-id', title: 'some title', error, dispatchToaster }); expect(dispatchToaster.mock.calls[0]).toEqual([ @@ -83,44 +124,4 @@ describe('error_to_toaster', () => { ]); }); }); - - describe('#isAnError', () => { - test('returns true if given an error object', () => { - const error = new Error('some error'); - expect(isAnError(error)).toEqual(true); - }); - - test('returns false if given a regular object', () => { - expect(isAnError({})).toEqual(false); - }); - - test('returns false if given a string', () => { - expect(isAnError('som string')).toEqual(false); - }); - - test('returns true if given a toaster error', () => { - const error = new ToasterErrors(['some error']); - expect(isAnError(error)).toEqual(true); - }); - }); - - describe('#isToasterError', () => { - test('returns true if given a ToasterErrors object', () => { - const error = new ToasterErrors(['some error']); - expect(isToasterError(error)).toEqual(true); - }); - - test('returns false if given a regular object', () => { - expect(isToasterError({})).toEqual(false); - }); - - test('returns false if given a string', () => { - expect(isToasterError('som string')).toEqual(false); - }); - - test('returns false if given a regular error', () => { - const error = new Error('some error'); - expect(isToasterError(error)).toEqual(false); - }); - }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/toasters/utils.ts b/x-pack/legacy/plugins/siem/public/components/toasters/utils.ts new file mode 100644 index 0000000000000..e624144c9826f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/toasters/utils.ts @@ -0,0 +1,123 @@ +/* + * 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 uuid from 'uuid'; +import { isError } from 'lodash/fp'; + +import { AppToast, ActionToaster } from './'; +import { isToasterError } from './errors'; +import { isApiError } from '../../utils/api'; + +/** + * Displays an error toast for the provided title and message + * + * @param errorTitle Title of error to display in toaster and modal + * @param errorMessages Message to display in error modal when clicked + * @param dispatchToaster provided by useStateToaster() + */ +export const displayErrorToast = ( + errorTitle: string, + errorMessages: string[], + dispatchToaster: React.Dispatch, + id: string = uuid.v4() +): void => { + const toast: AppToast = { + id, + title: errorTitle, + color: 'danger', + iconType: 'alert', + errors: errorMessages, + }; + dispatchToaster({ + type: 'addToaster', + toast, + }); +}; + +/** + * Displays a success toast for the provided title and message + * + * @param title success message to display in toaster and modal + * @param dispatchToaster provided by useStateToaster() + */ +export const displaySuccessToast = ( + title: string, + dispatchToaster: React.Dispatch, + id: string = uuid.v4() +): void => { + const toast: AppToast = { + id, + title, + color: 'success', + iconType: 'check', + }; + dispatchToaster({ + type: 'addToaster', + toast, + }); +}; + +export type ErrorToToasterArgs = Partial & { + error: unknown; + dispatchToaster: React.Dispatch; +}; + +/** + * Displays an error toast with messages parsed from the error + * + * @param title error message to display in toaster and modal + * @param error the error from which messages will be parsed + * @param dispatchToaster provided by useStateToaster() + */ +export const errorToToaster = ({ + id = uuid.v4(), + title, + error, + color = 'danger', + iconType = 'alert', + dispatchToaster, +}: ErrorToToasterArgs) => { + let toast: AppToast; + + if (isToasterError(error)) { + toast = { + id, + title, + color, + iconType, + errors: error.messages, + }; + } else if (isApiError(error)) { + toast = { + id, + title, + color, + iconType, + errors: [error.body.message], + }; + } else if (isError(error)) { + toast = { + id, + title, + color, + iconType, + errors: [error.message], + }; + } else { + toast = { + id, + title, + color, + iconType, + errors: ['Network Error'], + }; + } + + dispatchToaster({ + type: 'addToaster', + toast, + }); +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/api.ts index 81f8f83217e11..98bb1b470d561 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/api.ts @@ -13,7 +13,6 @@ import { } from '../../../../../../plugins/case/common/api'; import { KibanaServices } from '../../lib/kibana'; import { AllCases, Case, Comment, FetchCasesProps, SortFieldCase } from './types'; -import { throwIfNotOk } from '../../hooks/api/api'; import { CASES_URL } from './constants'; import { convertToCamelCase, @@ -28,22 +27,18 @@ const CaseSavedObjectType = 'cases'; export const getCase = async (caseId: string, includeComments: boolean = true): Promise => { const response = await KibanaServices.get().http.fetch(`${CASES_URL}/${caseId}`, { method: 'GET', - asResponse: true, query: { includeComments, }, }); - await throwIfNotOk(response.response); - return convertToCamelCase(decodeCaseResponse(response.body)); + return convertToCamelCase(decodeCaseResponse(response)); }; export const getTags = async (): Promise => { const response = await KibanaServices.get().http.fetch(`${CASES_URL}/tags`, { method: 'GET', - asResponse: true, }); - await throwIfNotOk(response.response); - return response.body ?? []; + return response ?? []; }; export const getCases = async ({ @@ -74,20 +69,16 @@ export const getCases = async ({ const response = await KibanaServices.get().http.fetch(`${CASES_URL}/_find`, { method: 'GET', query, - asResponse: true, }); - await throwIfNotOk(response.response); - return convertAllCasesToCamel(decodeCasesResponse(response.body)); + return convertAllCasesToCamel(decodeCasesResponse(response)); }; export const postCase = async (newCase: CaseRequest): Promise => { const response = await KibanaServices.get().http.fetch(`${CASES_URL}`, { method: 'POST', - asResponse: true, body: JSON.stringify(newCase), }); - await throwIfNotOk(response.response); - return convertToCamelCase(decodeCaseResponse(response.body)); + return convertToCamelCase(decodeCaseResponse(response)); }; export const patchCase = async ( @@ -97,11 +88,9 @@ export const patchCase = async ( ): Promise => { const response = await KibanaServices.get().http.fetch(`${CASES_URL}`, { method: 'PATCH', - asResponse: true, body: JSON.stringify({ ...updatedCase, id: caseId, version }), }); - await throwIfNotOk(response.response); - return convertToCamelCase(decodeCaseResponse(response.body)); + return convertToCamelCase(decodeCaseResponse(response)); }; export const postComment = async (newComment: CommentRequest, caseId: string): Promise => { @@ -109,12 +98,10 @@ export const postComment = async (newComment: CommentRequest, caseId: string): P `${CASES_URL}/${caseId}/comments`, { method: 'POST', - asResponse: true, body: JSON.stringify(newComment), } ); - await throwIfNotOk(response.response); - return convertToCamelCase(decodeCommentResponse(response.body)); + return convertToCamelCase(decodeCommentResponse(response)); }; export const patchComment = async ( @@ -124,9 +111,7 @@ export const patchComment = async ( ): Promise> => { const response = await KibanaServices.get().http.fetch(`${CASES_URL}/comments`, { method: 'PATCH', - asResponse: true, body: JSON.stringify({ comment: commentUpdate, id: commentId, version }), }); - await throwIfNotOk(response.response); - return convertToCamelCase(decodeCommentResponse(response.body)); + return convertToCamelCase(decodeCommentResponse(response)); }; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx index 5f1dc96735d32..b758f914c991e 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx @@ -9,9 +9,8 @@ import { useEffect, useReducer } from 'react'; import { Case } from './types'; import { FETCH_INIT, FETCH_FAILURE, FETCH_SUCCESS } from './constants'; import { getTypedPayload } from './utils'; -import { errorToToaster } from '../../components/ml/api/error_to_toaster'; import * as i18n from './translations'; -import { useStateToaster } from '../../components/toasters'; +import { errorToToaster, useStateToaster } from '../../components/toasters'; import { getCase } from './api'; interface CaseState { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx index 76e9b5c138269..99c7ef0c757c7 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx @@ -7,8 +7,7 @@ import { useCallback, useEffect, useReducer } from 'react'; import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from './constants'; import { AllCases, SortFieldCase, FilterOptions, QueryParams, Case } from './types'; -import { errorToToaster } from '../../components/ml/api/error_to_toaster'; -import { useStateToaster } from '../../components/toasters'; +import { errorToToaster, useStateToaster } from '../../components/toasters'; import * as i18n from './translations'; import { UpdateByKey } from './use_update_case'; import { getCases, patchCase } from './api'; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx index 7d3e00a4f2be4..5e6df9b92f462 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx @@ -5,9 +5,8 @@ */ import { useEffect, useReducer } from 'react'; -import { useStateToaster } from '../../components/toasters'; -import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import { errorToToaster, useStateToaster } from '../../components/toasters'; import { getTags } from './api'; import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS } from './constants'; import * as i18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx index 7497b30395155..5cd0911fae81a 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx @@ -7,9 +7,7 @@ import { useReducer, useCallback } from 'react'; import { CaseRequest } from '../../../../../../plugins/case/common/api'; -import { useStateToaster } from '../../components/toasters'; -import { errorToToaster } from '../../components/ml/api/error_to_toaster'; - +import { errorToToaster, useStateToaster } from '../../components/toasters'; import { postCase } from './api'; import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS } from './constants'; import * as i18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx index 63d24e2935c2a..1467c691f547e 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx @@ -7,8 +7,7 @@ import { useReducer, useCallback } from 'react'; import { CommentRequest } from '../../../../../../plugins/case/common/api'; -import { useStateToaster } from '../../components/toasters'; -import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import { errorToToaster, useStateToaster } from '../../components/toasters'; import { postComment } from './api'; import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS } from './constants'; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx index 21c8fb5dc7032..594677aefe245 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx @@ -7,8 +7,7 @@ import { useReducer, useCallback } from 'react'; import { CaseRequest } from '../../../../../../plugins/case/common/api'; -import { useStateToaster } from '../../components/toasters'; -import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import { errorToToaster, useStateToaster } from '../../components/toasters'; import { patchCase } from './api'; import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS } from './constants'; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx index d7649cb7d8fdb..bd9ce9bd37500 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx @@ -6,8 +6,7 @@ import { useReducer, useCallback } from 'react'; -import { useStateToaster } from '../../components/toasters'; -import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import { errorToToaster, useStateToaster } from '../../components/toasters'; import { patchComment } from './api'; import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS } from './constants'; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/utils.ts b/x-pack/legacy/plugins/siem/public/containers/case/utils.ts index a377c496fe726..6a0da7618c383 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/utils.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/utils.ts @@ -18,7 +18,7 @@ import { CommentResponse, CommentResponseRt, } from '../../../../../../plugins/case/common/api'; -import { ToasterErrors } from '../../hooks/api/throw_if_not_ok'; +import { ToasterError } from '../../components/toasters'; import { AllCases, Case } from './types'; export const getTypedPayload = (a: unknown): T => a as T; @@ -53,7 +53,7 @@ export const convertAllCasesToCamel = (snakeCases: CasesResponse): AllCases => ( total: snakeCases.total, }); -export const createToasterPlainError = (message: string) => new ToasterErrors([message]); +export const createToasterPlainError = (message: string) => new ToasterError([message]); export const decodeCaseResponse = (respCase?: CaseResponse) => pipe(CaseResponseRt.decode(respCase), fold(throwErrors(createToasterPlainError), identity)); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.test.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.test.ts index 05446577a0fa0..3048fc3dc5a02 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.test.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.test.ts @@ -20,115 +20,46 @@ import { getPrePackagedRulesStatus, } from './api'; import { ruleMock, rulesMock } from './mock'; -import { ToasterErrors } from '../../../hooks/api/throw_if_not_ok'; const abortCtrl = new AbortController(); const mockKibanaServices = KibanaServices.get as jest.Mock; jest.mock('../../../lib/kibana'); -const mockfetchSuccess = (body: unknown, fetchMock?: jest.Mock) => { - if (fetchMock) { - mockKibanaServices.mockImplementation(() => ({ - http: { - fetch: fetchMock, - }, - })); - } else { - mockKibanaServices.mockImplementation(() => ({ - http: { - fetch: () => ({ - response: { - ok: true, - message: 'success', - text: 'success', - }, - body, - }), - }, - })); - } -}; - -const mockfetchError = () => { - mockKibanaServices.mockImplementation(() => ({ - http: { - fetch: () => ({ - response: { - ok: false, - text: () => - JSON.stringify({ - message: 'super mega error, it is not that bad', - }), - }, - body: null, - }), - }, - })); -}; +const fetchMock = jest.fn(); +mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); describe('Detections Rules API', () => { - const fetchMock = jest.fn(); describe('addRule', () => { beforeEach(() => { - mockKibanaServices.mockClear(); fetchMock.mockClear(); - fetchMock.mockImplementation(() => ({ - response: { - ok: true, - message: 'success', - text: 'success', - }, - body: ruleMock, - })); + fetchMock.mockResolvedValue(ruleMock); }); - test('check parameter url, body', async () => { - mockfetchSuccess(null, fetchMock); + test('check parameter url, body', async () => { await addRule({ rule: ruleMock, signal: abortCtrl.signal }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules', { - asResponse: true, body: '{"description":"some desc","enabled":true,"false_positives":[],"filters":[],"from":"now-360s","index":["apm-*-transaction*","auditbeat-*","endgame-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf","language":"kuery","risk_score":75,"name":"Test rule","query":"user.email: \'root@elastic.co\'","references":[],"severity":"high","tags":["APM"],"to":"now","type":"query","threat":[]}', method: 'POST', signal: abortCtrl.signal, }); }); + test('happy path', async () => { - mockfetchSuccess(ruleMock); const ruleResp = await addRule({ rule: ruleMock, signal: abortCtrl.signal }); expect(ruleResp).toEqual(ruleMock); }); - test('unhappy path', async () => { - mockfetchError(); - try { - await addRule({ rule: ruleMock, signal: abortCtrl.signal }); - } catch (exp) { - expect(exp).toBeInstanceOf(ToasterErrors); - expect(exp.message).toEqual('super mega error, it is not that bad'); - } - }); }); describe('fetchRules', () => { beforeEach(() => { - mockKibanaServices.mockClear(); fetchMock.mockClear(); - fetchMock.mockImplementation(() => ({ - response: { - ok: true, - message: 'success', - text: 'success', - }, - body: rulesMock, - })); + fetchMock.mockResolvedValue(rulesMock); }); test('check parameter url, query without any options', async () => { - mockfetchSuccess(null, fetchMock); - await fetchRules({ signal: abortCtrl.signal }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { - asResponse: true, method: 'GET', query: { page: 1, @@ -141,8 +72,6 @@ describe('Detections Rules API', () => { }); test('check parameter url, query with a filter', async () => { - mockfetchSuccess(null, fetchMock); - await fetchRules({ filterOptions: { filter: 'hello world', @@ -154,8 +83,8 @@ describe('Detections Rules API', () => { }, signal: abortCtrl.signal, }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { - asResponse: true, method: 'GET', query: { filter: 'alert.attributes.name: hello world', @@ -169,8 +98,6 @@ describe('Detections Rules API', () => { }); test('check parameter url, query with showCustomRules', async () => { - mockfetchSuccess(null, fetchMock); - await fetchRules({ filterOptions: { filter: '', @@ -182,8 +109,8 @@ describe('Detections Rules API', () => { }, signal: abortCtrl.signal, }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { - asResponse: true, method: 'GET', query: { filter: 'alert.attributes.tags: "__internal_immutable:false"', @@ -197,8 +124,6 @@ describe('Detections Rules API', () => { }); test('check parameter url, query with showElasticRules', async () => { - mockfetchSuccess(null, fetchMock); - await fetchRules({ filterOptions: { filter: '', @@ -210,8 +135,8 @@ describe('Detections Rules API', () => { }, signal: abortCtrl.signal, }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { - asResponse: true, method: 'GET', query: { filter: 'alert.attributes.tags: "__internal_immutable:true"', @@ -225,8 +150,6 @@ describe('Detections Rules API', () => { }); test('check parameter url, query with tags', async () => { - mockfetchSuccess(null, fetchMock); - await fetchRules({ filterOptions: { filter: '', @@ -238,8 +161,8 @@ describe('Detections Rules API', () => { }, signal: abortCtrl.signal, }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { - asResponse: true, method: 'GET', query: { filter: 'alert.attributes.tags: hello AND alert.attributes.tags: world', @@ -253,8 +176,6 @@ describe('Detections Rules API', () => { }); test('check parameter url, query with all options', async () => { - mockfetchSuccess(null, fetchMock); - await fetchRules({ filterOptions: { filter: 'ruleName', @@ -267,7 +188,6 @@ describe('Detections Rules API', () => { signal: abortCtrl.signal, }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { - asResponse: true, method: 'GET', query: { filter: @@ -282,41 +202,20 @@ describe('Detections Rules API', () => { }); test('happy path', async () => { - mockfetchSuccess(rulesMock); - const rulesResp = await fetchRules({ signal: abortCtrl.signal }); expect(rulesResp).toEqual(rulesMock); }); - test('unhappy path', async () => { - mockfetchError(); - try { - await fetchRules({ signal: abortCtrl.signal }); - } catch (exp) { - expect(exp).toBeInstanceOf(ToasterErrors); - expect(exp.message).toEqual('super mega error, it is not that bad'); - } - }); }); describe('fetchRuleById', () => { beforeEach(() => { - mockKibanaServices.mockClear(); fetchMock.mockClear(); - fetchMock.mockImplementation(() => ({ - response: { - ok: true, - message: 'success', - text: 'success', - }, - body: ruleMock, - })); + fetchMock.mockResolvedValue(ruleMock); }); - test('check parameter url, query', async () => { - mockfetchSuccess(null, fetchMock); + test('check parameter url, query', async () => { await fetchRuleById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules', { - asResponse: true, query: { id: 'mySuperRuleId', }, @@ -324,192 +223,102 @@ describe('Detections Rules API', () => { signal: abortCtrl.signal, }); }); + test('happy path', async () => { - mockfetchSuccess(ruleMock); const ruleResp = await fetchRuleById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); expect(ruleResp).toEqual(ruleMock); }); - test('unhappy path', async () => { - mockfetchError(); - try { - await fetchRuleById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); - } catch (exp) { - expect(exp).toBeInstanceOf(ToasterErrors); - expect(exp.message).toEqual('super mega error, it is not that bad'); - } - }); }); describe('enableRules', () => { beforeEach(() => { - mockKibanaServices.mockClear(); fetchMock.mockClear(); - fetchMock.mockImplementation(() => ({ - response: { - ok: true, - message: 'success', - text: 'success', - }, - body: ruleMock, - })); + fetchMock.mockResolvedValue(rulesMock); }); - test('check parameter url, body when enabling rules', async () => { - mockfetchSuccess(null, fetchMock); + test('check parameter url, body when enabling rules', async () => { await enableRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'], enabled: true }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_update', { - asResponse: true, body: '[{"id":"mySuperRuleId","enabled":true},{"id":"mySuperRuleId_II","enabled":true}]', method: 'PATCH', }); }); test('check parameter url, body when disabling rules', async () => { - mockfetchSuccess(null, fetchMock); - await enableRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'], enabled: false }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_update', { - asResponse: true, body: '[{"id":"mySuperRuleId","enabled":false},{"id":"mySuperRuleId_II","enabled":false}]', method: 'PATCH', }); }); test('happy path', async () => { - mockfetchSuccess(rulesMock.data); const ruleResp = await enableRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'], enabled: true, }); - expect(ruleResp).toEqual(rulesMock.data); - }); - test('unhappy path', async () => { - mockfetchError(); - try { - await enableRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'], enabled: true }); - } catch (exp) { - expect(exp).toBeInstanceOf(ToasterErrors); - expect(exp.message).toEqual('super mega error, it is not that bad'); - } + expect(ruleResp).toEqual(rulesMock); }); }); describe('deleteRules', () => { beforeEach(() => { - mockKibanaServices.mockClear(); fetchMock.mockClear(); - fetchMock.mockImplementation(() => ({ - response: { - ok: true, - message: 'success', - text: 'success', - }, - body: ruleMock, - })); + fetchMock.mockResolvedValue(rulesMock); }); - test('check parameter url, body when deleting rules', async () => { - mockfetchSuccess(null, fetchMock); + test('check parameter url, body when deleting rules', async () => { await deleteRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'] }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_delete', { - asResponse: true, body: '[{"id":"mySuperRuleId"},{"id":"mySuperRuleId_II"}]', method: 'DELETE', }); }); + test('happy path', async () => { - mockfetchSuccess(ruleMock); const ruleResp = await deleteRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'], }); - expect(ruleResp).toEqual(ruleMock); - }); - test('unhappy path', async () => { - mockfetchError(); - try { - await deleteRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'] }); - } catch (exp) { - expect(exp).toBeInstanceOf(ToasterErrors); - expect(exp.message).toEqual('super mega error, it is not that bad'); - } + expect(ruleResp).toEqual(rulesMock); }); }); describe('duplicateRules', () => { beforeEach(() => { - mockKibanaServices.mockClear(); fetchMock.mockClear(); - fetchMock.mockImplementation(() => ({ - response: { - ok: true, - message: 'success', - text: 'success', - }, - body: ruleMock, - })); + fetchMock.mockResolvedValue(rulesMock); }); - test('check parameter url, body when duplicating rules', async () => { - mockfetchSuccess(null, fetchMock); + test('check parameter url, body when duplicating rules', async () => { await duplicateRules({ rules: rulesMock.data }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_create', { - asResponse: true, body: '[{"description":"Elastic Endpoint detected Credential Dumping. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.","enabled":false,"false_positives":[],"from":"now-660s","index":["endgame-*"],"interval":"10m","language":"kuery","output_index":".siem-signals-default","max_signals":100,"risk_score":73,"name":"Credential Dumping - Detected - Elastic Endpoint [Duplicate]","query":"event.kind:alert and event.module:endgame and event.action:cred_theft_event and endgame.metadata.type:detection","filters":[],"references":[],"severity":"high","tags":["Elastic","Endpoint"],"to":"now","type":"query","threat":[],"version":1},{"description":"Elastic Endpoint detected an Adversary Behavior. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.","enabled":false,"false_positives":[],"from":"now-660s","index":["endgame-*"],"interval":"10m","language":"kuery","output_index":".siem-signals-default","max_signals":100,"risk_score":47,"name":"Adversary Behavior - Detected - Elastic Endpoint [Duplicate]","query":"event.kind:alert and event.module:endgame and event.action:rules_engine_event","filters":[],"references":[],"severity":"medium","tags":["Elastic","Endpoint"],"to":"now","type":"query","threat":[],"version":1}]', method: 'POST', }); }); + test('happy path', async () => { - mockfetchSuccess(rulesMock.data); const ruleResp = await duplicateRules({ rules: rulesMock.data }); - expect(ruleResp).toEqual(rulesMock.data); - }); - test('unhappy path', async () => { - mockfetchError(); - try { - await duplicateRules({ rules: rulesMock.data }); - } catch (exp) { - expect(exp).toBeInstanceOf(ToasterErrors); - expect(exp.message).toEqual('super mega error, it is not that bad'); - } + expect(ruleResp).toEqual(rulesMock); }); }); describe('createPrepackagedRules', () => { beforeEach(() => { - mockKibanaServices.mockClear(); fetchMock.mockClear(); - fetchMock.mockImplementation(() => ({ - response: { - ok: true, - message: 'success', - text: 'success', - }, - body: ruleMock, - })); + fetchMock.mockResolvedValue('unknown'); }); - test('check parameter url when creating pre-packaged rules', async () => { - mockfetchSuccess(null, fetchMock); + test('check parameter url when creating pre-packaged rules', async () => { await createPrepackagedRules({ signal: abortCtrl.signal }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/prepackaged', { - asResponse: true, signal: abortCtrl.signal, method: 'PUT', }); }); test('happy path', async () => { - mockfetchSuccess(true); const resp = await createPrepackagedRules({ signal: abortCtrl.signal }); expect(resp).toEqual(true); }); - test('unhappy path', async () => { - mockfetchError(); - try { - await createPrepackagedRules({ signal: abortCtrl.signal }); - } catch (exp) { - expect(exp).toBeInstanceOf(ToasterErrors); - expect(exp.message).toEqual('super mega error, it is not that bad'); - } - }); }); describe('importRules', () => { @@ -525,23 +334,15 @@ describe('Detections Rules API', () => { } as File; const formData = new FormData(); formData.append('file', fileToImport); + beforeEach(() => { - mockKibanaServices.mockClear(); fetchMock.mockClear(); - fetchMock.mockImplementation(() => ({ - response: { - ok: true, - message: 'success', - text: 'success', - }, - body: ruleMock, - })); + fetchMock.mockResolvedValue('unknown'); }); + test('check parameter url, body and query when importing rules', async () => { - mockfetchSuccess(null, fetchMock); await importRules({ fileToImport, signal: abortCtrl.signal }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_import', { - asResponse: true, signal: abortCtrl.signal, method: 'POST', body: formData, @@ -555,11 +356,8 @@ describe('Detections Rules API', () => { }); test('check parameter url, body and query when importing rules with overwrite', async () => { - mockfetchSuccess(null, fetchMock); - await importRules({ fileToImport, overwrite: true, signal: abortCtrl.signal }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_import', { - asResponse: true, signal: abortCtrl.signal, method: 'POST', body: formData, @@ -573,7 +371,7 @@ describe('Detections Rules API', () => { }); test('happy path', async () => { - mockfetchSuccess({ + fetchMock.mockResolvedValue({ success: true, success_count: 33, errors: [], @@ -585,40 +383,29 @@ describe('Detections Rules API', () => { errors: [], }); }); - - test('unhappy path', async () => { - mockfetchError(); - try { - await importRules({ fileToImport, signal: abortCtrl.signal }); - } catch (exp) { - expect(exp).toBeInstanceOf(ToasterErrors); - expect(exp.message).toEqual('super mega error, it is not that bad'); - } - }); }); describe('exportRules', () => { + const blob: Blob = { + size: 89, + type: 'json', + arrayBuffer: jest.fn(), + slice: jest.fn(), + stream: jest.fn(), + text: jest.fn(), + } as Blob; + beforeEach(() => { - mockKibanaServices.mockClear(); fetchMock.mockClear(); - fetchMock.mockImplementation(() => ({ - response: { - ok: true, - message: 'success', - text: 'success', - }, - body: ruleMock, - })); + fetchMock.mockResolvedValue(blob); }); test('check parameter url, body and query when exporting rules', async () => { - mockfetchSuccess(null, fetchMock); await exportRules({ ruleIds: ['mySuperRuleId', 'mySuperRuleId_II'], signal: abortCtrl.signal, }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_export', { - asResponse: true, signal: abortCtrl.signal, method: 'POST', body: '{"objects":[{"rule_id":"mySuperRuleId"},{"rule_id":"mySuperRuleId_II"}]}', @@ -630,14 +417,12 @@ describe('Detections Rules API', () => { }); test('check parameter url, body and query when exporting rules with excludeExportDetails', async () => { - mockfetchSuccess(null, fetchMock); await exportRules({ excludeExportDetails: true, ruleIds: ['mySuperRuleId', 'mySuperRuleId_II'], signal: abortCtrl.signal, }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_export', { - asResponse: true, signal: abortCtrl.signal, method: 'POST', body: '{"objects":[{"rule_id":"mySuperRuleId"},{"rule_id":"mySuperRuleId_II"}]}', @@ -649,14 +434,12 @@ describe('Detections Rules API', () => { }); test('check parameter url, body and query when exporting rules with fileName', async () => { - mockfetchSuccess(null, fetchMock); await exportRules({ filename: 'myFileName.ndjson', ruleIds: ['mySuperRuleId', 'mySuperRuleId_II'], signal: abortCtrl.signal, }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_export', { - asResponse: true, signal: abortCtrl.signal, method: 'POST', body: '{"objects":[{"rule_id":"mySuperRuleId"},{"rule_id":"mySuperRuleId_II"}]}', @@ -668,7 +451,6 @@ describe('Detections Rules API', () => { }); test('check parameter url, body and query when exporting rules with all options', async () => { - mockfetchSuccess(null, fetchMock); await exportRules({ excludeExportDetails: true, filename: 'myFileName.ndjson', @@ -676,7 +458,6 @@ describe('Detections Rules API', () => { signal: abortCtrl.signal, }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_export', { - asResponse: true, signal: abortCtrl.signal, method: 'POST', body: '{"objects":[{"rule_id":"mySuperRuleId"},{"rule_id":"mySuperRuleId_II"}]}', @@ -688,55 +469,38 @@ describe('Detections Rules API', () => { }); test('happy path', async () => { - const blob: Blob = { - size: 89, - type: 'json', - arrayBuffer: jest.fn(), - slice: jest.fn(), - stream: jest.fn(), - text: jest.fn(), - } as Blob; - mockfetchSuccess(blob); const resp = await exportRules({ ruleIds: ['mySuperRuleId', 'mySuperRuleId_II'], signal: abortCtrl.signal, }); expect(resp).toEqual(blob); }); - - test('unhappy path', async () => { - mockfetchError(); - try { - await exportRules({ - ruleIds: ['mySuperRuleId', 'mySuperRuleId_II'], - signal: abortCtrl.signal, - }); - } catch (exp) { - expect(exp).toBeInstanceOf(ToasterErrors); - expect(exp.message).toEqual('super mega error, it is not that bad'); - } - }); }); describe('getRuleStatusById', () => { + const statusMock = { + myRule: { + current_status: { + alert_id: 'alertId', + status_date: 'mm/dd/yyyyTHH:MM:sssz', + status: 'succeeded', + last_failure_at: null, + last_success_at: 'mm/dd/yyyyTHH:MM:sssz', + last_failure_message: null, + last_success_message: 'it is a success', + }, + failures: [], + }, + }; + beforeEach(() => { - mockKibanaServices.mockClear(); fetchMock.mockClear(); - fetchMock.mockImplementation(() => ({ - response: { - ok: true, - message: 'success', - text: 'success', - }, - body: ruleMock, - })); + fetchMock.mockResolvedValue(statusMock); }); - test('check parameter url, query', async () => { - mockfetchSuccess(null, fetchMock); + test('check parameter url, query', async () => { await getRuleStatusById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find_statuses', { - asResponse: true, query: { ids: '["mySuperRuleId"]', }, @@ -744,117 +508,54 @@ describe('Detections Rules API', () => { signal: abortCtrl.signal, }); }); + test('happy path', async () => { - const statusMock = { - myRule: { - current_status: { - alert_id: 'alertId', - status_date: 'mm/dd/yyyyTHH:MM:sssz', - status: 'succeeded', - last_failure_at: null, - last_success_at: 'mm/dd/yyyyTHH:MM:sssz', - last_failure_message: null, - last_success_message: 'it is a success', - }, - failures: [], - }, - }; - mockfetchSuccess(statusMock); const ruleResp = await getRuleStatusById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); expect(ruleResp).toEqual(statusMock); }); - test('unhappy path', async () => { - mockfetchError(); - try { - await getRuleStatusById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); - } catch (exp) { - expect(exp).toBeInstanceOf(ToasterErrors); - expect(exp.message).toEqual('super mega error, it is not that bad'); - } - }); }); describe('fetchTags', () => { beforeEach(() => { - mockKibanaServices.mockClear(); fetchMock.mockClear(); - fetchMock.mockImplementation(() => ({ - response: { - ok: true, - message: 'success', - text: 'success', - }, - body: ruleMock, - })); + fetchMock.mockResolvedValue(['some', 'tags']); }); - test('check parameter url when fetching tags', async () => { - mockfetchSuccess(null, fetchMock); + test('check parameter url when fetching tags', async () => { await fetchTags({ signal: abortCtrl.signal }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/tags', { - asResponse: true, signal: abortCtrl.signal, method: 'GET', }); }); + test('happy path', async () => { - mockfetchSuccess(['hello', 'tags']); const resp = await fetchTags({ signal: abortCtrl.signal }); - expect(resp).toEqual(['hello', 'tags']); - }); - test('unhappy path', async () => { - mockfetchError(); - try { - await fetchTags({ signal: abortCtrl.signal }); - } catch (exp) { - expect(exp).toBeInstanceOf(ToasterErrors); - expect(exp.message).toEqual('super mega error, it is not that bad'); - } + expect(resp).toEqual(['some', 'tags']); }); }); describe('getPrePackagedRulesStatus', () => { + const prePackagedRulesStatus = { + rules_custom_installed: 33, + rules_installed: 12, + rules_not_installed: 0, + rules_not_updated: 2, + }; beforeEach(() => { - mockKibanaServices.mockClear(); fetchMock.mockClear(); - fetchMock.mockImplementation(() => ({ - response: { - ok: true, - message: 'success', - text: 'success', - }, - body: ruleMock, - })); + fetchMock.mockResolvedValue(prePackagedRulesStatus); }); test('check parameter url when fetching tags', async () => { - mockfetchSuccess(null, fetchMock); - await getPrePackagedRulesStatus({ signal: abortCtrl.signal }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/prepackaged/_status', { - asResponse: true, signal: abortCtrl.signal, method: 'GET', }); }); test('happy path', async () => { - const prePackagesRulesStatus = { - rules_custom_installed: 33, - rules_installed: 12, - rules_not_installed: 0, - rules_not_updated: 2, - }; - mockfetchSuccess(prePackagesRulesStatus); const resp = await getPrePackagedRulesStatus({ signal: abortCtrl.signal }); - expect(resp).toEqual(prePackagesRulesStatus); - }); - test('unhappy path', async () => { - mockfetchError(); - try { - await getPrePackagedRulesStatus({ signal: abortCtrl.signal }); - } catch (exp) { - expect(exp).toBeInstanceOf(ToasterErrors); - expect(exp.message).toEqual('super mega error, it is not that bad'); - } + expect(resp).toEqual(prePackagedRulesStatus); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts index dfd812251e3d6..b52c4964c6695 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts @@ -17,13 +17,12 @@ import { BasicFetchProps, ImportRulesProps, ExportRulesProps, - RuleError, RuleStatusResponse, ImportRulesResponse, PrePackagedRulesStatusResponse, + BulkRuleResponse, } from './types'; import { KibanaServices } from '../../../lib/kibana'; -import { throwIfNotOk } from '../../../hooks/api/api'; import { DETECTION_ENGINE_RULES_URL, DETECTION_ENGINE_PREPACKAGED_URL, @@ -38,19 +37,16 @@ import * as i18n from '../../../pages/detection_engine/rules/translations'; * * @param rule to add * @param signal to cancel request + * + * @throws An error if response is not OK */ -export const addRule = async ({ rule, signal }: AddRulesProps): Promise => { - const response = await KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_URL, { +export const addRule = async ({ rule, signal }: AddRulesProps): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_URL, { method: rule.id != null ? 'PUT' : 'POST', body: JSON.stringify(rule), - asResponse: true, signal, }); - await throwIfNotOk(response.response); - return response.body!; -}; - /** * Fetches all rules from the Detection Engine API * @@ -58,6 +54,7 @@ export const addRule = async ({ rule, signal }: AddRulesProps): Promise * @param pagination desired pagination options (e.g. page/perPage) * @param signal to cancel request * + * @throws An error if response is not OK */ export const fetchRules = async ({ filterOptions = { @@ -94,18 +91,14 @@ export const fetchRules = async ({ ...(filters.length ? { filter: filters.join(' AND ') } : {}), }; - const response = await KibanaServices.get().http.fetch( + return KibanaServices.get().http.fetch( `${DETECTION_ENGINE_RULES_URL}/_find`, { method: 'GET', query, signal, - asResponse: true, } ); - - await throwIfNotOk(response.response); - return response.body!; }; /** @@ -114,19 +107,15 @@ export const fetchRules = async ({ * @param id Rule ID's (not rule_id) * @param signal to cancel request * + * @throws An error if response is not OK */ -export const fetchRuleById = async ({ id, signal }: FetchRuleProps): Promise => { - const response = await KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_URL, { +export const fetchRuleById = async ({ id, signal }: FetchRuleProps): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_URL, { method: 'GET', query: { id }, - asResponse: true, signal, }); - await throwIfNotOk(response.response); - return response.body!; -}; - /** * Enables/Disables provided Rule ID's * @@ -135,19 +124,11 @@ export const fetchRuleById = async ({ id, signal }: FetchRuleProps): Promise => { - const response = await KibanaServices.get().http.fetch( - `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, - { - method: 'PATCH', - body: JSON.stringify(ids.map(id => ({ id, enabled }))), - asResponse: true, - } - ); - - await throwIfNotOk(response.response); - return response.body!; -}; +export const enableRules = async ({ ids, enabled }: EnableRulesProps): Promise => + KibanaServices.get().http.fetch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`, { + method: 'PATCH', + body: JSON.stringify(ids.map(id => ({ id, enabled }))), + }); /** * Deletes provided Rule ID's @@ -156,74 +137,57 @@ export const enableRules = async ({ ids, enabled }: EnableRulesProps): Promise> => { - const response = await KibanaServices.get().http.fetch( - `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, - { - method: 'DELETE', - body: JSON.stringify(ids.map(id => ({ id }))), - asResponse: true, - } - ); - - await throwIfNotOk(response.response); - return response.body!; -}; +export const deleteRules = async ({ ids }: DeleteRulesProps): Promise => + KibanaServices.get().http.fetch(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, { + method: 'DELETE', + body: JSON.stringify(ids.map(id => ({ id }))), + }); /** * Duplicates provided Rules * * @param rules to duplicate + * + * @throws An error if response is not OK */ -export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise => { - const response = await KibanaServices.get().http.fetch( - `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, - { - method: 'POST', - body: JSON.stringify( - rules.map(rule => ({ - ...rule, - name: `${rule.name} [${i18n.DUPLICATE}]`, - created_at: undefined, - created_by: undefined, - id: undefined, - rule_id: undefined, - updated_at: undefined, - updated_by: undefined, - enabled: rule.enabled, - immutable: undefined, - last_success_at: undefined, - last_success_message: undefined, - last_failure_at: undefined, - last_failure_message: undefined, - status: undefined, - status_date: undefined, - })) - ), - asResponse: true, - } - ); - - await throwIfNotOk(response.response); - return response.body!; -}; +export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise => + KibanaServices.get().http.fetch(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`, { + method: 'POST', + body: JSON.stringify( + rules.map(rule => ({ + ...rule, + name: `${rule.name} [${i18n.DUPLICATE}]`, + created_at: undefined, + created_by: undefined, + id: undefined, + rule_id: undefined, + updated_at: undefined, + updated_by: undefined, + enabled: rule.enabled, + immutable: undefined, + last_success_at: undefined, + last_success_message: undefined, + last_failure_at: undefined, + last_failure_message: undefined, + status: undefined, + status_date: undefined, + })) + ), + }); /** * Create Prepackaged Rules * * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK */ export const createPrepackagedRules = async ({ signal }: BasicFetchProps): Promise => { - const response = await KibanaServices.get().http.fetch( - DETECTION_ENGINE_PREPACKAGED_URL, - { - method: 'PUT', - signal, - asResponse: true, - } - ); + await KibanaServices.get().http.fetch(DETECTION_ENGINE_PREPACKAGED_URL, { + method: 'PUT', + signal, + }); - await throwIfNotOk(response.response); return true; }; @@ -244,20 +208,16 @@ export const importRules = async ({ const formData = new FormData(); formData.append('file', fileToImport); - const response = await KibanaServices.get().http.fetch( + return KibanaServices.get().http.fetch( `${DETECTION_ENGINE_RULES_URL}/_import`, { method: 'POST', headers: { 'Content-Type': undefined }, query: { overwrite }, body: formData, - asResponse: true, signal, } ); - - await throwIfNotOk(response.response); - return response.body!; }; /** @@ -281,22 +241,15 @@ export const exportRules = async ({ ? JSON.stringify({ objects: ruleIds.map(rule => ({ rule_id: rule })) }) : undefined; - const response = await KibanaServices.get().http.fetch( - `${DETECTION_ENGINE_RULES_URL}/_export`, - { - method: 'POST', - body, - query: { - exclude_export_details: excludeExportDetails, - file_name: filename, - }, - signal, - asResponse: true, - } - ); - - await throwIfNotOk(response.response); - return response.body!; + return KibanaServices.get().http.fetch(`${DETECTION_ENGINE_RULES_URL}/_export`, { + method: 'POST', + body, + query: { + exclude_export_details: excludeExportDetails, + file_name: filename, + }, + signal, + }); }; /** @@ -313,38 +266,26 @@ export const getRuleStatusById = async ({ }: { id: string; signal: AbortSignal; -}): Promise => { - const response = await KibanaServices.get().http.fetch( - DETECTION_ENGINE_RULES_STATUS_URL, - { - method: 'GET', - query: { ids: JSON.stringify([id]) }, - signal, - asResponse: true, - } - ); - - await throwIfNotOk(response.response); - return response.body!; -}; +}): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_STATUS_URL, { + method: 'GET', + query: { ids: JSON.stringify([id]) }, + signal, + }); /** * Fetch all unique Tags used by Rules * * @param signal to cancel request * + * @throws An error if response is not OK */ -export const fetchTags = async ({ signal }: { signal: AbortSignal }): Promise => { - const response = await KibanaServices.get().http.fetch(DETECTION_ENGINE_TAGS_URL, { +export const fetchTags = async ({ signal }: { signal: AbortSignal }): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_TAGS_URL, { method: 'GET', signal, - asResponse: true, }); - await throwIfNotOk(response.response); - return response.body!; -}; - /** * Get pre packaged rules Status * @@ -356,16 +297,11 @@ export const getPrePackagedRulesStatus = async ({ signal, }: { signal: AbortSignal; -}): Promise => { - const response = await KibanaServices.get().http.fetch( +}): Promise => + KibanaServices.get().http.fetch( DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL, { method: 'GET', signal, - asResponse: true, } ); - - await throwIfNotOk(response.response); - return response.body!; -}; 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 06c4d1054bca4..c5aefac15f48e 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 @@ -15,8 +15,7 @@ import { getIndexFields, sourceQuery, } from '../../../containers/source'; -import { useStateToaster } from '../../../components/toasters'; -import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; +import { errorToToaster, useStateToaster } from '../../../components/toasters'; import { SourceQuery } from '../../../graphql/types'; import { useApolloClient } from '../../../utils/apollo_context'; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx index e720a1e70f153..4d4f6c9d8f63a 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx @@ -6,8 +6,7 @@ import { useEffect, useState, Dispatch } from 'react'; -import { useStateToaster } from '../../../components/toasters'; -import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; +import { errorToToaster, useStateToaster } from '../../../components/toasters'; import { addRule as persistRule } from './api'; import * as i18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts index ff49bb8a8c3a2..4d2aec4ee8740 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts @@ -96,10 +96,13 @@ export type Rule = t.TypeOf; export type Rules = t.TypeOf; export interface RuleError { - rule_id: string; + id?: string; + rule_id?: string; error: { status_code: number; message: string }; } +export type BulkRuleResponse = Array; + export interface RuleResponseBuckets { rules: Rule[]; errors: RuleError[]; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx index 04d7e3ef67da4..0dd95bea8a0b2 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx @@ -6,8 +6,7 @@ import { useEffect, useState } from 'react'; -import { useStateToaster, displaySuccessToast } from '../../../components/toasters'; -import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; +import { errorToToaster, useStateToaster, displaySuccessToast } from '../../../components/toasters'; import { getPrePackagedRulesStatus, createPrepackagedRules } from './api'; import * as i18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx index ab08bd39688ce..d6a49e006e1b8 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx @@ -6,8 +6,7 @@ import { useEffect, useState } from 'react'; -import { useStateToaster } from '../../../components/toasters'; -import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; +import { errorToToaster, useStateToaster } from '../../../components/toasters'; import { fetchRuleById } from './api'; import * as i18n from './translations'; import { Rule } from './types'; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx index fcf95ac061ba3..8d06e037e0979 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx @@ -6,8 +6,7 @@ import { useEffect, useRef, useState } from 'react'; -import { useStateToaster } from '../../../components/toasters'; -import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; +import { errorToToaster, useStateToaster } from '../../../components/toasters'; import { getRuleStatusById } from './api'; import * as i18n from './translations'; import { RuleStatus } from './types'; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx index 81b8b04ed6648..6e41e229c2490 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx @@ -8,9 +8,8 @@ import { noop } from 'lodash/fp'; import { useEffect, useState, useRef } from 'react'; import { FetchRulesResponse, FilterOptions, PaginationOptions, Rule } from './types'; -import { useStateToaster } from '../../../components/toasters'; +import { errorToToaster, useStateToaster } from '../../../components/toasters'; import { fetchRules } from './api'; -import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; import * as i18n from './translations'; export type ReturnRules = [ diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.test.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.test.tsx index 68f54b35754f6..222ff3d1ede2e 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.test.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.test.tsx @@ -12,7 +12,7 @@ jest.mock('./api'); describe('useTags', () => { test('init', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useTags()); + const { result, waitForNextUpdate } = renderHook(() => useTags()); await waitForNextUpdate(); expect(result.current).toEqual([true, [], result.current[2]]); }); @@ -20,7 +20,7 @@ describe('useTags', () => { test('fetch tags', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useTags()); + const { result, waitForNextUpdate } = renderHook(() => useTags()); await waitForNextUpdate(); await waitForNextUpdate(); expect(result.current).toEqual([ diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.tsx index 5985200fa16ec..669efedc619bb 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.tsx @@ -6,9 +6,8 @@ import { noop } from 'lodash/fp'; import { useEffect, useState, useRef } from 'react'; -import { useStateToaster } from '../../../components/toasters'; +import { errorToToaster, useStateToaster } from '../../../components/toasters'; import { fetchTags } from './api'; -import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; import * as i18n from './translations'; export type ReturnTags = [boolean, string[], () => void]; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/__mocks__/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/__mocks__/api.ts new file mode 100644 index 0000000000000..7cb1d7d574cf8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/__mocks__/api.ts @@ -0,0 +1,29 @@ +/* + * 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 { + QuerySignals, + SignalSearchResponse, + BasicSignals, + SignalsIndex, + Privilege, +} from '../types'; +import { signalsMock, mockSignalIndex, mockUserPrivilege } from '../mock'; + +export const fetchQuerySignals = async ({ + query, + signal, +}: QuerySignals): Promise> => + Promise.resolve(signalsMock as SignalSearchResponse); + +export const getSignalIndex = async ({ signal }: BasicSignals): Promise => + Promise.resolve(mockSignalIndex); + +export const getUserPrivilege = async ({ signal }: BasicSignals): Promise => + Promise.resolve(mockUserPrivilege); + +export const createSignalIndex = async ({ signal }: BasicSignals): Promise => + Promise.resolve(mockSignalIndex); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.test.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.test.ts new file mode 100644 index 0000000000000..c011ecffb35bc --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.test.ts @@ -0,0 +1,165 @@ +/* + * 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 { KibanaServices } from '../../../lib/kibana'; +import { + signalsMock, + mockSignalsQuery, + mockStatusSignalQuery, + mockSignalIndex, + mockUserPrivilege, +} from './mock'; +import { + fetchQuerySignals, + updateSignalStatus, + getSignalIndex, + getUserPrivilege, + createSignalIndex, +} from './api'; + +const abortCtrl = new AbortController(); +const mockKibanaServices = KibanaServices.get as jest.Mock; +jest.mock('../../../lib/kibana'); + +const fetchMock = jest.fn(); +mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); + +describe('Detections Signals API', () => { + describe('fetchQuerySignals', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(signalsMock); + }); + + test('check parameter url, body', async () => { + await fetchQuerySignals({ query: mockSignalsQuery, signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/signals/search', { + body: + '{"aggs":{"signalsByGrouping":{"terms":{"field":"signal.rule.risk_score","missing":"All others","order":{"_count":"desc"},"size":10},"aggs":{"signals":{"date_histogram":{"field":"@timestamp","fixed_interval":"81000000ms","min_doc_count":0,"extended_bounds":{"min":1579644343954,"max":1582236343955}}}}}},"query":{"bool":{"filter":[{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}},{"range":{"@timestamp":{"gte":1579644343954,"lte":1582236343955}}}]}}}', + method: 'POST', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const signalsResp = await fetchQuerySignals({ + query: mockSignalsQuery, + signal: abortCtrl.signal, + }); + expect(signalsResp).toEqual(signalsMock); + }); + }); + + describe('updateSignalStatus', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue({}); + }); + + test('check parameter url, body when closing a signal', async () => { + await updateSignalStatus({ + query: mockStatusSignalQuery, + signal: abortCtrl.signal, + status: 'closed', + }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/signals/status', { + body: + '{"status":"closed","bool":{"filter":{"terms":{"_id":["b4ee5c32e3a321057edcc953ca17228c6fdfe5ba43fdbbdaffa8cefa11605cc5"]}}}}', + method: 'POST', + signal: abortCtrl.signal, + }); + }); + + test('check parameter url, body when opening a signal', async () => { + await updateSignalStatus({ + query: mockStatusSignalQuery, + signal: abortCtrl.signal, + status: 'open', + }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/signals/status', { + body: + '{"status":"open","bool":{"filter":{"terms":{"_id":["b4ee5c32e3a321057edcc953ca17228c6fdfe5ba43fdbbdaffa8cefa11605cc5"]}}}}', + method: 'POST', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const signalsResp = await updateSignalStatus({ + query: mockStatusSignalQuery, + signal: abortCtrl.signal, + status: 'open', + }); + expect(signalsResp).toEqual({}); + }); + }); + + describe('getSignalIndex', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(mockSignalIndex); + }); + + test('check parameter url', async () => { + await getSignalIndex({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/index', { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const signalsResp = await getSignalIndex({ + signal: abortCtrl.signal, + }); + expect(signalsResp).toEqual(mockSignalIndex); + }); + }); + + describe('getUserPrivilege', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(mockUserPrivilege); + }); + + test('check parameter url', async () => { + await getUserPrivilege({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/privileges', { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const signalsResp = await getUserPrivilege({ + signal: abortCtrl.signal, + }); + expect(signalsResp).toEqual(mockUserPrivilege); + }); + }); + + describe('createSignalIndex', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(mockSignalIndex); + }); + + test('check parameter url', async () => { + await createSignalIndex({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/index', { + method: 'POST', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const signalsResp = await createSignalIndex({ + signal: abortCtrl.signal, + }); + expect(signalsResp).toEqual(mockSignalIndex); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts index 085bde3e54ef1..25263c2d32735 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts @@ -5,7 +5,6 @@ */ import { KibanaServices } from '../../../lib/kibana'; -import { throwIfNotOk } from '../../../hooks/api/api'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL, DETECTION_ENGINE_SIGNALS_STATUS_URL, @@ -14,10 +13,8 @@ import { } from '../../../../common/constants'; import { BasicSignals, - PostSignalError, Privilege, QuerySignals, - SignalIndexError, SignalSearchResponse, SignalsIndex, UpdateSignalStatusProps, @@ -27,101 +24,78 @@ import { * Fetch Signals by providing a query * * @param query String to match a dsl + * @param signal to cancel request + * + * @throws An error if response is not OK */ export const fetchQuerySignals = async ({ query, signal, -}: QuerySignals): Promise> => { - const response = await KibanaServices.get().http.fetch>( +}: QuerySignals): Promise> => + KibanaServices.get().http.fetch>( DETECTION_ENGINE_QUERY_SIGNALS_URL, { method: 'POST', body: JSON.stringify(query), - asResponse: true, signal, } ); - await throwIfNotOk(response.response); - return response.body!; -}; - /** * Update signal status by query * * @param query of signals to update * @param status to update to('open' / 'closed') * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK */ export const updateSignalStatus = async ({ query, status, signal, -}: UpdateSignalStatusProps): Promise => { - const response = await KibanaServices.get().http.fetch(DETECTION_ENGINE_SIGNALS_STATUS_URL, { +}: UpdateSignalStatusProps): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_SIGNALS_STATUS_URL, { method: 'POST', body: JSON.stringify({ status, ...query }), - asResponse: true, signal, }); - await throwIfNotOk(response.response); - return response.body!; -}; - /** * Fetch Signal Index * * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK */ -export const getSignalIndex = async ({ signal }: BasicSignals): Promise => { - try { - return await KibanaServices.get().http.fetch(DETECTION_ENGINE_INDEX_URL, { - method: 'GET', - signal, - }); - } catch (e) { - if (e.body) { - throw new SignalIndexError(e.body); - } - throw e; - } -}; +export const getSignalIndex = async ({ signal }: BasicSignals): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_INDEX_URL, { + method: 'GET', + signal, + }); /** * Get User Privileges * * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK */ -export const getUserPrivilege = async ({ signal }: BasicSignals): Promise => { - const response = await KibanaServices.get().http.fetch( - DETECTION_ENGINE_PRIVILEGES_URL, - { - method: 'GET', - signal, - asResponse: true, - } - ); - - await throwIfNotOk(response.response); - return response.body!; -}; +export const getUserPrivilege = async ({ signal }: BasicSignals): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_PRIVILEGES_URL, { + method: 'GET', + signal, + }); /** * Create Signal Index if needed it * * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK */ -export const createSignalIndex = async ({ signal }: BasicSignals): Promise => { - try { - return await KibanaServices.get().http.fetch(DETECTION_ENGINE_INDEX_URL, { - method: 'POST', - signal, - }); - } catch (e) { - if (e.body) { - throw new PostSignalError(e.body); - } - throw e; - } -}; +export const createSignalIndex = async ({ signal }: BasicSignals): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_INDEX_URL, { + method: 'POST', + signal, + }); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/errors_types/get_index_error.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/errors_types/get_index_error.ts deleted file mode 100644 index 79dae5b8acb87..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/errors_types/get_index_error.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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 { MessageBody } from '../../../../hooks/api/throw_if_not_ok'; - -export class SignalIndexError extends Error { - message: string = ''; - status_code: number = -1; - error: string = ''; - - constructor(errObj: MessageBody) { - super(errObj.message); - this.message = errObj.message ?? ''; - this.status_code = errObj.status_code ?? -1; - this.error = errObj.error ?? ''; - this.name = 'SignalIndexError'; - - // Set the prototype explicitly. - Object.setPrototypeOf(this, SignalIndexError.prototype); - } -} diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/errors_types/post_index_error.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/errors_types/post_index_error.ts deleted file mode 100644 index 227699af71b42..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/errors_types/post_index_error.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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 { MessageBody } from '../../../../hooks/api/throw_if_not_ok'; - -export class PostSignalError extends Error { - message: string = ''; - statusCode: number = -1; - error: string = ''; - - constructor(errObj: MessageBody) { - super(errObj.message); - this.message = errObj.message ?? ''; - this.statusCode = errObj.statusCode ?? -1; - this.error = errObj.error ?? ''; - this.name = 'PostSignalError'; - - // Set the prototype explicitly. - Object.setPrototypeOf(this, PostSignalError.prototype); - } -} diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/errors_types/privilege_user_error.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/errors_types/privilege_user_error.ts deleted file mode 100644 index 19915e898bbeb..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/errors_types/privilege_user_error.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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 { MessageBody } from '../../../../hooks/api/throw_if_not_ok'; - -export class PrivilegeUserError extends Error { - message: string = ''; - statusCode: number = -1; - error: string = ''; - - constructor(errObj: MessageBody) { - super(errObj.message); - this.message = errObj.message ?? ''; - this.statusCode = errObj.statusCode ?? -1; - this.error = errObj.error ?? ''; - this.name = 'PrivilegeUserError'; - - // Set the prototype explicitly. - Object.setPrototypeOf(this, PrivilegeUserError.prototype); - } -} diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/mock.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/mock.ts new file mode 100644 index 0000000000000..37e93b1481e15 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/mock.ts @@ -0,0 +1,1037 @@ +/* + * 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 { SignalSearchResponse, SignalsIndex, Privilege } from './types'; + +export const signalsMock: SignalSearchResponse = { + took: 7, + timeout: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + total: { + value: 10000, + relation: 'gte', + }, + hits: [ + { + _index: '.siem-signals-default-000001', + _id: '820e05ab0a10a2110d6f0ab2e1864402724a88680d5b49840ecc17dd069d7646', + _score: 0, + _source: { + '@timestamp': '2020-02-15T00:15:19.231Z', + event: { + kind: 'signal', + code: 4625, + created: '2020-02-15T00:09:19.454Z', + module: 'security', + type: 'authentication_failure', + outcome: 'failure', + provider: 'Microsoft-Windows-Security-Auditing', + action: 'logon-failed', + category: 'authentication', + }, + winlog: { + record_id: 4864460, + task: 'Logon', + logon: { + failure: { + reason: 'Unknown user name or bad password.', + status: 'This is either due to a bad username or authentication information', + sub_status: 'User logon with misspelled or bad user account', + }, + type: 'Network', + }, + channel: 'Security', + event_id: 4625, + process: { + pid: 548, + thread: { + id: 292, + }, + }, + api: 'wineventlog', + opcode: 'Info', + computer_name: 'siem-windows', + keywords: ['Audit Failure'], + activity_id: '{96816605-032c-0000-eaad-4c5f58e1d501}', + provider_guid: '{54849625-5478-4994-a5ba-3e3b0328c30d}', + event_data: { + Status: '0xc000006d', + LmPackageName: '-', + SubjectUserSid: 'S-1-0-0', + SubjectLogonId: '0x0', + TransmittedServices: '-', + SubjectDomainName: '-', + LogonProcessName: 'NtLmSsp ', + AuthenticationPackageName: 'NTLM', + KeyLength: '0', + SubjectUserName: '-', + TargetUserSid: 'S-1-0-0', + FailureReason: '%%2313', + SubStatus: '0xc0000064', + LogonType: '3', + TargetUserName: 'ADMIN', + }, + provider_name: 'Microsoft-Windows-Security-Auditing', + }, + process: { + pid: 0, + executable: '-', + name: '-', + }, + agent: { + type: 'winlogbeat', + ephemeral_id: 'cbee8ae0-2c75-4999-ba16-71d482247f52', + hostname: 'siem-windows', + id: '19b2de73-7b9a-4e92-b3e7-82383ac5f389', + version: '7.5.1', + }, + cloud: { + availability_zone: 'us-east1-b', + project: { + id: 'elastic-beats', + }, + provider: 'gcp', + instance: { + id: '3849238371046563697', + name: 'siem-windows', + }, + machine: { + type: 'g1-small', + }, + }, + log: { + level: 'information', + }, + message: + 'An account failed to log on.\n\nSubject:\n\tSecurity ID:\t\tS-1-0-0\n\tAccount Name:\t\t-\n\tAccount Domain:\t\t-\n\tLogon ID:\t\t0x0\n\nLogon Type:\t\t\t3\n\nAccount For Which Logon Failed:\n\tSecurity ID:\t\tS-1-0-0\n\tAccount Name:\t\tADMIN\n\tAccount Domain:\t\t\n\nFailure Information:\n\tFailure Reason:\t\tUnknown user name or bad password.\n\tStatus:\t\t\t0xC000006D\n\tSub Status:\t\t0xC0000064\n\nProcess Information:\n\tCaller Process ID:\t0x0\n\tCaller Process Name:\t-\n\nNetwork Information:\n\tWorkstation Name:\t-\n\tSource Network Address:\t185.209.0.96\n\tSource Port:\t\t0\n\nDetailed Authentication Information:\n\tLogon Process:\t\tNtLmSsp \n\tAuthentication Package:\tNTLM\n\tTransited Services:\t-\n\tPackage Name (NTLM only):\t-\n\tKey Length:\t\t0\n\nThis event is generated when a logon request fails. It is generated on the computer where access was attempted.\n\nThe Subject fields indicate the account on the local system which requested the logon. This is most commonly a service such as the Server service, or a local process such as Winlogon.exe or Services.exe.\n\nThe Logon Type field indicates the kind of logon that was requested. The most common types are 2 (interactive) and 3 (network).\n\nThe Process Information fields indicate which account and process on the system requested the logon.\n\nThe Network Information fields indicate where a remote logon request originated. Workstation name is not always available and may be left blank in some cases.\n\nThe authentication information fields provide detailed information about this specific logon request.\n\t- Transited services indicate which intermediate services have participated in this logon request.\n\t- Package name indicates which sub-protocol was used among the NTLM protocols.\n\t- Key length indicates the length of the generated session key. This will be 0 if no session key was requested.', + user: { + name: 'ADMIN', + id: 'S-1-0-0', + }, + source: { + ip: '185.209.0.96', + port: 0, + domain: '-', + }, + ecs: { + version: '1.1.0', + }, + host: { + name: 'siem-windows', + os: { + name: 'Windows Server 2019 Datacenter', + kernel: '10.0.17763.1039 (WinBuild.160101.0800)', + build: '17763.1039', + platform: 'windows', + version: '10.0', + family: 'windows', + }, + id: 'ae32054e-0d4a-4c4d-88ec-b840f992e1c2', + hostname: 'siem-windows', + architecture: 'x86_64', + }, + signal: { + parent: { + rule: '2df3a613-f5a8-4b55-bf6a-487fc820b842', + id: 'AdctRnABMQha2n6boR1M', + type: 'event', + index: 'winlogbeat-7.5.1-2020.01.15-000001', + depth: 1, + }, + ancestors: [ + { + rule: '2df3a613-f5a8-4b55-bf6a-487fc820b842', + id: 'AdctRnABMQha2n6boR1M', + type: 'event', + index: 'winlogbeat-7.5.1-2020.01.15-000001', + depth: 1, + }, + ], + original_time: '2020-02-15T00:09:18.714Z', + status: 'open', + rule: { + id: '2df3a613-f5a8-4b55-bf6a-487fc820b842', + rule_id: '82b2b065-a2ee-49fc-9d6d-781a75c3d280', + false_positives: [], + meta: { + from: '1m', + }, + max_signals: 100, + risk_score: 79, + output_index: '.siem-signals-default', + description: 'matches most events', + from: 'now-360s', + immutable: false, + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + interval: '5m', + language: 'kuery', + name: 'matches host.name exists', + query: 'host.name : *', + references: ['https://google.com'], + severity: 'high', + tags: [ + 'host.name exists', + 'for testing', + '__internal_rule_id:82b2b065-a2ee-49fc-9d6d-781a75c3d280', + '__internal_immutable:false', + ], + type: 'query', + to: 'now', + enabled: true, + filters: [], + created_by: 'elastic', + updated_by: 'elastic', + threat: [ + { + framework: 'MITRE ATT&CK', + technique: [ + { + reference: 'https://attack.mitre.org/techniques/T1110', + name: 'Brute Force', + id: 'T1110', + }, + { + reference: 'https://attack.mitre.org/techniques/T1098', + name: 'Account Manipulation', + id: 'T1098', + }, + { + reference: 'https://attack.mitre.org/techniques/T1081', + name: 'Credentials in Files', + id: 'T1081', + }, + ], + tactic: { + reference: 'https://attack.mitre.org/tactics/TA0006', + name: 'Credential Access', + id: 'TA0006', + }, + }, + { + framework: 'MITRE ATT&CK', + technique: [ + { + reference: 'https://attack.mitre.org/techniques/T1530', + name: 'Data from Cloud Storage Object', + id: 'T1530', + }, + ], + tactic: { + reference: 'https://attack.mitre.org/tactics/TA0009', + name: 'Collection', + id: 'TA0009', + }, + }, + ], + version: 1, + created_at: '2020-02-12T19:49:29.417Z', + updated_at: '2020-02-14T23:15:06.186Z', + }, + original_event: { + kind: 'event', + code: 4625, + created: '2020-02-15T00:09:19.454Z', + module: 'security', + type: 'authentication_failure', + outcome: 'failure', + provider: 'Microsoft-Windows-Security-Auditing', + action: 'logon-failed', + category: 'authentication', + }, + }, + }, + }, + { + _index: '.siem-signals-default-000001', + _id: 'f461e2132bdf3926ef1fe10c83e671707ff3f12348ce600b8490c97a0c704086', + _score: 0, + _source: { + '@timestamp': '2020-02-15T00:15:19.231Z', + source: { + ip: '10.142.0.7', + port: 42774, + packets: 2, + bytes: 80, + }, + server: { + bytes: 10661, + ip: '169.254.169.254', + port: 80, + packets: 3, + }, + service: { + type: 'system', + }, + system: { + audit: { + socket: { + egid: 0, + kernel_sock_address: '0xffff8dd0103d2000', + uid: 0, + gid: 0, + euid: 0, + }, + }, + }, + destination: { + bytes: 10661, + ip: '169.254.169.254', + port: 80, + packets: 3, + }, + host: { + architecture: 'x86_64', + os: { + name: 'Debian GNU/Linux', + kernel: '4.9.0-8-amd64', + codename: 'stretch', + platform: 'debian', + version: '9 (stretch)', + family: 'debian', + }, + id: 'aa7ca589f1b8220002f2fc61c64cfbf1', + containerized: false, + hostname: 'siem-kibana', + name: 'siem-kibana', + }, + agent: { + type: 'auditbeat', + ephemeral_id: '60adc2c2-ab48-4e5c-b557-e73549400a79', + hostname: 'siem-kibana', + id: '03ccb0ce-f65c-4279-a619-05f1d5bb000b', + version: '7.5.0', + }, + client: { + ip: '10.142.0.7', + port: 42774, + packets: 2, + bytes: 80, + }, + cloud: { + machine: { + type: 'n1-standard-2', + }, + availability_zone: 'us-east1-b', + instance: { + name: 'siem-kibana', + id: '5412578377715150143', + }, + project: { + id: 'elastic-beats', + }, + provider: 'gcp', + }, + network: { + type: 'ipv4', + transport: 'tcp', + packets: 5, + bytes: 10741, + community_id: '1:qTY0+fxFYZvNHSUM4xTnCKjq8hM=', + direction: 'outbound', + }, + group: { + name: 'root', + id: '0', + }, + tags: ['7.5.0-bc2'], + ecs: { + version: '1.1.0', + }, + user: { + id: '0', + name: 'root', + }, + event: { + dataset: 'socket', + kind: 'signal', + action: 'network_flow', + category: 'network_traffic', + start: '2020-02-15T00:09:18.360Z', + end: '2020-02-15T00:09:18.361Z', + duration: 746181, + module: 'system', + }, + process: { + pid: 746, + name: 'google_accounts', + args: ['/usr/bin/python3', '/usr/bin/google_accounts_daemon'], + executable: '/usr/bin/python3.5', + created: '2020-02-14T18:31:08.280Z', + }, + flow: { + final: true, + complete: false, + }, + signal: { + parent: { + rule: '2df3a613-f5a8-4b55-bf6a-487fc820b842', + id: '59ctRnABMQha2n6bmhzN', + type: 'event', + index: 'auditbeat-7.5.0-2020.01.14-000002', + depth: 1, + }, + ancestors: [ + { + rule: '2df3a613-f5a8-4b55-bf6a-487fc820b842', + id: '59ctRnABMQha2n6bmhzN', + type: 'event', + index: 'auditbeat-7.5.0-2020.01.14-000002', + depth: 1, + }, + ], + original_time: '2020-02-15T00:09:18.795Z', + status: 'open', + rule: { + id: '2df3a613-f5a8-4b55-bf6a-487fc820b842', + rule_id: '82b2b065-a2ee-49fc-9d6d-781a75c3d280', + false_positives: [], + meta: { + from: '1m', + }, + max_signals: 100, + risk_score: 79, + output_index: '.siem-signals-default', + description: 'matches most events', + from: 'now-360s', + immutable: false, + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + interval: '5m', + language: 'kuery', + name: 'matches host.name exists', + query: 'host.name : *', + references: ['https://google.com'], + severity: 'high', + tags: [ + 'host.name exists', + 'for testing', + '__internal_rule_id:82b2b065-a2ee-49fc-9d6d-781a75c3d280', + '__internal_immutable:false', + ], + type: 'query', + to: 'now', + enabled: true, + filters: [], + created_by: 'elastic', + updated_by: 'elastic', + threat: [ + { + framework: 'MITRE ATT&CK', + technique: [ + { + reference: 'https://attack.mitre.org/techniques/T1110', + name: 'Brute Force', + id: 'T1110', + }, + { + reference: 'https://attack.mitre.org/techniques/T1098', + name: 'Account Manipulation', + id: 'T1098', + }, + { + reference: 'https://attack.mitre.org/techniques/T1081', + name: 'Credentials in Files', + id: 'T1081', + }, + ], + tactic: { + reference: 'https://attack.mitre.org/tactics/TA0006', + name: 'Credential Access', + id: 'TA0006', + }, + }, + { + framework: 'MITRE ATT&CK', + technique: [ + { + reference: 'https://attack.mitre.org/techniques/T1530', + name: 'Data from Cloud Storage Object', + id: 'T1530', + }, + ], + tactic: { + reference: 'https://attack.mitre.org/tactics/TA0009', + name: 'Collection', + id: 'TA0009', + }, + }, + ], + version: 1, + created_at: '2020-02-12T19:49:29.417Z', + updated_at: '2020-02-14T23:15:06.186Z', + }, + original_event: { + dataset: 'socket', + kind: 'event', + action: 'network_flow', + category: 'network_traffic', + start: '2020-02-15T00:09:18.360Z', + end: '2020-02-15T00:09:18.361Z', + duration: 746181, + module: 'system', + }, + }, + }, + }, + { + _index: '.siem-signals-default-000001', + _id: '428551fed9382740e808f27ea64ce53b4d3b8cc82401d83afd47969339a0f6e3', + _score: 0, + _source: { + '@timestamp': '2020-02-15T00:15:19.231Z', + service: { + type: 'system', + }, + message: 'Process sleep (PID: 317535) by user root STARTED', + ecs: { + version: '1.0.0', + }, + host: { + name: 'beats-ci-immutable-ubuntu-1604-1581723302100990071', + hostname: 'beats-ci-immutable-ubuntu-1604-1581723302100990071', + architecture: 'x86_64', + os: { + platform: 'ubuntu', + version: '16.04.6 LTS (Xenial Xerus)', + family: 'debian', + name: 'Ubuntu', + kernel: '4.15.0-1052-gcp', + codename: 'xenial', + }, + id: 'c428794c81ade2eb0633d2bbea7ecf51', + containerized: false, + }, + cloud: { + machine: { + type: 'n1-highmem-4', + }, + availability_zone: 'us-central1-b', + project: { + id: 'elastic-ci-prod', + }, + provider: 'gcp', + instance: { + id: '5167639562480685129', + name: 'beats-ci-immutable-ubuntu-1604-1581723302100990071', + }, + }, + event: { + kind: 'signal', + action: 'process_started', + module: 'system', + dataset: 'process', + }, + process: { + executable: '/bin/sleep', + start: '2020-02-15T00:09:17.850Z', + args: ['sleep', '1'], + working_directory: '/', + name: 'sleep', + ppid: 239348, + pid: 317535, + hash: { + sha1: '9dc3644a028d1a4c853924c427f5e7d668c38ef7', + }, + entity_id: 'vtgDN10edfL0mX5p', + }, + user: { + id: '0', + group: { + id: '0', + name: 'root', + }, + effective: { + id: '0', + group: { + id: '0', + }, + }, + saved: { + id: '0', + group: { + id: '0', + }, + }, + name: 'root', + }, + agent: { + id: '4ae34f08-4770-4e5b-bd5b-c8b13741eafa', + version: '7.2.0', + type: 'auditbeat', + ephemeral_id: '3b3939af-dc90-4be8-b20b-a3d9f555d379', + hostname: 'beats-ci-immutable-ubuntu-1604-1581723302100990071', + }, + signal: { + parent: { + rule: '2df3a613-f5a8-4b55-bf6a-487fc820b842', + id: '7tctRnABMQha2n6bnxxQ', + type: 'event', + index: 'auditbeat-7.2.0', + depth: 1, + }, + ancestors: [ + { + rule: '2df3a613-f5a8-4b55-bf6a-487fc820b842', + id: '7tctRnABMQha2n6bnxxQ', + type: 'event', + index: 'auditbeat-7.2.0', + depth: 1, + }, + ], + original_time: '2020-02-15T00:09:18.860Z', + status: 'open', + rule: { + id: '2df3a613-f5a8-4b55-bf6a-487fc820b842', + rule_id: '82b2b065-a2ee-49fc-9d6d-781a75c3d280', + false_positives: [], + meta: { + from: '1m', + }, + max_signals: 100, + risk_score: 79, + output_index: '.siem-signals-default', + description: 'matches most events', + from: 'now-360s', + immutable: false, + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + interval: '5m', + language: 'kuery', + name: 'matches host.name exists', + query: 'host.name : *', + references: ['https://google.com'], + severity: 'high', + tags: [ + 'host.name exists', + 'for testing', + '__internal_rule_id:82b2b065-a2ee-49fc-9d6d-781a75c3d280', + '__internal_immutable:false', + ], + type: 'query', + to: 'now', + enabled: true, + filters: [], + created_by: 'elastic', + updated_by: 'elastic', + threat: [ + { + framework: 'MITRE ATT&CK', + technique: [ + { + reference: 'https://attack.mitre.org/techniques/T1110', + name: 'Brute Force', + id: 'T1110', + }, + { + reference: 'https://attack.mitre.org/techniques/T1098', + name: 'Account Manipulation', + id: 'T1098', + }, + { + reference: 'https://attack.mitre.org/techniques/T1081', + name: 'Credentials in Files', + id: 'T1081', + }, + ], + tactic: { + reference: 'https://attack.mitre.org/tactics/TA0006', + name: 'Credential Access', + id: 'TA0006', + }, + }, + { + framework: 'MITRE ATT&CK', + technique: [ + { + reference: 'https://attack.mitre.org/techniques/T1530', + name: 'Data from Cloud Storage Object', + id: 'T1530', + }, + ], + tactic: { + reference: 'https://attack.mitre.org/tactics/TA0009', + name: 'Collection', + id: 'TA0009', + }, + }, + ], + version: 1, + created_at: '2020-02-12T19:49:29.417Z', + updated_at: '2020-02-14T23:15:06.186Z', + }, + original_event: { + kind: 'event', + action: 'process_started', + module: 'system', + dataset: 'process', + }, + }, + }, + }, + { + _index: '.siem-signals-default-000001', + _id: '9f6d771532d8f2b314c65b5007b1b9e2fcd206dca352b9b244c971341a09f5ce', + _score: 0, + _source: { + '@timestamp': '2020-02-15T00:15:19.231Z', + service: { + type: 'system', + }, + event: { + dataset: 'process', + kind: 'signal', + action: 'process_error', + module: 'system', + }, + message: + 'ERROR for PID 317759: failed to hash executable / for PID 317759: failed to calculate file hashes: read /: is a directory', + cloud: { + instance: { + id: '5167639562480685129', + name: 'beats-ci-immutable-ubuntu-1604-1581723302100990071', + }, + machine: { + type: 'n1-highmem-4', + }, + availability_zone: 'us-central1-b', + project: { + id: 'elastic-ci-prod', + }, + provider: 'gcp', + }, + host: { + architecture: 'x86_64', + os: { + platform: 'ubuntu', + version: '16.04.6 LTS (Xenial Xerus)', + family: 'debian', + name: 'Ubuntu', + kernel: '4.15.0-1052-gcp', + codename: 'xenial', + }, + name: 'beats-ci-immutable-ubuntu-1604-1581723302100990071', + id: 'c428794c81ade2eb0633d2bbea7ecf51', + containerized: false, + hostname: 'beats-ci-immutable-ubuntu-1604-1581723302100990071', + }, + agent: { + ephemeral_id: '3b3939af-dc90-4be8-b20b-a3d9f555d379', + hostname: 'beats-ci-immutable-ubuntu-1604-1581723302100990071', + id: '4ae34f08-4770-4e5b-bd5b-c8b13741eafa', + version: '7.2.0', + type: 'auditbeat', + }, + error: { + message: + 'failed to hash executable / for PID 317759: failed to calculate file hashes: read /: is a directory', + }, + process: { + entity_id: 'ahsj04Ppla09U8Q2', + name: 'runc:[2:INIT]', + args: ['runc', 'init'], + pid: 317759, + ppid: 317706, + working_directory: '/', + executable: '/', + start: '2020-02-15T00:09:18.360Z', + }, + user: { + name: 'root', + id: '0', + group: { + id: '0', + name: 'root', + }, + effective: { + id: '0', + group: { + id: '0', + }, + }, + saved: { + id: '0', + group: { + id: '0', + }, + }, + }, + ecs: { + version: '1.0.0', + }, + signal: { + parent: { + rule: '2df3a613-f5a8-4b55-bf6a-487fc820b842', + id: '79ctRnABMQha2n6bnxxQ', + type: 'event', + index: 'auditbeat-7.2.0', + depth: 1, + }, + ancestors: [ + { + rule: '2df3a613-f5a8-4b55-bf6a-487fc820b842', + id: '79ctRnABMQha2n6bnxxQ', + type: 'event', + index: 'auditbeat-7.2.0', + depth: 1, + }, + ], + original_time: '2020-02-15T00:09:18.860Z', + status: 'open', + rule: { + id: '2df3a613-f5a8-4b55-bf6a-487fc820b842', + rule_id: '82b2b065-a2ee-49fc-9d6d-781a75c3d280', + false_positives: [], + meta: { + from: '1m', + }, + max_signals: 100, + risk_score: 79, + output_index: '.siem-signals-default', + description: 'matches most events', + from: 'now-360s', + immutable: false, + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + interval: '5m', + language: 'kuery', + name: 'matches host.name exists', + query: 'host.name : *', + references: ['https://google.com'], + severity: 'high', + tags: [ + 'host.name exists', + 'for testing', + '__internal_rule_id:82b2b065-a2ee-49fc-9d6d-781a75c3d280', + '__internal_immutable:false', + ], + type: 'query', + to: 'now', + enabled: true, + filters: [], + created_by: 'elastic', + updated_by: 'elastic', + threat: [ + { + framework: 'MITRE ATT&CK', + technique: [ + { + reference: 'https://attack.mitre.org/techniques/T1110', + name: 'Brute Force', + id: 'T1110', + }, + { + reference: 'https://attack.mitre.org/techniques/T1098', + name: 'Account Manipulation', + id: 'T1098', + }, + { + reference: 'https://attack.mitre.org/techniques/T1081', + name: 'Credentials in Files', + id: 'T1081', + }, + ], + tactic: { + reference: 'https://attack.mitre.org/tactics/TA0006', + name: 'Credential Access', + id: 'TA0006', + }, + }, + { + framework: 'MITRE ATT&CK', + technique: [ + { + reference: 'https://attack.mitre.org/techniques/T1530', + name: 'Data from Cloud Storage Object', + id: 'T1530', + }, + ], + tactic: { + reference: 'https://attack.mitre.org/tactics/TA0009', + name: 'Collection', + id: 'TA0009', + }, + }, + ], + version: 1, + created_at: '2020-02-12T19:49:29.417Z', + updated_at: '2020-02-14T23:15:06.186Z', + }, + original_event: { + dataset: 'process', + kind: 'error', + action: 'process_error', + module: 'system', + }, + }, + }, + }, + ], + }, + aggregations: { + signalsByGrouping: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: '4', + doc_count: 12600, + signals: { + buckets: [ + { + key_as_string: '2020-01-21T04:30:00.000Z', + key: 1579581000000, + doc_count: 0, + }, + { + key_as_string: '2020-01-22T03:00:00.000Z', + key: 1579662000000, + doc_count: 0, + }, + { + key_as_string: '2020-01-23T01:30:00.000Z', + key: 1579743000000, + doc_count: 0, + }, + { + key_as_string: '2020-01-24T00:00:00.000Z', + key: 1579824000000, + doc_count: 0, + }, + ], + }, + }, + ], + }, + }, +}; + +export const mockSignalsQuery: object = { + aggs: { + signalsByGrouping: { + terms: { + field: 'signal.rule.risk_score', + missing: 'All others', + order: { _count: 'desc' }, + size: 10, + }, + aggs: { + signals: { + date_histogram: { + field: '@timestamp', + fixed_interval: '81000000ms', + min_doc_count: 0, + extended_bounds: { min: 1579644343954, max: 1582236343955 }, + }, + }, + }, + }, + }, + query: { + bool: { + filter: [ + { bool: { must: [], filter: [{ match_all: {} }], should: [], must_not: [] } }, + { range: { '@timestamp': { gte: 1579644343954, lte: 1582236343955 } } }, + ], + }, + }, +}; + +export const mockStatusSignalQuery: object = { + bool: { + filter: { + terms: { _id: ['b4ee5c32e3a321057edcc953ca17228c6fdfe5ba43fdbbdaffa8cefa11605cc5'] }, + }, + }, +}; + +export const mockSignalIndex: SignalsIndex = { + name: 'mock-signal-index', +}; + +export const mockUserPrivilege: Privilege = { + username: 'elastic', + has_all_requested: false, + cluster: { + monitor_ml: true, + manage_ccr: true, + manage_index_templates: true, + monitor_watcher: true, + monitor_transform: true, + read_ilm: true, + manage_api_key: true, + manage_security: true, + manage_own_api_key: false, + manage_saml: true, + all: true, + manage_ilm: true, + manage_ingest_pipelines: true, + read_ccr: true, + manage_rollup: true, + monitor: true, + manage_watcher: true, + manage: true, + manage_transform: true, + manage_token: true, + manage_ml: true, + manage_pipeline: true, + monitor_rollup: true, + transport_client: true, + create_snapshot: true, + }, + index: { + '.siem-signals-default': { + all: true, + manage_ilm: true, + read: true, + create_index: true, + read_cross_cluster: true, + index: true, + monitor: true, + delete: true, + manage: true, + delete_index: true, + create_doc: true, + view_index_metadata: true, + create: true, + manage_follow_index: true, + manage_leader_index: true, + write: true, + }, + }, + is_authenticated: true, + has_encryption_key: true, +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts index 752de13567e5c..d90f94d32001d 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './errors_types'; - export interface BasicSignals { signal: AbortSignal; } diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.test.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.test.tsx new file mode 100644 index 0000000000000..2682742960442 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.test.tsx @@ -0,0 +1,70 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { usePrivilegeUser, ReturnPrivilegeUser } from './use_privilege_user'; +import * as api from './api'; + +jest.mock('./api'); + +describe('usePrivilegeUser', () => { + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePrivilegeUser() + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + hasEncryptionKey: null, + hasIndexManage: null, + hasIndexWrite: null, + hasManageApiKey: null, + isAuthenticated: null, + loading: true, + }); + }); + }); + + test('fetch user privilege', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePrivilegeUser() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + hasEncryptionKey: true, + hasIndexManage: true, + hasIndexWrite: true, + hasManageApiKey: true, + isAuthenticated: true, + loading: false, + }); + }); + }); + + test('if there is an error when fetching user privilege, we should get back false for every properties', async () => { + const spyOnGetUserPrivilege = jest.spyOn(api, 'getUserPrivilege'); + spyOnGetUserPrivilege.mockImplementation(() => { + throw new Error('Something went wrong, let see what happen'); + }); + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePrivilegeUser() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + hasEncryptionKey: false, + hasIndexManage: false, + hasIndexWrite: false, + hasManageApiKey: false, + isAuthenticated: false, + loading: false, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx index 55f3386b503d8..c58e62c062fae 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx @@ -6,12 +6,11 @@ import { useEffect, useState } from 'react'; -import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; -import { useStateToaster } from '../../../components/toasters'; +import { errorToToaster, useStateToaster } from '../../../components/toasters'; import { getUserPrivilege } from './api'; import * as i18n from './translations'; -interface Return { +export interface ReturnPrivilegeUser { loading: boolean; isAuthenticated: boolean | null; hasEncryptionKey: boolean | null; @@ -23,11 +22,11 @@ interface Return { * Hook to get user privilege from * */ -export const usePrivilegeUser = (): Return => { +export const usePrivilegeUser = (): ReturnPrivilegeUser => { const [loading, setLoading] = useState(true); const [privilegeUser, setPrivilegeUser] = useState< Pick< - Return, + ReturnPrivilegeUser, | 'isAuthenticated' | 'hasEncryptionKey' | 'hasIndexManage' diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.test.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.test.tsx new file mode 100644 index 0000000000000..b0440cfb8373f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.test.tsx @@ -0,0 +1,130 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { useQuerySignals, ReturnQuerySignals } from './use_query'; +import * as api from './api'; +import { mockSignalsQuery, signalsMock } from './mock'; + +jest.mock('./api'); + +describe('useQuerySignals', () => { + const indexName = 'mock-index-name'; + beforeEach(() => { + jest.resetAllMocks(); + }); + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + [object, string], + ReturnQuerySignals + >(() => useQuerySignals(mockSignalsQuery, indexName)); + await waitForNextUpdate(); + expect(result.current).toEqual({ + loading: true, + data: null, + response: '', + request: '', + setQuery: result.current.setQuery, + refetch: null, + }); + }); + }); + + test('fetch signals data', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + [object, string], + ReturnQuerySignals + >(() => useQuerySignals(mockSignalsQuery, indexName)); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + loading: false, + data: signalsMock, + response: JSON.stringify(signalsMock, null, 2), + request: JSON.stringify({ index: [indexName] ?? [''], body: mockSignalsQuery }, null, 2), + setQuery: result.current.setQuery, + refetch: result.current.refetch, + }); + }); + }); + + test('re-fetch signals data', async () => { + const spyOnfetchQuerySignals = jest.spyOn(api, 'fetchQuerySignals'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + [object, string], + ReturnQuerySignals + >(() => useQuerySignals(mockSignalsQuery, indexName)); + await waitForNextUpdate(); + await waitForNextUpdate(); + if (result.current.refetch) { + result.current.refetch(); + } + await waitForNextUpdate(); + expect(spyOnfetchQuerySignals).toHaveBeenCalledTimes(2); + }); + }); + + test('fetch signal when index name changed', async () => { + const spyOnfetchRules = jest.spyOn(api, 'fetchQuerySignals'); + await act(async () => { + const { rerender, waitForNextUpdate } = renderHook< + [object, string], + ReturnQuerySignals + >(args => useQuerySignals(args[0], args[1]), { + initialProps: [mockSignalsQuery, indexName], + }); + await waitForNextUpdate(); + await waitForNextUpdate(); + rerender([mockSignalsQuery, 'new-mock-index-name']); + await waitForNextUpdate(); + expect(spyOnfetchRules).toHaveBeenCalledTimes(2); + }); + }); + + test('fetch signal when query object changed', async () => { + const spyOnfetchRules = jest.spyOn(api, 'fetchQuerySignals'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + [object, string], + ReturnQuerySignals + >(args => useQuerySignals(args[0], args[1]), { + initialProps: [mockSignalsQuery, indexName], + }); + await waitForNextUpdate(); + await waitForNextUpdate(); + if (result.current.setQuery) { + result.current.setQuery({ ...mockSignalsQuery }); + } + await waitForNextUpdate(); + expect(spyOnfetchRules).toHaveBeenCalledTimes(2); + }); + }); + + test('if there is an error when fetching data, we should get back the init value for every properties', async () => { + const spyOnGetUserPrivilege = jest.spyOn(api, 'fetchQuerySignals'); + spyOnGetUserPrivilege.mockImplementation(() => { + throw new Error('Something went wrong, let see what happen'); + }); + await act(async () => { + const { result, waitForNextUpdate } = renderHook>( + () => useQuerySignals(mockSignalsQuery, 'mock-index-name') + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + loading: false, + data: null, + response: '', + request: '', + setQuery: result.current.setQuery, + refetch: result.current.refetch, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.tsx index 45f191f4a6fe5..531e080ed7d1f 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.tsx @@ -11,7 +11,7 @@ import { SignalSearchResponse } from './types'; type Func = () => void; -interface Return { +export interface ReturnQuerySignals { loading: boolean; data: SignalSearchResponse | null; setQuery: React.Dispatch>; @@ -29,10 +29,10 @@ interface Return { export const useQuerySignals = ( initialQuery: object, indexName?: string | null -): Return => { +): ReturnQuerySignals => { const [query, setQuery] = useState(initialQuery); const [signals, setSignals] = useState< - Pick, 'data' | 'setQuery' | 'response' | 'request' | 'refetch'> + Pick, 'data' | 'setQuery' | 'response' | 'request' | 'refetch'> >({ data: null, response: '', diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.test.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.test.tsx new file mode 100644 index 0000000000000..c834e4ab14be2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.test.tsx @@ -0,0 +1,127 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { useSignalIndex, ReturnSignalIndex } from './use_signal_index'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useSignalIndex', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + }); + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useSignalIndex() + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + createDeSignalIndex: null, + loading: true, + signalIndexExists: null, + signalIndexName: null, + }); + }); + }); + + test('fetch signals info', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useSignalIndex() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + createDeSignalIndex: result.current.createDeSignalIndex, + loading: false, + signalIndexExists: true, + signalIndexName: 'mock-signal-index', + }); + }); + }); + + test('make sure that createSignalIndex is giving back the signal info', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useSignalIndex() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + if (result.current.createDeSignalIndex != null) { + await result.current.createDeSignalIndex(); + } + await waitForNextUpdate(); + expect(result.current).toEqual({ + createDeSignalIndex: result.current.createDeSignalIndex, + loading: false, + signalIndexExists: true, + signalIndexName: 'mock-signal-index', + }); + }); + }); + + test('make sure that createSignalIndex have been called when trying to create signal index', async () => { + const spyOnCreateSignalIndex = jest.spyOn(api, 'createSignalIndex'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useSignalIndex() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + if (result.current.createDeSignalIndex != null) { + await result.current.createDeSignalIndex(); + } + await waitForNextUpdate(); + expect(spyOnCreateSignalIndex).toHaveBeenCalledTimes(1); + }); + }); + + test('if there is an error during createSignalIndex, we should get back signalIndexExists === false && signalIndexName == null', async () => { + const spyOnCreateSignalIndex = jest.spyOn(api, 'createSignalIndex'); + spyOnCreateSignalIndex.mockImplementation(() => { + throw new Error('Something went wrong, let see what happen'); + }); + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useSignalIndex() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + if (result.current.createDeSignalIndex != null) { + await result.current.createDeSignalIndex(); + } + expect(result.current).toEqual({ + createDeSignalIndex: result.current.createDeSignalIndex, + loading: false, + signalIndexExists: false, + signalIndexName: null, + }); + }); + }); + + test('if there is an error when fetching signals info, signalIndexExists === false && signalIndexName == null', async () => { + const spyOnGetSignalIndex = jest.spyOn(api, 'getSignalIndex'); + spyOnGetSignalIndex.mockImplementation(() => { + throw new Error('Something went wrong, let see what happen'); + }); + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useSignalIndex() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + createDeSignalIndex: result.current.createDeSignalIndex, + loading: false, + signalIndexExists: false, + signalIndexName: null, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx index 813bd2483689c..a7f5c9731320e 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx @@ -6,15 +6,14 @@ import { useEffect, useState } from 'react'; -import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; -import { useStateToaster } from '../../../components/toasters'; +import { errorToToaster, useStateToaster } from '../../../components/toasters'; import { createSignalIndex, getSignalIndex } from './api'; import * as i18n from './translations'; -import { PostSignalError, SignalIndexError } from './types'; +import { isApiError } from '../../../utils/api'; type Func = () => void; -interface Return { +export interface ReturnSignalIndex { loading: boolean; signalIndexExists: boolean | null; signalIndexName: string | null; @@ -26,10 +25,10 @@ interface Return { * * */ -export const useSignalIndex = (): Return => { +export const useSignalIndex = (): ReturnSignalIndex => { const [loading, setLoading] = useState(true); const [signalIndex, setSignalIndex] = useState< - Pick + Pick >({ signalIndexExists: null, signalIndexName: null, @@ -60,7 +59,7 @@ export const useSignalIndex = (): Return => { signalIndexName: null, createDeSignalIndex: createIndex, }); - if (error instanceof SignalIndexError && error.status_code !== 404) { + if (isApiError(error) && error.body.status_code !== 404) { errorToToaster({ title: i18n.SIGNAL_GET_NAME_FAILURE, error, dispatchToaster }); } } @@ -82,7 +81,7 @@ export const useSignalIndex = (): Return => { } } catch (error) { if (isSubscribed) { - if (error instanceof PostSignalError && error.statusCode === 409) { + if (isApiError(error) && error.body.status_code === 409) { fetchData(); } else { setSignalIndex({ diff --git a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.test.tsx b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.test.tsx index 06367ab8657a8..80899a061e7c1 100644 --- a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.test.tsx @@ -8,7 +8,7 @@ import { useQuery } from '.'; import { mount } from 'enzyme'; import React from 'react'; import { useApolloClient } from '../../utils/apollo_context'; -import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import { errorToToaster } from '../../components/toasters'; import { MatrixOverTimeHistogramData, HistogramType } from '../../graphql/types'; import { InspectQuery, Refetch } from '../../store/inputs/model'; @@ -41,7 +41,10 @@ jest.mock('./index.gql_query', () => { }; }); -jest.mock('../../components/ml/api/error_to_toaster'); +jest.mock('../../components/toasters/', () => ({ + useStateToaster: () => [jest.fn(), jest.fn()], + errorToToaster: jest.fn(), +})); describe('useQuery', () => { let result: { diff --git a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.ts b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.ts index 683d5b68c305b..0b369b4180fb8 100644 --- a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.ts +++ b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.ts @@ -6,8 +6,7 @@ import { useEffect, useState, useRef } from 'react'; import { MatrixHistogramQueryProps } from '../../components/matrix_histogram/types'; import { DEFAULT_INDEX_KEY } from '../../../common/constants'; -import { useStateToaster } from '../../components/toasters'; -import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import { errorToToaster, useStateToaster } from '../../components/toasters'; import { useUiSetting$ } from '../../lib/kibana'; import { createFilter } from '../helpers'; import { useApolloClient } from '../../utils/apollo_context'; diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/api.test.ts b/x-pack/legacy/plugins/siem/public/hooks/api/api.test.ts deleted file mode 100644 index 208a3b14ca283..0000000000000 --- a/x-pack/legacy/plugins/siem/public/hooks/api/api.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 fetchMock from 'fetch-mock'; -import { throwIfNotOk } from './api'; - -describe('api', () => { - afterEach(() => { - fetchMock.reset(); - }); - - describe('#throwIfNotOk', () => { - test('throws a network error if there is no response', async () => { - await expect(throwIfNotOk()).rejects.toThrow('Network Error'); - }); - - test('does a throw if it is given response that is not ok and the body is not parsable', async () => { - fetchMock.mock('http://example.com', 500); - const response = await fetch('http://example.com'); - await expect(throwIfNotOk(response)).rejects.toThrow('Network Error: Internal Server Error'); - }); - - test('does a throw and returns a body if it is parsable', async () => { - fetchMock.mock('http://example.com', { - status: 500, - body: { - statusCode: 500, - message: 'I am a custom message', - }, - }); - const response = await fetch('http://example.com'); - await expect(throwIfNotOk(response)).rejects.toThrow('I am a custom message'); - }); - - test('does NOT do a throw if it is given response is not ok', async () => { - fetchMock.mock('http://example.com', 200); - const response = await fetch('http://example.com'); - await expect(throwIfNotOk(response)).resolves.toEqual(undefined); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx b/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx index 1dfd6416531ee..8120e3819d9a8 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx +++ b/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as i18n from '../translations'; import { StartServices } from '../../plugin'; -import { parseJsonFromBody, ToasterErrors } from './throw_if_not_ok'; import { IndexPatternSavedObject, IndexPatternSavedObjectAttributes } from '../types'; /** @@ -25,24 +23,3 @@ export const getIndexPatterns = async ( return response.savedObjects; }; - -export const throwIfNotOk = async (response?: Response): Promise => { - if (!response) { - throw new ToasterErrors([i18n.NETWORK_ERROR]); - } - - if (!response.ok) { - const body = await parseJsonFromBody(response); - if (body != null && body.message) { - if (body.statusCode != null) { - throw new ToasterErrors([body.message, `${i18n.STATUS_CODE} ${body.statusCode}`]); - } else if (body.status_code != null) { - throw new ToasterErrors([body.message, `${i18n.STATUS_CODE} ${body.status_code}`]); - } else { - throw new ToasterErrors([body.message]); - } - } else { - throw new ToasterErrors([`${i18n.NETWORK_ERROR} ${response.statusText}`]); - } - } -}; diff --git a/x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx b/x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx index e10d4873f1b6e..05b0521e35217 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx +++ b/x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx @@ -7,8 +7,7 @@ import { useEffect, useState } from 'react'; import { useKibana } from '../lib/kibana'; -import { useStateToaster } from '../components/toasters'; -import { errorToToaster } from '../components/ml/api/error_to_toaster'; +import { errorToToaster, useStateToaster } from '../components/toasters'; import * as i18n from './translations'; import { IndexPatternSavedObject } from './types'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx index a17fd34d1c344..bc5d0c32bb9c6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx @@ -20,6 +20,7 @@ import { ActionToaster, displayErrorToast, displaySuccessToast, + errorToToaster, } from '../../../../components/toasters'; import { track, METRIC_TYPE, TELEMETRY_EVENT } from '../../../../lib/telemetry'; @@ -50,9 +51,9 @@ export const duplicateRulesAction = async ( displaySuccessToast(i18n.SUCCESSFULLY_DUPLICATED_RULES(ruleIds.length), dispatchToaster); } dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); - } catch (e) { + } catch (error) { dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); - displayErrorToast(i18n.DUPLICATE_RULE_ERROR, [e.message], dispatchToaster); + errorToToaster({ title: i18n.DUPLICATE_RULE_ERROR, error, dispatchToaster }); } }; @@ -80,13 +81,13 @@ export const deleteRulesAction = async ( } else if (onRuleDeleted) { onRuleDeleted(); } - } catch (e) { + } catch (error) { dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); - displayErrorToast( - i18n.BATCH_ACTION_DELETE_SELECTED_ERROR(ruleIds.length), - [e.message], - dispatchToaster - ); + errorToToaster({ + title: i18n.BATCH_ACTION_DELETE_SELECTED_ERROR(ruleIds.length), + error, + dispatchToaster, + }); } }; 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 5ce26144a4d9c..0ebeb84d57468 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 @@ -5,17 +5,16 @@ */ import { - Rule, - RuleError, + BulkRuleResponse, RuleResponseBuckets, } from '../../../../containers/detection_engine/rules'; /** * Separates rules/errors from bulk rules API response (create/update/delete) * - * @param response Array from bulk rules API + * @param response BulkRuleResponse from bulk rules API */ -export const bucketRulesResponse = (response: Array) => +export const bucketRulesResponse = (response: BulkRuleResponse) => response.reduce( (acc, cv): RuleResponseBuckets => { return 'error' in cv diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx index 97649fb03dac0..ef42b5097e364 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx @@ -26,6 +26,7 @@ import { displayErrorToast, displaySuccessToast, useStateToaster, + errorToToaster, } from '../../../../../components/toasters'; import * as i18n from './translations'; @@ -83,9 +84,9 @@ export const ImportRuleModalComponent = ({ importComplete(); cleanupAndCloseModal(); - } catch (e) { + } catch (error) { cleanupAndCloseModal(); - displayErrorToast(i18n.IMPORT_FAILED, [e.message], dispatchToaster); + errorToToaster({ title: i18n.IMPORT_FAILED, error, dispatchToaster }); } } }, [selectedFiles, overwrite]); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_downloader/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_downloader/index.tsx index 5d3086051a6e2..959864d50747f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_downloader/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_downloader/index.tsx @@ -8,7 +8,7 @@ import React, { useEffect, useRef } from 'react'; import styled from 'styled-components'; import { isFunction } from 'lodash/fp'; import { exportRules } from '../../../../../containers/detection_engine/rules'; -import { displayErrorToast, useStateToaster } from '../../../../../components/toasters'; +import { useStateToaster, errorToToaster } from '../../../../../components/toasters'; import * as i18n from './translations'; const InvisibleAnchor = styled.a` @@ -65,7 +65,7 @@ export const RuleDownloaderComponent = ({ } } catch (error) { if (isSubscribed) { - displayErrorToast(i18n.EXPORT_FAILURE, [error.message], dispatchToaster); + errorToToaster({ title: i18n.EXPORT_FAILURE, error, dispatchToaster }); } } } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx index 09b7ecc9df982..44845ea68d954 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx @@ -15,10 +15,12 @@ import { isEmpty } from 'lodash/fp'; import styled from 'styled-components'; import React, { useCallback, useState, useEffect } from 'react'; +import * as i18n from '../../translations'; import { enableRules } from '../../../../../containers/detection_engine/rules'; import { enableRulesAction } from '../../all/actions'; import { Action } from '../../all/reducer'; -import { useStateToaster } from '../../../../../components/toasters'; +import { useStateToaster, displayErrorToast } from '../../../../../components/toasters'; +import { bucketRulesResponse } from '../../all/helpers'; const StaticSwitch = styled(EuiSwitch)` .euiSwitch__thumb, @@ -62,13 +64,29 @@ export const RuleSwitchComponent = ({ await enableRulesAction([id], event.target.checked!, dispatch, dispatchToaster); } else { try { - const updatedRules = await enableRules({ + const enabling = event.target.checked!; + const response = await enableRules({ ids: [id], - enabled: event.target.checked!, + enabled: enabling, }); - setMyEnabled(updatedRules[0].enabled); - if (onChange != null) { - onChange(updatedRules[0].enabled); + const { rules, errors } = bucketRulesResponse(response); + + if (errors.length > 0) { + setMyIsLoading(false); + const title = enabling + ? i18n.BATCH_ACTION_ACTIVATE_SELECTED_ERROR(1) + : i18n.BATCH_ACTION_DEACTIVATE_SELECTED_ERROR(1); + displayErrorToast( + title, + errors.map(e => e.error.message), + dispatchToaster + ); + } else { + const [rule] = rules; + setMyEnabled(rule.enabled); + if (onChange != null) { + onChange(rule.enabled); + } } } catch { setMyIsLoading(false); diff --git a/x-pack/legacy/plugins/siem/public/utils/api/index.ts b/x-pack/legacy/plugins/siem/public/utils/api/index.ts index 3c70083136505..e47e03ce4e627 100644 --- a/x-pack/legacy/plugins/siem/public/utils/api/index.ts +++ b/x-pack/legacy/plugins/siem/public/utils/api/index.ts @@ -4,18 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface MessageBody { - error?: string; - message?: string; - statusCode?: number; - status_code?: number; +import { has } from 'lodash/fp'; + +export interface KibanaApiError { + message: string; + body: { + message: string; + status_code: number; + }; } -export const parseJsonFromBody = async (response: Response): Promise => { - try { - const text = await response.text(); - return JSON.parse(text); - } catch (error) { - return null; - } -}; +export const isApiError = (error: unknown): error is KibanaApiError => + has('message', error) && has('body.message', error) && has('body.status_code', error); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/errors_types/index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/errors/bad_request_error.ts similarity index 68% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/errors_types/index.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/errors/bad_request_error.ts index 4ce8e6ba89183..2ad3bbf759ad7 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/errors_types/index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/errors/bad_request_error.ts @@ -4,6 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './get_index_error'; -export * from './post_index_error'; -export * from './privilege_user_error'; +export class BadRequestError extends Error {} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts index f18e158db4269..6768e9534a87e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts @@ -8,6 +8,7 @@ import Boom from 'boom'; import { SavedObjectsFindResponse } from 'kibana/server'; import { IRuleSavedAttributesSavedObjectAttributes, IRuleStatusAttributes } from '../rules/types'; +import { BadRequestError } from '../errors/bad_request_error'; import { transformError, transformBulkError, @@ -70,8 +71,8 @@ describe('utils', () => { }); }); - test('it detects a TypeError and returns a status code of 400 from that particular error type', () => { - const error: TypeError = new TypeError('I have a type error'); + test('it detects a BadRequestError and returns a status code of 400 from that particular error type', () => { + const error: BadRequestError = new BadRequestError('I have a type error'); const transformed = transformError(error); expect(transformed).toEqual({ message: 'I have a type error', @@ -79,8 +80,8 @@ describe('utils', () => { }); }); - test('it detects a TypeError and returns a Boom status of 400', () => { - const error: TypeError = new TypeError('I have a type error'); + test('it detects a BadRequestError and returns a Boom status of 400', () => { + const error: BadRequestError = new BadRequestError('I have a type error'); const transformed = transformError(error); expect(transformed).toEqual({ message: 'I have a type error', @@ -127,8 +128,8 @@ describe('utils', () => { expect(transformed).toEqual(expected); }); - test('it detects a TypeError and returns a Boom status of 400', () => { - const error: TypeError = new TypeError('I have a type error'); + test('it detects a BadRequestError and returns a Boom status of 400', () => { + const error: BadRequestError = new BadRequestError('I have a type error'); const transformed = transformBulkError('rule-1', error); const expected: BulkError = { rule_id: 'rule-1', @@ -279,8 +280,8 @@ describe('utils', () => { expect(transformed).toEqual(expected); }); - test('it detects a TypeError and returns a Boom status of 400', () => { - const error: TypeError = new TypeError('I have a type error'); + test('it detects a BadRequestError and returns a Boom status of 400', () => { + const error: BadRequestError = new BadRequestError('I have a type error'); const transformed = transformImportError('rule-1', error, { success_count: 1, success: false, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index 6c98517c4dc0c..79c2f47658f7e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -13,6 +13,7 @@ import { KibanaResponseFactory, CustomHttpResponseOptions, } from '../../../../../../../../src/core/server'; +import { BadRequestError } from '../errors/bad_request_error'; export interface OutputError { message: string; @@ -31,9 +32,8 @@ export const transformError = (err: Error & { statusCode?: number }): OutputErro message: err.message, statusCode: err.statusCode, }; - } else if (err instanceof TypeError) { - // allows us to throw type errors instead of booms in some conditions - // where we don't want to mingle Boom with the rest of the code + } else if (err instanceof BadRequestError) { + // allows us to throw request validation errors in the absence of Boom return { message: err.message, statusCode: 400, @@ -178,7 +178,7 @@ export const transformImportError = ( message: err.message, existingImportSuccessError, }); - } else if (err instanceof TypeError) { + } else if (err instanceof BadRequestError) { return createImportErrorObject({ ruleId, statusCode: 400, @@ -205,7 +205,7 @@ export const transformBulkError = ( statusCode: err.output.statusCode, message: err.message, }); - } else if (err instanceof TypeError) { + } else if (err instanceof BadRequestError) { return createBulkErrorObject({ ruleId, statusCode: 400, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts index b1dc62f6fc90f..8705682f61bcc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts @@ -7,6 +7,7 @@ import { Readable } from 'stream'; import { createRulesStreamFromNdJson } from './create_rules_stream_from_ndjson'; import { createPromiseFromStreams } from 'src/legacy/utils/streams'; import { ImportRuleAlertRest } from '../types'; +import { BadRequestError } from '../errors/bad_request_error'; type PromiseFromStreams = ImportRuleAlertRest | Error; @@ -331,7 +332,7 @@ describe('create_rules_stream_from_ndjson', () => { ndJsonStream, ...rulesObjectsStream, ]); - const resultOrError = result as TypeError[]; + const resultOrError = result as BadRequestError[]; expect(resultOrError[0]).toEqual({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -383,7 +384,7 @@ describe('create_rules_stream_from_ndjson', () => { }); }); - test('non validated data is an instanceof TypeError', async () => { + test('non validated data is an instanceof BadRequestError', async () => { const sample1 = getOutputSample(); const sample2 = getOutputSample(); sample2.rule_id = 'rule-2'; @@ -400,8 +401,8 @@ describe('create_rules_stream_from_ndjson', () => { ndJsonStream, ...rulesObjectsStream, ]); - const resultOrError = result as TypeError[]; - expect(resultOrError[1] instanceof TypeError).toEqual(true); + const resultOrError = result as BadRequestError[]; + expect(resultOrError[1] instanceof BadRequestError).toEqual(true); }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts index ae0dfa20852aa..3e22999528101 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts @@ -13,6 +13,7 @@ import { createConcatStream, } from '../../../../../../../../src/legacy/utils/streams'; import { importRulesSchema } from '../routes/schemas/import_rules_schema'; +import { BadRequestError } from '../errors/bad_request_error'; export interface RulesObjectsExportResultDetails { /** number of successfully exported objects */ @@ -42,7 +43,7 @@ export const validateRules = (): Transform => { if (!(obj instanceof Error)) { const validated = importRulesSchema.validate(obj); if (validated.error != null) { - return new TypeError(validated.error.message); + return new BadRequestError(validated.error.message); } else { return validated.value; } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_prepackaged_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_prepackaged_rules.ts index bcfe6ee203ecd..e81200fe94376 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_prepackaged_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_prepackaged_rules.ts @@ -6,6 +6,7 @@ import { PrepackagedRules } from '../types'; import { addPrepackagedRulesSchema } from '../routes/schemas/add_prepackaged_rules_schema'; +import { BadRequestError } from '../errors/bad_request_error'; import { rawRules } from './prepackaged_rules'; /** @@ -19,7 +20,7 @@ export const validateAllPrepackagedRules = (rules: PrepackagedRules[]): Prepacka if (validatedRule.error != null) { const ruleName = rule.name ? rule.name : '(rule name unknown)'; const ruleId = rule.rule_id ? rule.rule_id : '(rule rule_id unknown)'; - throw new TypeError( + throw new BadRequestError( `name: "${ruleName}", rule_id: "${ruleId}" within the folder rules/prepackaged_rules ` + `is not a valid detection engine rule. Expect the system ` + `to not work with pre-packaged rules until this rule is fixed ` + diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.ts index d1f41efdddd14..9c3e15de7ce90 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.ts @@ -14,6 +14,7 @@ import { IIndexPattern, } from '../../../../../../../../src/plugins/data/server'; import { PartialFilter, RuleAlertParams } from '../types'; +import { BadRequestError } from '../errors/bad_request_error'; export const getQueryFilter = ( query: string, @@ -74,7 +75,7 @@ export const getFilter = async ({ if (query != null && language != null && index != null) { return getQueryFilter(query, language, filters || [], index); } else { - throw new TypeError('query, filters, and index parameter should be defined'); + throw new BadRequestError('query, filters, and index parameter should be defined'); } } case 'saved_query': { @@ -103,7 +104,7 @@ export const getFilter = async ({ } } } else { - throw new TypeError('savedId parameter should be defined'); + throw new BadRequestError('savedId parameter should be defined'); } } }