From ece0658414caccc9f831c70a1ba4395c86b41f2b Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Thu, 21 Nov 2019 14:25:23 -0700 Subject: [PATCH] [SIEM] [Detection Engine] Adds Rules Table (#50839) This PR wires up the Detection Engine Rules Table and provides the following features: * [x] Lists all rules for a given user/space * [x] Search/Filtering via `Rule Name` * [x] Sorting via `Activate` * [x] Pagination * [x] Enable/Disable Action * [x] Rule Selection / Batch Actions * [x] Rule Import w/ validation via `io-ts` * [x] Batch Actions * [x] Activate selected * [x] Deactivate selected * [x] Export selected (as `.ndjson`) * [ ] ~Edit selected index patterns...~ (Waiting on supported feature) * [x] Delete selected * [x] Individual Overflow Actions * [ ] ~Edit rule settings~ (Waiting on supported feature) * [ ] ~Run rule manually...~ (Waiting on supported feature) * [x] Duplicate rule... * [X] Export rule * [x] Delete rule... ![sort_and_filter](https://user-images.githubusercontent.com/2946766/69286404-641d1a80-0bb0-11ea-9930-8eada88b36f6.gif) ![import_and_export](https://user-images.githubusercontent.com/2946766/69286806-79df0f80-0bb1-11ea-99c5-92df0a706f0e.gif) ![import_failed_validation](https://user-images.githubusercontent.com/2946766/69286797-72b80180-0bb1-11ea-9397-71fa0ff0b203.gif) ![batch_activate_deactivate](https://user-images.githubusercontent.com/2946766/69287019-0093ec80-0bb2-11ea-8320-57cc7fec27a8.gif) ![batch_delete](https://user-images.githubusercontent.com/2946766/69287139-6e401880-0bb2-11ea-948c-c5b92ba90e6f.gif) ![dupe_and_delete](https://user-images.githubusercontent.com/2946766/69287143-74ce9000-0bb2-11ea-88b3-db75f66ba666.gif) Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. - [x] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) - [ ] ~[Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~ -- * Will work with @benskelker on overall Detection Engine documentation - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios * Includes basic tests -- will expand coverage as features solidify - [ ] ~This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~ - [ ] ~This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~ - [ ] ~This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~ --- .../__snapshots__/utility_bar.test.tsx.snap | 6 +- .../utility_bar/utility_bar.test.tsx | 6 +- .../utility_bar/utility_bar_action.test.tsx | 2 +- .../utility_bar/utility_bar_action.tsx | 8 +- .../containers/detection_engine/rules/api.ts | 172 +++ .../detection_engine/rules/translations.ts | 11 + .../detection_engine/rules/types.ts | 81 ++ .../detection_engine/rules/use_rules.tsx | 83 ++ .../detection_engine/detection_engine.tsx | 6 +- .../detection_engine/rule_details/index.tsx | 6 +- .../rules/activity_monitor/columns.tsx | 87 ++ .../rules/activity_monitor/index.tsx | 337 ++++++ .../rules/all_rules/actions.tsx | 59 + .../rules/all_rules/batch_actions.tsx | 89 ++ .../rules/all_rules/columns.tsx | 167 +++ .../rules/all_rules/helpers.ts | 30 + .../rules/all_rules/index.tsx | 226 ++++ .../rules/all_rules/reducer.ts | 144 +++ .../__snapshots__/index.test.tsx.snap | 66 + .../import_rule_modal/index.test.tsx | 30 + .../components/import_rule_modal/index.tsx | 160 +++ .../import_rule_modal/translations.ts | 59 + .../__snapshots__/index.test.tsx.snap | 3 + .../components/json_downloader/index.test.tsx | 61 + .../components/json_downloader/index.tsx | 69 ++ .../__snapshots__/index.test.tsx.snap | 21 + .../components/rule_switch/index.test.tsx | 19 + .../rules/components/rule_switch/index.tsx | 55 + .../pages/detection_engine/rules/index.tsx | 1069 +---------------- .../detection_engine/rules/translations.ts | 196 +++ .../pages/detection_engine/rules/types.ts | 41 + 31 files changed, 2325 insertions(+), 1044 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/activity_monitor/columns.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/activity_monitor/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/actions.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/batch_actions.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/columns.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/helpers.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/reducer.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/__snapshots__/index.test.tsx.snap create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/__snapshots__/index.test.tsx.snap create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar.test.tsx.snap index 1f892acef7ef3..03a04983f9f86 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar.test.tsx.snap @@ -12,11 +12,7 @@ exports[`UtilityBar it renders 1`] = ` - Test popover -

- } + popoverContent={[Function]} > Test action
diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx index 0ae247f5c9dd0..27688ec24530e 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx @@ -32,7 +32,7 @@ describe('UtilityBar', () => {
- {'Test popover'}

}> +

{'Test popover'}

}> {'Test action'}
@@ -60,7 +60,7 @@ describe('UtilityBar', () => { - {'Test popover'}

}> +

{'Test popover'}

}> {'Test action'}
@@ -90,7 +90,7 @@ describe('UtilityBar', () => { - {'Test popover'}

}> +

{'Test popover'}

}> {'Test action'}
diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx index 74eed8cfabf2d..f71bdfda705d0 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx @@ -28,7 +28,7 @@ describe('UtilityBarAction', () => { test('it renders a popover', () => { const wrapper = mount( - {'Test popover'}

}> +

{'Test popover'}

}> {'Test action'}
diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx index ae4362bdbcd7b..2ad48bc9b9c92 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx @@ -5,7 +5,7 @@ */ import { EuiPopover } from '@elastic/eui'; -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { LinkIcon, LinkIconProps } from '../../link_icon'; import { BarAction } from './styles'; @@ -14,6 +14,8 @@ const Popover = React.memo( ({ children, color, iconSide, iconSize, iconType, popoverContent }) => { const [popoverState, setPopoverState] = useState(false); + const closePopover = useCallback(() => setPopoverState(false), [setPopoverState]); + return ( ( closePopover={() => setPopoverState(false)} isOpen={popoverState} > - {popoverContent} + {popoverContent?.(closePopover)} ); } @@ -38,7 +40,7 @@ const Popover = React.memo( Popover.displayName = 'Popover'; export interface UtilityBarActionProps extends LinkIconProps { - popoverContent?: React.ReactNode; + popoverContent?: (closePopover: () => void) => React.ReactNode; } export const UtilityBarAction = React.memo( 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 new file mode 100644 index 0000000000000..333baefe034fd --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts @@ -0,0 +1,172 @@ +/* + * 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 chrome from 'ui/chrome'; +import { + DeleteRulesProps, + DuplicateRulesProps, + EnableRulesProps, + FetchRulesProps, + FetchRulesResponse, + Rule, +} from './types'; +import { throwIfNotOk } from '../../../hooks/api/api'; + +/** + * Fetches all rules or single specified rule from the Detection Engine API + * + * @param filterOptions desired filters (e.g. filter/sortField/sortOrder) + * @param pagination desired pagination options (e.g. page/perPage) + * @param id if specified, will return specific rule if exists + * @param kbnVersion current Kibana Version to use for headers + */ +export const fetchRules = async ({ + filterOptions = { + filter: '', + sortField: 'enabled', + sortOrder: 'desc', + }, + pagination = { + page: 1, + perPage: 20, + total: 0, + }, + id, + kbnVersion, + signal, +}: FetchRulesProps): Promise => { + const queryParams = [ + `page=${pagination.page}`, + `per_page=${pagination.perPage}`, + `sort_field=${filterOptions.sortField}`, + `sort_order=${filterOptions.sortOrder}`, + ...(filterOptions.filter.length !== 0 + ? [`filter=alert.attributes.name:%20${encodeURIComponent(filterOptions.filter)}`] + : []), + ]; + + const endpoint = + id != null + ? `${chrome.getBasePath()}/api/detection_engine/rules?id="${id}"` + : `${chrome.getBasePath()}/api/detection_engine/rules/_find?${queryParams.join('&')}`; + + const response = await fetch(endpoint, { + method: 'GET', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-version': kbnVersion, + 'kbn-xsrf': kbnVersion, + }, + signal, + }); + await throwIfNotOk(response); + return id != null + ? { + page: 0, + perPage: 1, + total: 1, + data: response.json(), + } + : response.json(); +}; + +/** + * Enables/Disables provided Rule ID's + * + * @param ids array of Rule ID's (not rule_id) to enable/disable + * @param enabled to enable or disable + * @param kbnVersion current Kibana Version to use for headers + */ +export const enableRules = async ({ + ids, + enabled, + kbnVersion, +}: EnableRulesProps): Promise => { + const requests = ids.map(id => + fetch(`${chrome.getBasePath()}/api/detection_engine/rules`, { + method: 'PUT', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-version': kbnVersion, + 'kbn-xsrf': kbnVersion, + }, + body: JSON.stringify({ id, enabled }), + }) + ); + + const responses = await Promise.all(requests); + await responses.map(response => throwIfNotOk(response)); + return Promise.all( + responses.map>(response => response.json()) + ); +}; + +/** + * Deletes provided Rule ID's + * + * @param ids array of Rule ID's (not rule_id) to delete + * @param kbnVersion current Kibana Version to use for headers + */ +export const deleteRules = async ({ ids, kbnVersion }: DeleteRulesProps): Promise => { + // TODO: Don't delete if immutable! + const requests = ids.map(id => + fetch(`${chrome.getBasePath()}/api/detection_engine/rules?id=${id}`, { + method: 'DELETE', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-version': kbnVersion, + 'kbn-xsrf': kbnVersion, + }, + }) + ); + + const responses = await Promise.all(requests); + await responses.map(response => throwIfNotOk(response)); + return Promise.all( + responses.map>(response => response.json()) + ); +}; + +/** + * Duplicates provided Rules + * + * @param rule to duplicate + * @param kbnVersion current Kibana Version to use for headers + */ +export const duplicateRules = async ({ + rules, + kbnVersion, +}: DuplicateRulesProps): Promise => { + const requests = rules.map(rule => + fetch(`${chrome.getBasePath()}/api/detection_engine/rules`, { + method: 'POST', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-version': kbnVersion, + 'kbn-xsrf': kbnVersion, + }, + body: JSON.stringify({ + ...rule, + name: `${rule.name} [Duplicate]`, + created_by: undefined, + id: undefined, + rule_id: undefined, + updated_by: undefined, + enabled: rule.enabled, + }), + }) + ); + + const responses = await Promise.all(requests); + await responses.map(response => throwIfNotOk(response)); + return Promise.all( + responses.map>(response => response.json()) + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/translations.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/translations.ts new file mode 100644 index 0000000000000..a1ea2afb822f9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/translations.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const RULE_FETCH_FAILURE = i18n.translate('xpack.siem.containers.detectionEngine.rules', { + defaultMessage: 'Failed to fetch Rules', +}); 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 new file mode 100644 index 0000000000000..afb0158fea677 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts @@ -0,0 +1,81 @@ +/* + * 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 * as t from 'io-ts'; + +export const RuleSchema = t.intersection([ + t.type({ + created_by: t.string, + description: t.string, + enabled: t.boolean, + id: t.string, + index: t.array(t.string), + interval: t.string, + language: t.string, + name: t.string, + query: t.string, + rule_id: t.string, + severity: t.string, + type: t.string, + updated_by: t.string, + }), + t.partial({ + false_positives: t.array(t.string), + from: t.string, + max_signals: t.number, + references: t.array(t.string), + tags: t.array(t.string), + to: t.string, + }), +]); + +export const RulesSchema = t.array(RuleSchema); + +export type Rule = t.TypeOf; +export type Rules = t.TypeOf; + +export interface PaginationOptions { + page: number; + perPage: number; + total: number; +} + +export interface FetchRulesProps { + pagination?: PaginationOptions; + filterOptions?: FilterOptions; + id?: string; + kbnVersion: string; + signal: AbortSignal; +} + +export interface FilterOptions { + filter: string; + sortField: string; + sortOrder: 'asc' | 'desc'; +} + +export interface FetchRulesResponse { + page: number; + perPage: number; + total: number; + data: Rule[]; +} + +export interface EnableRulesProps { + ids: string[]; + enabled: boolean; + kbnVersion: string; +} + +export interface DeleteRulesProps { + ids: string[]; + kbnVersion: string; +} + +export interface DuplicateRulesProps { + rules: Rules; + kbnVersion: string; +} 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 new file mode 100644 index 0000000000000..2b8bb986a296a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx @@ -0,0 +1,83 @@ +/* + * 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 { useEffect, useState } from 'react'; + +import { useKibanaUiSetting } from '../../../lib/settings/use_kibana_ui_setting'; +import { DEFAULT_KBN_VERSION } from '../../../../common/constants'; +import { FetchRulesResponse, FilterOptions, PaginationOptions } from './types'; +import { useStateToaster } from '../../../components/toasters'; +import { fetchRules } from './api'; +import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; +import * as i18n from './translations'; + +type Return = [boolean, FetchRulesResponse]; + +/** + * Hook for using the list of Rules from the Detection Engine API + * + * @param pagination desired pagination options (e.g. page/perPage) + * @param filterOptions desired filters (e.g. filter/sortField/sortOrder) + * @param refetchToggle toggle for refetching data + */ +export const useRules = ( + pagination: PaginationOptions, + filterOptions: FilterOptions, + refetchToggle: boolean +): Return => { + const [rules, setRules] = useState({ + page: 1, + perPage: 20, + total: 0, + data: [], + }); + const [loading, setLoading] = useState(true); + const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION); + const [, dispatchToaster] = useStateToaster(); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + setLoading(true); + + async function fetchData() { + try { + const fetchRulesResult = await fetchRules({ + filterOptions, + pagination, + kbnVersion, + signal: abortCtrl.signal, + }); + + if (isSubscribed) { + setRules(fetchRulesResult); + } + } catch (error) { + if (isSubscribed) { + errorToToaster({ title: i18n.RULE_FETCH_FAILURE, error, dispatchToaster }); + } + } + if (isSubscribed) { + setLoading(false); + } + } + + fetchData(); + return () => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, [ + refetchToggle, + pagination.page, + pagination.perPage, + filterOptions.filter, + filterOptions.sortField, + filterOptions.sortOrder, + ]); + + return [loading, rules]; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx index 9b63a6e160e42..f02e80ebfaf66 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx @@ -48,7 +48,7 @@ const OpenSignals = React.memo(() => { {'Batch actions context menu here.'}

} + popoverContent={() =>

{'Batch actions context menu here.'}

} > {'Batch actions'}
@@ -70,7 +70,7 @@ const OpenSignals = React.memo(() => { {'Customize columns context menu here.'}

} + popoverContent={() =>

{'Customize columns context menu here.'}

} > {'Customize columns'}
@@ -100,7 +100,7 @@ const ClosedSignals = React.memo(() => { {'Customize columns context menu here.'}

} + popoverContent={() =>

{'Customize columns context menu here.'}

} > {'Customize columns'}
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rule_details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rule_details/index.tsx index da3e5fb2083dd..b16036e3142fc 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rule_details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rule_details/index.tsx @@ -66,7 +66,7 @@ const OpenSignals = React.memo(() => { {'Batch actions context menu here.'}

} + popoverContent={() =>

{'Batch actions context menu here.'}

} > {'Batch actions'}
@@ -88,7 +88,7 @@ const OpenSignals = React.memo(() => { {'Customize columns context menu here.'}

} + popoverContent={() =>

{'Customize columns context menu here.'}

} > {'Customize columns'}
@@ -118,7 +118,7 @@ const ClosedSignals = React.memo(() => { {'Customize columns context menu here.'}

} + popoverContent={() =>

{'Customize columns context menu here.'}

} > {'Customize columns'}
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/activity_monitor/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/activity_monitor/columns.tsx new file mode 100644 index 0000000000000..58e2b9f0cabc7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/activity_monitor/columns.tsx @@ -0,0 +1,87 @@ +/* + * 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 { EuiIconTip, EuiLink, EuiTextColor } from '@elastic/eui'; +import React from 'react'; +import { getEmptyTagValue } from '../../../../components/empty_value'; +import { ColumnTypes } from './index'; + +const actions = [ + { + available: (item: ColumnTypes) => item.status === 'Running', + description: 'Stop', + icon: 'stop', + isPrimary: true, + name: 'Stop', + onClick: () => {}, + type: 'icon', + }, + { + available: (item: ColumnTypes) => item.status === 'Stopped', + description: 'Resume', + icon: 'play', + isPrimary: true, + name: 'Resume', + onClick: () => {}, + type: 'icon', + }, +]; + +// Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes? +export const columns = [ + { + field: 'rule', + name: 'Rule', + render: (value: ColumnTypes['rule']) => {value.name}, + sortable: true, + truncateText: true, + }, + { + field: 'ran', + name: 'Ran', + render: (value: ColumnTypes['ran']) => '--', + sortable: true, + truncateText: true, + }, + { + field: 'lookedBackTo', + name: 'Looked back to', + render: (value: ColumnTypes['lookedBackTo']) => '--', + sortable: true, + truncateText: true, + }, + { + field: 'status', + name: 'Status', + sortable: true, + truncateText: true, + }, + { + field: 'response', + name: 'Response', + render: (value: ColumnTypes['response']) => { + return value === undefined ? ( + getEmptyTagValue() + ) : ( + <> + {value === 'Fail' ? ( + + {value} + + ) : ( + {value} + )} + + ); + }, + sortable: true, + truncateText: true, + }, + { + actions, + width: '40px', + }, +]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/activity_monitor/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/activity_monitor/index.tsx new file mode 100644 index 0000000000000..d7306b8630bc2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/activity_monitor/index.tsx @@ -0,0 +1,337 @@ +/* + * 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 { EuiBasicTable, EuiPanel, EuiSpacer } from '@elastic/eui'; +import React, { useState } from 'react'; +import { HeaderSection } from '../../../../components/header_section'; +import { + UtilityBar, + UtilityBarAction, + UtilityBarGroup, + UtilityBarSection, + UtilityBarText, +} from '../../../../components/detection_engine/utility_bar'; +import { columns } from './columns'; + +export interface RuleTypes { + href: string; + name: string; +} + +export interface ColumnTypes { + id: number; + rule: RuleTypes; + ran: string; + lookedBackTo: string; + status: string; + response: string | undefined; +} + +export interface PageTypes { + index: number; + size: number; +} + +export interface SortTypes { + field: string; + direction: string; +} + +export const ActivityMonitor = React.memo(() => { + const sampleTableData = [ + { + id: 1, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Running', + }, + { + id: 2, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Stopped', + }, + { + id: 3, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Fail', + }, + { + id: 4, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 5, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 6, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 7, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 8, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 9, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 10, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 11, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 12, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 13, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 14, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 15, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 16, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 17, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 18, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 19, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 20, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 21, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + ]; + + const [itemsTotalState] = useState(sampleTableData.length); + const [pageState, setPageState] = useState({ index: 0, size: 20 }); + // const [selectedState, setSelectedState] = useState([]); + const [sortState, setSortState] = useState({ field: 'ran', direction: 'desc' }); + + return ( + <> + + + + + + + + + {'Showing: 39 activites'} + + + + {'Selected: 2 activities'} + + {'Stop selected'} + + + + {'Clear 7 filters'} + + + + + { + setPageState(page); + setSortState(sort); + }} + pagination={{ + pageIndex: pageState.index, + pageSize: pageState.size, + totalItemCount: itemsTotalState, + pageSizeOptions: [5, 10, 20], + }} + selection={{ + selectable: (item: ColumnTypes) => item.status !== 'Completed', + selectableMessage: (selectable: boolean) => + selectable ? undefined : 'Completed runs cannot be acted upon', + onSelectionChange: (selectedItems: ColumnTypes[]) => { + // setSelectedState(selectedItems); + }, + }} + sorting={{ + sort: sortState, + }} + /> + + + ); +}); +ActivityMonitor.displayName = 'ActivityMonitor'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/actions.tsx new file mode 100644 index 0000000000000..a54296f65a382 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/actions.tsx @@ -0,0 +1,59 @@ +/* + * 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 React from 'react'; +import { + deleteRules, + duplicateRules, + enableRules, +} from '../../../../containers/detection_engine/rules/api'; +import { Action } from './reducer'; +import { Rule } from '../../../../containers/detection_engine/rules/types'; + +export const editRuleAction = () => {}; + +export const runRuleAction = () => {}; + +export const duplicateRuleAction = async ( + rule: Rule, + dispatch: React.Dispatch, + kbnVersion: string +) => { + dispatch({ type: 'updateLoading', ids: [rule.id], isLoading: true }); + const duplicatedRule = await duplicateRules({ rules: [rule], kbnVersion }); + dispatch({ type: 'updateLoading', ids: [rule.id], isLoading: false }); + dispatch({ type: 'updateRules', rules: duplicatedRule }); +}; + +export const exportRulesAction = async (rules: Rule[], dispatch: React.Dispatch) => { + dispatch({ type: 'setExportPayload', exportPayload: rules }); +}; + +export const deleteRulesAction = async ( + ids: string[], + dispatch: React.Dispatch, + kbnVersion: string +) => { + dispatch({ type: 'updateLoading', ids, isLoading: true }); + const deletedRules = await deleteRules({ ids, kbnVersion }); + dispatch({ type: 'deleteRules', rules: deletedRules }); +}; + +export const enableRulesAction = async ( + ids: string[], + enabled: boolean, + dispatch: React.Dispatch, + kbnVersion: string +) => { + try { + dispatch({ type: 'updateLoading', ids, isLoading: true }); + const updatedRules = await enableRules({ ids, enabled, kbnVersion }); + dispatch({ type: 'updateRules', rules: updatedRules }); + } catch { + // TODO Add error toast support to actions (and @throw jsdoc to api calls) + dispatch({ type: 'updateLoading', ids, isLoading: false }); + } +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/batch_actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/batch_actions.tsx new file mode 100644 index 0000000000000..c8fb9d98fde6a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/batch_actions.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiContextMenuItem } from '@elastic/eui'; +import React from 'react'; +import * as i18n from '../translations'; +import { TableData } from '../types'; +import { Action } from './reducer'; +import { deleteRulesAction, enableRulesAction, exportRulesAction } from './actions'; + +export const getBatchItems = ( + selectedState: TableData[], + dispatch: React.Dispatch, + closePopover: () => void, + kbnVersion: string +) => { + const containsEnabled = selectedState.some(v => v.activate); + const containsDisabled = selectedState.some(v => !v.activate); + const containsLoading = selectedState.some(v => v.isLoading); + + return [ + { + closePopover(); + const deactivatedIds = selectedState.filter(s => !s.activate).map(s => s.id); + await enableRulesAction(deactivatedIds, true, dispatch, kbnVersion); + }} + > + {i18n.BATCH_ACTION_ACTIVATE_SELECTED} + , + { + closePopover(); + const activatedIds = selectedState.filter(s => s.activate).map(s => s.id); + await enableRulesAction(activatedIds, false, dispatch, kbnVersion); + }} + > + {i18n.BATCH_ACTION_DEACTIVATE_SELECTED} + , + { + closePopover(); + await exportRulesAction( + selectedState.map(s => s.sourceRule), + dispatch + ); + }} + > + {i18n.BATCH_ACTION_EXPORT_SELECTED} + , + { + closePopover(); + }} + > + {i18n.BATCH_ACTION_EDIT_INDEX_PATTERNS} + , + { + closePopover(); + await deleteRulesAction( + selectedState.map(({ sourceRule: { id } }) => id), + dispatch, + kbnVersion + ); + }} + > + {i18n.BATCH_ACTION_DELETE_SELECTED} + , + ]; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/columns.tsx new file mode 100644 index 0000000000000..cae0fb3eaf906 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/columns.tsx @@ -0,0 +1,167 @@ +/* + * 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 { EuiBadge, EuiHealth, EuiIconTip, EuiLink, EuiTextColor } from '@elastic/eui'; +import React from 'react'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { getEmptyTagValue } from '../../../../components/empty_value'; +import { + deleteRulesAction, + duplicateRuleAction, + editRuleAction, + enableRulesAction, + exportRulesAction, + runRuleAction, +} from './actions'; + +import { Action } from './reducer'; +import { TableData } from '../types'; +import * as i18n from '../translations'; +import { PreferenceFormattedDate } from '../../../../components/formatted_date'; +import { RuleSwitch } from '../components/rule_switch'; + +const getActions = (dispatch: React.Dispatch, kbnVersion: string) => [ + { + description: i18n.EDIT_RULE_SETTINGS, + icon: 'visControls', + name: i18n.EDIT_RULE_SETTINGS, + onClick: editRuleAction, + enabled: () => false, + }, + { + description: i18n.RUN_RULE_MANUALLY, + icon: 'play', + name: i18n.RUN_RULE_MANUALLY, + onClick: runRuleAction, + enabled: () => false, + }, + { + description: i18n.DUPLICATE_RULE, + icon: 'copy', + name: i18n.DUPLICATE_RULE, + onClick: (rowItem: TableData) => duplicateRuleAction(rowItem.sourceRule, dispatch, kbnVersion), + }, + { + description: i18n.EXPORT_RULE, + icon: 'exportAction', + name: i18n.EXPORT_RULE, + onClick: (rowItem: TableData) => exportRulesAction([rowItem.sourceRule], dispatch), + }, + { + description: i18n.DELETE_RULE, + icon: 'trash', + name: i18n.DELETE_RULE, + onClick: (rowItem: TableData) => deleteRulesAction([rowItem.id], dispatch, kbnVersion), + }, +]; + +// Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes? +export const getColumns = (dispatch: React.Dispatch, kbnVersion: string) => [ + { + field: 'rule', + name: i18n.COLUMN_RULE, + render: (value: TableData['rule']) => {value.name}, + truncateText: true, + width: '24%', + }, + { + field: 'method', + name: i18n.COLUMN_METHOD, + truncateText: true, + }, + { + field: 'severity', + name: i18n.COLUMN_SEVERITY, + render: (value: TableData['severity']) => ( + + {value} + + ), + truncateText: true, + }, + { + field: 'lastCompletedRun', + name: i18n.COLUMN_LAST_COMPLETE_RUN, + render: (value: TableData['lastCompletedRun']) => { + return value == null ? ( + getEmptyTagValue() + ) : ( + + ); + }, + sortable: true, + truncateText: true, + width: '16%', + }, + { + field: 'lastResponse', + name: i18n.COLUMN_LAST_RESPONSE, + render: (value: TableData['lastResponse']) => { + return value == null ? ( + getEmptyTagValue() + ) : ( + <> + {value.type === 'Fail' ? ( + + {value.type} + + ) : ( + {value.type} + )} + + ); + }, + truncateText: true, + }, + { + field: 'tags', + name: i18n.COLUMN_TAGS, + render: (value: TableData['tags']) => ( +
+ <> + {value.map((tag, i) => ( + + {tag} + + ))} + +
+ ), + truncateText: true, + width: '20%', + }, + { + align: 'center', + field: 'activate', + name: i18n.COLUMN_ACTIVATE, + render: (value: TableData['activate'], item: TableData) => ( + { + await enableRulesAction([id], enabled, dispatch, kbnVersion); + }} + /> + ), + sortable: true, + width: '85px', + }, + { + actions: getActions(dispatch, kbnVersion), + width: '40px', + }, +]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/helpers.ts new file mode 100644 index 0000000000000..db02d41771f68 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/helpers.ts @@ -0,0 +1,30 @@ +/* + * 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 { Rule } from '../../../../containers/detection_engine/rules/types'; +import { TableData } from '../types'; +import { getEmptyValue } from '../../../../components/empty_value'; + +export const formatRules = (rules: Rule[], selectedIds?: string[]): TableData[] => + rules.map(rule => ({ + id: rule.id, + rule_id: rule.rule_id, + rule: { + href: `#/detection-engine/rules/rule-details/${encodeURIComponent(rule.id)}`, + name: rule.name, + status: 'Status Placeholder', + }, + method: rule.type, // TODO: Map to i18n? + severity: rule.severity, + lastCompletedRun: undefined, // TODO: Not available yet + lastResponse: { + type: getEmptyValue(), // TODO: Not available yet + }, + tags: rule.tags ?? [], + activate: rule.enabled, + sourceRule: rule, + isLoading: selectedIds?.includes(rule.id) ?? false, + })); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/index.tsx new file mode 100644 index 0000000000000..a73ebeb61db3c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/index.tsx @@ -0,0 +1,226 @@ +/* + * 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 { + EuiBasicTable, + EuiContextMenuPanel, + EuiFieldSearch, + EuiLoadingContent, + EuiSpacer, +} from '@elastic/eui'; +import React, { useCallback, useEffect, useReducer, useState } from 'react'; + +import uuid from 'uuid'; +import { HeaderSection } from '../../../../components/header_section'; +import { + UtilityBar, + UtilityBarAction, + UtilityBarGroup, + UtilityBarSection, + UtilityBarText, +} from '../../../../components/detection_engine/utility_bar'; +import { getColumns } from './columns'; +import { useRules } from '../../../../containers/detection_engine/rules/use_rules'; +import { Loader } from '../../../../components/loader'; +import { Panel } from '../../../../components/panel'; +import { getBatchItems } from './batch_actions'; +import { EuiBasicTableOnChange, TableData } from '../types'; +import { allRulesReducer, State } from './reducer'; +import * as i18n from '../translations'; +import { useKibanaUiSetting } from '../../../../lib/settings/use_kibana_ui_setting'; +import { DEFAULT_KBN_VERSION } from '../../../../../common/constants'; +import { JSONDownloader } from '../components/json_downloader'; +import { useStateToaster } from '../../../../components/toasters'; + +const initialState: State = { + isLoading: true, + rules: [], + tableData: [], + selectedItems: [], + refreshToggle: true, + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + filterOptions: { + filter: '', + sortField: 'enabled', + sortOrder: 'desc', + }, +}; + +/** + * Table Component for displaying all Rules for a given cluster. Provides the ability to filter + * by name, sort by enabled, and perform the following actions: + * * Enable/Disable + * * Duplicate + * * Delete + * * Import/Export + */ +export const AllRules = React.memo<{ importCompleteToggle: boolean }>(importCompleteToggle => { + const [ + { + exportPayload, + filterOptions, + isLoading, + refreshToggle, + selectedItems, + tableData, + pagination, + }, + dispatch, + ] = useReducer(allRulesReducer, initialState); + + const [isInitialLoad, setIsInitialLoad] = useState(true); + const [isLoadingRules, rulesData] = useRules(pagination, filterOptions, refreshToggle); + const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION); + const [, dispatchToaster] = useStateToaster(); + + const getBatchItemsPopoverContent = useCallback( + (closePopover: () => void) => ( + + ), + [selectedItems, dispatch, kbnVersion] + ); + + useEffect(() => { + dispatch({ type: 'loading', isLoading: isLoadingRules }); + + if (!isLoadingRules) { + setIsInitialLoad(false); + } + }, [isLoadingRules]); + + useEffect(() => { + if (!isInitialLoad) { + dispatch({ type: 'refresh' }); + } + }, [importCompleteToggle]); + + useEffect(() => { + dispatch({ + type: 'updateRules', + rules: rulesData.data, + pagination: { + page: rulesData.page, + perPage: rulesData.perPage, + total: rulesData.total, + }, + }); + }, [rulesData]); + + return ( + <> + { + dispatchToaster({ + type: 'addToaster', + toast: { + id: uuid.v4(), + title: i18n.SUCCESSFULLY_EXPORTED_RULES(exportCount), + color: 'success', + iconType: 'check', + }, + }); + }} + /> + + + + {isInitialLoad ? ( + + ) : ( + <> + + { + dispatch({ + type: 'updateFilterOptions', + filterOptions: { + ...filterOptions, + filter: filterString, + }, + }); + }} + /> + + + + + + {i18n.SHOWING_RULES(pagination.total ?? 0)} + + + + {i18n.SELECTED_RULES(selectedItems.length)} + + {i18n.BATCH_ACTIONS} + + dispatch({ type: 'refresh' })} + > + {i18n.REFRESH} + + + + + + { + dispatch({ + type: 'updatePagination', + pagination: { ...pagination, page: page.index + 1, perPage: page.size }, + }); + dispatch({ + type: 'updateFilterOptions', + filterOptions: { + ...filterOptions, + sortField: 'enabled', // Only enabled is supported for sorting currently + sortOrder: sort.direction, + }, + }); + }} + pagination={{ + pageIndex: pagination.page - 1, + pageSize: pagination.perPage, + totalItemCount: pagination.total, + pageSizeOptions: [5, 10, 20], + }} + selection={{ + selectable: (item: TableData) => !item.isLoading, + onSelectionChange: (selected: TableData[]) => + dispatch({ type: 'setSelected', selectedItems: selected }), + }} + sorting={{ sort: { field: 'activate', direction: filterOptions.sortOrder } }} + /> + {isLoading && } + + )} + + + ); +}); + +AllRules.displayName = 'AllRules'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/reducer.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/reducer.ts new file mode 100644 index 0000000000000..c59c5687c10c9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/reducer.ts @@ -0,0 +1,144 @@ +/* + * 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 { + FilterOptions, + PaginationOptions, + Rule, +} from '../../../../containers/detection_engine/rules/types'; +import { TableData } from '../types'; +import { formatRules } from './helpers'; + +export interface State { + isLoading: boolean; + rules: Rule[]; + selectedItems: TableData[]; + pagination: PaginationOptions; + filterOptions: FilterOptions; + refreshToggle: boolean; + tableData: TableData[]; + exportPayload?: object[]; +} + +export type Action = + | { type: 'refresh' } + | { type: 'loading'; isLoading: boolean } + | { type: 'deleteRules'; rules: Rule[] } + | { type: 'duplicate'; rule: Rule } + | { type: 'setExportPayload'; exportPayload?: object[] } + | { type: 'setSelected'; selectedItems: TableData[] } + | { type: 'updateLoading'; ids: string[]; isLoading: boolean } + | { type: 'updateRules'; rules: Rule[]; pagination?: PaginationOptions } + | { type: 'updatePagination'; pagination: PaginationOptions } + | { type: 'updateFilterOptions'; filterOptions: FilterOptions } + | { type: 'failure' }; + +export const allRulesReducer = (state: State, action: Action): State => { + switch (action.type) { + case 'refresh': { + return { + ...state, + refreshToggle: !state.refreshToggle, + }; + } + case 'updateRules': { + // If pagination included, this was a hard refresh + if (action.pagination) { + return { + ...state, + rules: action.rules, + pagination: action.pagination, + tableData: formatRules(action.rules), + }; + } + + const ruleIds = state.rules.map(r => r.rule_id); + const updatedRules = action.rules.reduce( + (rules, updatedRule) => + ruleIds.includes(updatedRule.rule_id) + ? rules.map(r => (updatedRule.rule_id === r.rule_id ? updatedRule : r)) + : [...rules, updatedRule], + [...state.rules] + ); + + // Update enabled on selectedItems so that batch actions show correct available actions + const updatedRuleIdToState = action.rules.reduce>( + (acc, r) => ({ ...acc, [r.id]: r.enabled }), + {} + ); + const updatedSelectedItems = state.selectedItems.map(selectedItem => + Object.keys(updatedRuleIdToState).includes(selectedItem.id) + ? { ...selectedItem, activate: updatedRuleIdToState[selectedItem.id] } + : selectedItem + ); + + return { + ...state, + rules: updatedRules, + tableData: formatRules(updatedRules), + selectedItems: updatedSelectedItems, + }; + } + case 'updatePagination': { + return { + ...state, + pagination: action.pagination, + }; + } + case 'updateFilterOptions': { + return { + ...state, + filterOptions: action.filterOptions, + }; + } + case 'deleteRules': { + const deletedRuleIds = action.rules.map(r => r.rule_id); + const updatedRules = state.rules.reduce( + (rules, rule) => (deletedRuleIds.includes(rule.rule_id) ? rules : [...rules, rule]), + [] + ); + return { + ...state, + rules: updatedRules, + tableData: formatRules(updatedRules), + }; + } + case 'setSelected': { + return { + ...state, + selectedItems: action.selectedItems, + }; + } + case 'updateLoading': { + return { + ...state, + rules: state.rules, + tableData: formatRules(state.rules, action.ids), + }; + } + case 'loading': { + return { + ...state, + isLoading: action.isLoading, + }; + } + case 'failure': { + return { + ...state, + isLoading: false, + rules: [], + }; + } + case 'setExportPayload': { + return { + ...state, + exportPayload: action.exportPayload, + }; + } + default: + return state; + } +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..6b0aa02d4edfa --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/__snapshots__/index.test.tsx.snap @@ -0,0 +1,66 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ImportRuleModal renders correctly against snapshot 1`] = ` + + + + + + Import rule + + + + +

+ Select a SIEM rule (as exported from the Detection Engine UI) to import +

+
+ + + + +
+ + + Cancel + + + Import rule + + +
+
+
+`; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.test.tsx new file mode 100644 index 0000000000000..b397e50201f14 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.test.tsx @@ -0,0 +1,30 @@ +/* + * 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 { shallow } from 'enzyme'; +import toJson from 'enzyme-to-json'; +import * as React from 'react'; +import { ImportRuleModal } from './index'; +import { useKibanaUiSetting } from '../../../../../lib/settings/use_kibana_ui_setting'; +import { getMockKibanaUiSetting, MockFrameworks } from '../../../../../mock'; +import { DEFAULT_KBN_VERSION } from '../../../../../../common/constants'; + +const mockUseKibanaUiSetting: jest.Mock = useKibanaUiSetting as jest.Mock; +jest.mock('../../../../../lib/settings/use_kibana_ui_setting', () => ({ + useKibanaUiSetting: jest.fn(), +})); + +describe('ImportRuleModal', () => { + test('renders correctly against snapshot', () => { + mockUseKibanaUiSetting.mockImplementation( + getMockKibanaUiSetting((DEFAULT_KBN_VERSION as unknown) as MockFrameworks) + ); + const wrapper = shallow( + + ); + expect(toJson(wrapper)).toMatchSnapshot(); + }); +}); 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 new file mode 100644 index 0000000000000..fdcf6263f414f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx @@ -0,0 +1,160 @@ +/* + * 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 { + EuiButton, + EuiButtonEmpty, + EuiCheckbox, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiOverlayMask, + // @ts-ignore no-exported-member + EuiFilePicker, + EuiSpacer, + EuiText, +} from '@elastic/eui'; + +import React, { useCallback, useState } from 'react'; +import { failure } from 'io-ts/lib/PathReporter'; +import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import uuid from 'uuid'; +import * as i18n from './translations'; +import { duplicateRules } from '../../../../../containers/detection_engine/rules/api'; +import { useKibanaUiSetting } from '../../../../../lib/settings/use_kibana_ui_setting'; +import { DEFAULT_KBN_VERSION } from '../../../../../../common/constants'; +import { ndjsonToJSON } from '../json_downloader'; +import { RulesSchema } from '../../../../../containers/detection_engine/rules/types'; +import { useStateToaster } from '../../../../../components/toasters'; + +interface ImportRuleModalProps { + showModal: boolean; + closeModal: () => void; + importComplete: () => void; +} + +/** + * Modal component for importing Rules from a json file + * + * @param filename name of file to be downloaded + * @param payload JSON string to write to file + * + */ +export const ImportRuleModal = React.memo( + ({ showModal, closeModal, importComplete }) => { + const [selectedFiles, setSelectedFiles] = useState(null); + const [isImporting, setIsImporting] = useState(false); + const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION); + const [, dispatchToaster] = useStateToaster(); + + const cleanupAndCloseModal = () => { + setIsImporting(false); + setSelectedFiles(null); + closeModal(); + }; + + const importRules = useCallback(async () => { + if (selectedFiles != null) { + setIsImporting(true); + const reader = new FileReader(); + reader.onload = async event => { + // @ts-ignore type is string, not ArrayBuffer as FileReader.readAsText is called + const importedRules = ndjsonToJSON(event?.target?.result ?? ''); + + const decodedRules = pipe( + RulesSchema.decode(importedRules), + fold(errors => { + cleanupAndCloseModal(); + dispatchToaster({ + type: 'addToaster', + toast: { + id: uuid.v4(), + title: i18n.IMPORT_FAILED, + color: 'danger', + iconType: 'alert', + errors: failure(errors), + }, + }); + throw new Error(failure(errors).join('\n')); + }, identity) + ); + + const duplicatedRules = await duplicateRules({ rules: decodedRules, kbnVersion }); + importComplete(); + cleanupAndCloseModal(); + + dispatchToaster({ + type: 'addToaster', + toast: { + id: uuid.v4(), + title: i18n.SUCCESSFULLY_IMPORTED_RULES(duplicatedRules.length), + color: 'success', + iconType: 'check', + }, + }); + }; + Object.values(selectedFiles).map(f => reader.readAsText(f)); + } + }, [selectedFiles]); + + return ( + <> + {showModal && ( + + + + {i18n.IMPORT_RULE} + + + + +

{i18n.SELECT_RULE}

+
+ + + { + setSelectedFiles(Object.keys(files).length > 0 ? files : null); + }} + display={'large'} + fullWidth={true} + isLoading={isImporting} + /> + + {}} + /> +
+ + + {i18n.CANCEL_BUTTON} + + {i18n.IMPORT_RULE} + + +
+
+ )} + + ); + } +); + +ImportRuleModal.displayName = 'ImportRuleModal'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/translations.ts new file mode 100644 index 0000000000000..50c3c75b6109f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/translations.ts @@ -0,0 +1,59 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const IMPORT_RULE = i18n.translate( + 'xpack.siem.detectionEngine.components.importRuleModal.importRuleTitle', + { + defaultMessage: 'Import rule', + } +); + +export const SELECT_RULE = i18n.translate( + 'xpack.siem.detectionEngine.components.importRuleModal.selectRuleDescription', + { + defaultMessage: 'Select a SIEM rule (as exported from the Detection Engine UI) to import', + } +); + +export const INITIAL_PROMPT_TEXT = i18n.translate( + 'xpack.siem.detectionEngine.components.importRuleModal.initialPromptTextDescription', + { + defaultMessage: 'Select or drag and drop files', + } +); + +export const OVERWRITE_WITH_SAME_NAME = i18n.translate( + 'xpack.siem.detectionEngine.components.importRuleModal.overwriteDescription', + { + defaultMessage: 'Automatically overwrite saved objects with the same name', + } +); + +export const CANCEL_BUTTON = i18n.translate( + 'xpack.siem.detectionEngine.components.importRuleModal.cancelTitle', + { + defaultMessage: 'Cancel', + } +); + +export const SUCCESSFULLY_IMPORTED_RULES = (totalRules: number) => + i18n.translate( + 'xpack.siem.detectionEngine.components.importRuleModal.successfullyImportedRulesTitle', + { + values: { totalRules }, + defaultMessage: + 'Successfully imported {totalRules} {totalRules, plural, =1 {rule} other {rules}}', + } + ); + +export const IMPORT_FAILED = i18n.translate( + 'xpack.siem.detectionEngine.components.importRuleModal.importFailedTitle', + { + defaultMessage: 'Failed to import rules', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..c4377c265c2c2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/__snapshots__/index.test.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`JSONDownloader renders correctly against snapshot 1`] = ``; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.test.tsx new file mode 100644 index 0000000000000..ef6493f89f383 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.test.tsx @@ -0,0 +1,61 @@ +/* + * 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 { shallow } from 'enzyme'; +import toJson from 'enzyme-to-json'; +import * as React from 'react'; +import { JSONDownloader, jsonToNDJSON, ndjsonToJSON } from './index'; + +const jsonArray = [ + { + description: 'Detecting root and admin users1', + created_by: 'elastic', + false_positives: [], + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + max_signals: 100, + }, + { + description: 'Detecting root and admin users2', + created_by: 'elastic', + false_positives: [], + index: ['auditbeat-*', 'packetbeat-*', 'winlogbeat-*'], + max_signals: 101, + }, +]; + +const ndjson = `{"description":"Detecting root and admin users1","created_by":"elastic","false_positives":[],"index":["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"max_signals":100} +{"description":"Detecting root and admin users2","created_by":"elastic","false_positives":[],"index":["auditbeat-*","packetbeat-*","winlogbeat-*"],"max_signals":101}`; + +const ndjsonSorted = `{"created_by":"elastic","description":"Detecting root and admin users1","false_positives":[],"index":["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"max_signals":100} +{"created_by":"elastic","description":"Detecting root and admin users2","false_positives":[],"index":["auditbeat-*","packetbeat-*","winlogbeat-*"],"max_signals":101}`; + +describe('JSONDownloader', () => { + test('renders correctly against snapshot', () => { + const wrapper = shallow( + + ); + expect(toJson(wrapper)).toMatchSnapshot(); + }); + + describe('jsonToNDJSON', () => { + test('converts to NDJSON', () => { + const output = jsonToNDJSON(jsonArray, false); + expect(output).toEqual(ndjson); + }); + + test('converts to NDJSON with keys sorted', () => { + const output = jsonToNDJSON(jsonArray); + expect(output).toEqual(ndjsonSorted); + }); + }); + + describe('ndjsonToJSON', () => { + test('converts to JSON', () => { + const output = ndjsonToJSON(ndjson); + expect(output).toEqual(jsonArray); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.tsx new file mode 100644 index 0000000000000..e9c2c69f067cc --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.tsx @@ -0,0 +1,69 @@ +/* + * 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 React, { useEffect, useRef } from 'react'; +import styled from 'styled-components'; + +const InvisibleAnchor = styled.a` + display: none; +`; + +export interface JSONDownloaderProps { + filename: string; + payload?: object[]; + onExportComplete: (exportCount: number) => void; +} + +/** + * Component for downloading JSON as a file. Download will occur on each update to `payload` param + * + * @param filename name of file to be downloaded + * @param payload JSON string to write to file + * + */ +export const JSONDownloader = React.memo( + ({ filename, payload, onExportComplete }) => { + const anchorRef = useRef(null); + + useEffect(() => { + if (anchorRef && anchorRef.current && payload != null) { + const blob = new Blob([jsonToNDJSON(payload)], { type: 'application/json' }); + // @ts-ignore function is not always defined -- this is for supporting IE + if (window.navigator.msSaveOrOpenBlob) { + window.navigator.msSaveBlob(blob); + } else { + const objectURL = window.URL.createObjectURL(blob); + anchorRef.current.href = objectURL; + anchorRef.current.download = filename; + anchorRef.current.click(); + window.URL.revokeObjectURL(objectURL); + } + onExportComplete(payload.length); + } + }, [payload]); + + return ; + } +); + +JSONDownloader.displayName = 'JSONDownloader'; + +export const jsonToNDJSON = (jsonArray: object[], sortKeys = true): string => { + return jsonArray + .map(j => JSON.stringify(j, sortKeys ? Object.keys(j).sort() : null, 0)) + .join('\n'); +}; + +export const ndjsonToJSON = (ndjson: string): object[] => { + const jsonLines = ndjson.split(/\r?\n/); + return jsonLines.reduce((acc, line) => { + try { + return [...acc, JSON.parse(line)]; + } catch (e) { + return acc; + } + }, []); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..98f8ae6a80e07 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RuleSwitch renders correctly against snapshot 1`] = ` + + + + + +`; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.test.tsx new file mode 100644 index 0000000000000..9e5f4317678e8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.test.tsx @@ -0,0 +1,19 @@ +/* + * 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 { shallow } from 'enzyme'; +import toJson from 'enzyme-to-json'; +import * as React from 'react'; +import { RuleSwitch } from './index'; + +describe('RuleSwitch', () => { + test('renders correctly against snapshot', () => { + const wrapper = shallow( + + ); + expect(toJson(wrapper)).toMatchSnapshot(); + }); +}); 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 new file mode 100644 index 0000000000000..da58b2e076e0d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx @@ -0,0 +1,55 @@ +/* + * 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 styled from 'styled-components'; +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSwitch } from '@elastic/eui'; + +const StaticSwitch = styled(EuiSwitch)` + .euiSwitch__thumb, + .euiSwitch__icon { + transition: none; + } +`; + +StaticSwitch.displayName = 'StaticSwitch'; + +export interface RuleSwitchProps { + id: string; + enabled: boolean; + isLoading: boolean; + onRuleStateChange: (isEnabled: boolean, id: string) => void; +} + +/** + * Basic switch component for displaying loader when enabled/disabled + */ +export const RuleSwitch = React.memo( + ({ id, enabled, isLoading, onRuleStateChange }) => { + return ( + + + {isLoading ? ( + + ) : ( + { + onRuleStateChange(e.target.checked!, id); + }} + /> + )} + + + ); + } +); + +RuleSwitch.displayName = 'RuleSwitch'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx index 41a6cf54ff5ab..afff0f07dfac4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx @@ -4,1050 +4,66 @@ * you may not use this file except in compliance with the Elastic License. */ -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; -import { - EuiBadge, - EuiBasicTable, - EuiButton, - EuiFieldSearch, - EuiFlexGroup, - EuiFlexItem, - EuiHealth, - EuiIconTip, - EuiLink, - EuiPanel, - EuiSpacer, - EuiSwitch, - EuiTabbedContent, - EuiTextColor, -} from '@elastic/eui'; -import moment from 'moment'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiTabbedContent } from '@elastic/eui'; import React, { useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; -import { getEmptyTagValue } from '../../../components/empty_value'; import { HeaderPage } from '../../../components/header_page'; -import { HeaderSection } from '../../../components/header_section'; -import { - UtilityBar, - UtilityBarAction, - UtilityBarGroup, - UtilityBarSection, - UtilityBarText, -} from '../../../components/detection_engine/utility_bar'; + import { WrapperPage } from '../../../components/wrapper_page'; import { SpyRoute } from '../../../utils/route/spy_routes'; import * as i18n from './translations'; - -// Michael: Will need to change this to get the current datetime format from Kibana settings. -const dateTimeFormat = (value: string) => { - return moment(value).format('M/D/YYYY, h:mm A'); -}; - -const AllRules = React.memo(() => { - interface RuleTypes { - href: string; - name: string; - status: string; - } - - interface LastResponseTypes { - type: string; - message?: string; - } - - interface ColumnTypes { - id: number; - rule: RuleTypes; - method: string; - severity: string; - lastCompletedRun: string; - lastResponse: LastResponseTypes; - tags: string | string[]; - activate: boolean; - } - - interface PageTypes { - index: number; - size: number; - } - - interface SortTypes { - field: string; - direction: string; - } - - const actions = [ - { - description: 'Edit rule settings', - icon: 'visControls', - name: 'Edit rule settings', - onClick: () => {}, - }, - { - description: 'Run rule manually…', - icon: 'play', - name: 'Run rule manually…', - onClick: () => {}, - }, - { - description: 'Duplicate rule…', - icon: 'copy', - name: 'Duplicate rule…', - onClick: () => {}, - }, - { - description: 'Export rule', - icon: 'exportAction', - name: 'Export rule', - onClick: () => {}, - }, - { - description: 'Delete rule…', - icon: 'trash', - name: 'Delete rule…', - onClick: () => {}, - }, - ]; - - // Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes? - const columns = [ - { - field: 'rule', - name: 'Rule', - render: (value: ColumnTypes['rule']) => ( -
- {value.name}{' '} - {value.status} -
- ), - sortable: true, - truncateText: true, - width: '24%', - }, - { - field: 'method', - name: 'Method', - sortable: true, - truncateText: true, - }, - { - field: 'severity', - name: 'Severity', - render: (value: ColumnTypes['severity']) => ( - - {value} - - ), - sortable: true, - truncateText: true, - }, - { - field: 'lastCompletedRun', - name: 'Last completed run', - render: (value: ColumnTypes['lastCompletedRun']) => { - return value === undefined ? ( - getEmptyTagValue() - ) : ( - - ); - }, - sortable: true, - truncateText: true, - width: '16%', - }, - { - field: 'lastResponse', - name: 'Last response', - render: (value: ColumnTypes['lastResponse']) => { - return value === undefined ? ( - getEmptyTagValue() - ) : ( - <> - {value.type === 'Fail' ? ( - - {value.type} - - ) : ( - {value.type} - )} - - ); - }, - sortable: true, - truncateText: true, - }, - { - field: 'tags', - name: 'Tags', - render: (value: ColumnTypes['tags']) => ( -
- {typeof value !== 'string' ? ( - <> - {value.map((tag, i) => ( - - {tag} - - ))} - - ) : ( - {value} - )} -
- ), - sortable: true, - truncateText: true, - width: '20%', - }, - { - align: 'center', - field: 'activate', - name: 'Activate', - render: (value: ColumnTypes['activate']) => ( - // Michael: Uncomment props below when EUI 14.9.0 is added to Kibana. - {}} showLabel={false} /> - ), - sortable: true, - width: '65px', - }, - { - actions, - width: '40px', - }, - ]; - - const sampleTableData = [ - { - id: 1, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Low', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: ['attack.t1234', 'attack.t4321'], - activate: true, - }, - { - id: 2, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Medium', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Fail', - message: 'Full fail message here.', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 3, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'High', - tags: 'attack.t1234', - activate: false, - }, - { - id: 4, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 5, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 6, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 7, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 8, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 9, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 10, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 11, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 12, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 13, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 14, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 15, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 16, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 17, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 18, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 19, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 20, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 21, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - ]; - - const [itemsTotalState] = useState(sampleTableData.length); - const [pageState, setPageState] = useState({ index: 0, size: 20 }); - // const [selectedState, setSelectedState] = useState([]); - const [sortState, setSortState] = useState({ field: 'rule', direction: 'asc' }); - - return ( - <> - - - - - - - - - - - {'Showing: 39 rules'} - - - - {'Selected: 2 rules'} - - {'Batch actions context menu here.'}

} - > - {'Batch actions'} -
-
- - - {'Clear 7 filters'} - -
-
- - { - setPageState(page); - setSortState(sort); - }} - pagination={{ - pageIndex: pageState.index, - pageSize: pageState.size, - totalItemCount: itemsTotalState, - pageSizeOptions: [5, 10, 20], - }} - selection={{ - selectable: () => true, - onSelectionChange: (selectedItems: ColumnTypes[]) => { - // setSelectedState(selectedItems); - }, - }} - sorting={{ - sort: sortState, - }} - /> -
- - ); -}); -AllRules.displayName = 'AllRules'; - -const ActivityMonitor = React.memo(() => { - interface RuleTypes { - href: string; - name: string; - } - - interface ColumnTypes { - id: number; - rule: RuleTypes; - ran: string; - lookedBackTo: string; - status: string; - response: string | undefined; - } - - interface PageTypes { - index: number; - size: number; - } - - interface SortTypes { - field: string; - direction: string; - } - - const actions = [ - { - available: (item: ColumnTypes) => item.status === 'Running', - description: 'Stop', - icon: 'stop', - isPrimary: true, - name: 'Stop', - onClick: () => {}, - type: 'icon', - }, - { - available: (item: ColumnTypes) => item.status === 'Stopped', - description: 'Resume', - icon: 'play', - isPrimary: true, - name: 'Resume', - onClick: () => {}, - type: 'icon', - }, - ]; - - // Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes? - const columns = [ - { - field: 'rule', - name: 'Rule', - render: (value: ColumnTypes['rule']) => {value.name}, - sortable: true, - truncateText: true, - }, - { - field: 'ran', - name: 'Ran', - render: (value: ColumnTypes['ran']) => , - sortable: true, - truncateText: true, - }, - { - field: 'lookedBackTo', - name: 'Looked back to', - render: (value: ColumnTypes['lookedBackTo']) => ( - - ), - sortable: true, - truncateText: true, - }, - { - field: 'status', - name: 'Status', - sortable: true, - truncateText: true, - }, - { - field: 'response', - name: 'Response', - render: (value: ColumnTypes['response']) => { - return value === undefined ? ( - getEmptyTagValue() - ) : ( - <> - {value === 'Fail' ? ( - - {value} - - ) : ( - {value} - )} - - ); - }, - sortable: true, - truncateText: true, - }, - { - actions, - width: '40px', - }, - ]; - - const sampleTableData = [ - { - id: 1, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Running', - }, - { - id: 2, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Stopped', - }, - { - id: 3, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Fail', - }, - { - id: 4, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 5, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 6, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 7, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 8, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 9, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 10, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 11, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 12, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 13, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 14, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 15, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 16, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 17, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 18, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 19, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 20, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 21, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - ]; - - const [itemsTotalState] = useState(sampleTableData.length); - const [pageState, setPageState] = useState({ index: 0, size: 20 }); - // const [selectedState, setSelectedState] = useState([]); - const [sortState, setSortState] = useState({ field: 'ran', direction: 'desc' }); - - return ( - <> - - - - - - - - - {'Showing: 39 activites'} - - - - {'Selected: 2 activities'} - - {'Stop selected'} - - - - {'Clear 7 filters'} - - - - - { - setPageState(page); - setSortState(sort); - }} - pagination={{ - pageIndex: pageState.index, - pageSize: pageState.size, - totalItemCount: itemsTotalState, - pageSizeOptions: [5, 10, 20], - }} - selection={{ - selectable: (item: ColumnTypes) => item.status !== 'Completed', - selectableMessage: (selectable: boolean) => - selectable ? undefined : 'Completed runs cannot be acted upon', - onSelectionChange: (selectedItems: ColumnTypes[]) => { - // setSelectedState(selectedItems); - }, - }} - sorting={{ - sort: sortState, - }} - /> - - - ); -}); -ActivityMonitor.displayName = 'ActivityMonitor'; +import { AllRules } from './all_rules'; +import { ActivityMonitor } from './activity_monitor'; +import { FormattedRelativePreferenceDate } from '../../../components/formatted_date'; +import { getEmptyTagValue } from '../../../components/empty_value'; +import { ImportRuleModal } from './components/import_rule_modal'; export const RulesComponent = React.memo(() => { + const [showImportModal, setShowImportModal] = useState(false); + const [importCompleteToggle, setImportCompleteToggle] = useState(false); + + const lastCompletedRun = undefined; return ( <> + setShowImportModal(false)} + importComplete={() => setImportCompleteToggle(!importCompleteToggle)} + /> , + }} + /> + ) : ( + getEmptyTagValue() + ) + } title={i18n.PAGE_TITLE} > - - {'Import rule…'} + { + setShowImportModal(true); + }} + > + {i18n.IMPORT_RULE} - {'Add new rule'} + {i18n.ADD_NEW_RULE} @@ -1057,12 +73,12 @@ export const RulesComponent = React.memo(() => { tabs={[ { id: 'tabAllRules', - name: 'All rules', - content: , + name: i18n.ALL_RULES, + content: , }, { id: 'tabActivityMonitor', - name: 'Activity monitor', + name: i18n.ACTIVITY_MONITOR, content: , }, ]} @@ -1073,4 +89,5 @@ export const RulesComponent = React.memo(() => { ); }); + RulesComponent.displayName = 'RulesComponent'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts index 2b20c726d4b3f..9ae266e396f6d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts @@ -6,6 +6,202 @@ import { i18n } from '@kbn/i18n'; +export const BACK_TO_DETECTION_ENGINE = i18n.translate( + 'xpack.siem.detectionEngine.rules.backOptionsHeader', + { + defaultMessage: 'Back to detection engine', + } +); + +export const IMPORT_RULE = i18n.translate('xpack.siem.detectionEngine.rules.importRuleTitle', { + defaultMessage: 'Import rule…', +}); + +export const ADD_NEW_RULE = i18n.translate('xpack.siem.detectionEngine.rules.addNewRuleTitle', { + defaultMessage: 'Add new rule', +}); + +export const ACTIVITY_MONITOR = i18n.translate( + 'xpack.siem.detectionEngine.rules.activityMonitorTitle', + { + defaultMessage: 'Activity monitor', + } +); + export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.rules.pageTitle', { defaultMessage: 'Rules', }); + +export const REFRESH = i18n.translate('xpack.siem.detectionEngine.rules.allRules.refreshTitle', { + defaultMessage: 'Refresh', +}); + +export const BATCH_ACTIONS = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.batchActionsTitle', + { + defaultMessage: 'Batch actions', + } +); + +export const BATCH_ACTION_ACTIVATE_SELECTED = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.batchActions.activateSelectedTitle', + { + defaultMessage: 'Activate selected', + } +); + +export const BATCH_ACTION_DEACTIVATE_SELECTED = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.batchActions.deactivateSelectedTitle', + { + defaultMessage: 'Deactivate selected', + } +); + +export const BATCH_ACTION_EXPORT_SELECTED = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.batchActions.exportSelectedTitle', + { + defaultMessage: 'Export selected', + } +); + +export const BATCH_ACTION_EDIT_INDEX_PATTERNS = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.batchActions.editIndexPatternsTitle', + { + defaultMessage: 'Edit selected index patterns…', + } +); + +export const BATCH_ACTION_DELETE_SELECTED = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.batchActions.deleteSelectedTitle', + { + defaultMessage: 'Delete selected…', + } +); + +export const EXPORT_FILENAME = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.exportFilenameTitle', + { + defaultMessage: 'rules_export', + } +); + +export const SUCCESSFULLY_EXPORTED_RULES = (totalRules: number) => + i18n.translate('xpack.siem.detectionEngine.rules.allRules.successfullyExportedRulesTitle', { + values: { totalRules }, + defaultMessage: + 'Successfully exported {totalRules} {totalRules, plural, =1 {rule} other {rules}}', + }); + +export const ALL_RULES = i18n.translate('xpack.siem.detectionEngine.rules.allRules.tableTitle', { + defaultMessage: 'All rules', +}); + +export const SEARCH_RULES = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.searchAriaLabel', + { + defaultMessage: 'Search rules', + } +); + +export const SEARCH_PLACEHOLDER = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.searchPlaceholder', + { + defaultMessage: 'e.g. rule name', + } +); + +export const SHOWING_RULES = (totalRules: number) => + i18n.translate('xpack.siem.detectionEngine.rules.allRules.showingRulesTitle', { + values: { totalRules }, + defaultMessage: 'Showing {totalRules} {totalRules, plural, =1 {rule} other {rules}}', + }); + +export const SELECTED_RULES = (selectedRules: number) => + i18n.translate('xpack.siem.detectionEngine.rules.allRules.selectedRulesTitle', { + values: { selectedRules }, + defaultMessage: 'Selected {selectedRules} {selectedRules, plural, =1 {rule} other {rules}}', + }); + +export const EDIT_RULE_SETTINGS = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.actions.editRuleSettingsDescription', + { + defaultMessage: 'Edit rule settings', + } +); + +export const RUN_RULE_MANUALLY = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.actions.runRuleManuallyDescription', + { + defaultMessage: 'Run rule manually…', + } +); + +export const DUPLICATE_RULE = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.actions.duplicateRuleDescription', + { + defaultMessage: 'Duplicate rule…', + } +); + +export const EXPORT_RULE = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.actions.exportRuleDescription', + { + defaultMessage: 'Export rule', + } +); + +export const DELETE_RULE = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.actions.deleteeRuleDescription', + { + defaultMessage: 'Delete rule…', + } +); + +export const COLUMN_RULE = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.columns.ruleTitle', + { + defaultMessage: 'Rule', + } +); + +export const COLUMN_METHOD = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.columns.methodTitle', + { + defaultMessage: 'Method', + } +); + +export const COLUMN_SEVERITY = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.columns.severityTitle', + { + defaultMessage: 'Severity', + } +); + +export const COLUMN_LAST_COMPLETE_RUN = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.columns.lastCompletedRunTitle', + { + defaultMessage: 'Last completed run', + } +); + +export const COLUMN_LAST_RESPONSE = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.columns.lastResponseTitle', + { + defaultMessage: 'Last response', + } +); + +export const COLUMN_TAGS = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.columns.tagsTitle', + { + defaultMessage: 'Tags', + } +); + +export const COLUMN_ACTIVATE = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.columns.activateTitle', + { + defaultMessage: 'Activate', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts new file mode 100644 index 0000000000000..8cbc61e677f8c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts @@ -0,0 +1,41 @@ +/* + * 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 { Rule } from '../../../containers/detection_engine/rules/types'; + +export interface EuiBasicTableSortTypes { + field: string; + direction: 'asc' | 'desc'; +} + +export interface EuiBasicTableOnChange { + page: { + index: number; + size: number; + }; + sort: EuiBasicTableSortTypes; +} + +export interface TableData { + id: string; + rule_id: string; + rule: { + href: string; + name: string; + status: string; + }; + method: string; + severity: string; + lastCompletedRun: string | undefined; + lastResponse: { + type: string; + message?: string; + }; + tags: string[]; + activate: boolean; + isLoading: boolean; + sourceRule: Rule; +}