From bfe9b45a55980e4b43ab76634307c020d9a2dd7c Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Tue, 21 Jan 2020 21:54:55 -0500 Subject: [PATCH] [SIEM] Detections create prepackage rules (#55403) (#55503) * update extra action on rule detail to match design * remove experimental label * allow pre-package to be deleted + do not allow wrong user to create pre-packages rules * Additional look back minimum value to 1 * fix flow with edit rule * add success toaster when rule is created or updated * Fix Timeline selector loading * review ben doc + change detectin engine to detection even in url * Succeeded text size consistency in rule details page * fix description of threats * fix test * fix type * fix internatinalization * adding pre-packaged rules * fix bug + enhance ux * unified icon * fix i18n * fix bugs * review I * review II * add border back --- .../legacy/plugins/siem/common/constants.ts | 3 +- .../containers/detection_engine/rules/api.ts | 41 +- .../detection_engine/rules/index.ts | 1 + .../detection_engine/rules/translations.ts | 14 + .../rules/use_create_packaged_rules.tsx | 81 ---- .../rules/use_pre_packaged_rules.tsx | 166 +++++++ .../detection_engine/rules/use_rules.tsx | 19 +- .../signals/use_privilege_user.tsx | 1 + .../components/user_info/index.tsx | 9 - .../detection_engine/detection_engine.tsx | 34 +- .../detection_engine/rules/all/actions.tsx | 7 +- .../detection_engine/rules/all/index.tsx | 435 ++++++++++-------- .../detection_engine/rules/all/reducer.ts | 36 +- .../pre_packaged_rules/load_empty_prompt.tsx | 65 +++ .../pre_packaged_rules/translations.ts | 57 +++ .../pre_packaged_rules/update_callout.tsx | 31 ++ .../detection_engine/rules/create/index.tsx | 30 +- .../detection_engine/rules/details/index.tsx | 16 +- .../detection_engine/rules/edit/index.tsx | 19 +- .../pages/detection_engine/rules/helpers.tsx | 46 ++ .../pages/detection_engine/rules/index.tsx | 99 +++- .../detection_engine/rules/translations.ts | 14 + 22 files changed, 865 insertions(+), 359 deletions(-) delete mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_create_packaged_rules.tsx create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.tsx diff --git a/x-pack/legacy/plugins/siem/common/constants.ts b/x-pack/legacy/plugins/siem/common/constants.ts index e67b533e46bc..03824dd4e396 100644 --- a/x-pack/legacy/plugins/siem/common/constants.ts +++ b/x-pack/legacy/plugins/siem/common/constants.ts @@ -60,7 +60,8 @@ export const DETECTION_ENGINE_PREPACKAGED_URL = `${DETECTION_ENGINE_RULES_URL}/p export const DETECTION_ENGINE_PRIVILEGES_URL = `${DETECTION_ENGINE_URL}/privileges`; export const DETECTION_ENGINE_INDEX_URL = `${DETECTION_ENGINE_URL}/index`; export const DETECTION_ENGINE_TAGS_URL = `${DETECTION_ENGINE_URL}/tags`; -export const DETECTION_ENGINE_RULES_STATUS = `${DETECTION_ENGINE_URL}/rules/_find_statuses`; +export const DETECTION_ENGINE_RULES_STATUS_URL = `${DETECTION_ENGINE_RULES_URL}/_find_statuses`; +export const DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL = `${DETECTION_ENGINE_RULES_URL}/prepackaged/_status`; /** * Default signals index key for kibana.dev.yml diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts index a83e874437c1..6c9fd5c0ff3b 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts @@ -26,7 +26,8 @@ import { throwIfNotOk } from '../../../hooks/api/api'; import { DETECTION_ENGINE_RULES_URL, DETECTION_ENGINE_PREPACKAGED_URL, - DETECTION_ENGINE_RULES_STATUS, + DETECTION_ENGINE_RULES_STATUS_URL, + DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL, } from '../../../../common/constants'; import * as i18n from '../../../pages/detection_engine/rules/translations'; @@ -63,7 +64,7 @@ export const addRule = async ({ rule, signal }: AddRulesProps): Promise export const fetchRules = async ({ filterOptions = { filter: '', - sortField: 'enabled', + sortField: 'name', sortOrder: 'desc', }, pagination = { @@ -313,6 +314,7 @@ export const exportRules = async ({ * Get Rule Status provided Rule ID * * @param id string of Rule ID's (not rule_id) + * @param signal AbortSignal for cancelling request * * @throws An error if response is not OK */ @@ -324,7 +326,7 @@ export const getRuleStatusById = async ({ signal: AbortSignal; }): Promise> => { const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_STATUS}?ids=${encodeURIComponent( + `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_STATUS_URL}?ids=${encodeURIComponent( JSON.stringify([id]) )}`, { @@ -341,3 +343,36 @@ export const getRuleStatusById = async ({ await throwIfNotOk(response); return response.json(); }; + +/** + * Get pre packaged rules Status + * + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const getPrePackagedRulesStatus = async ({ + signal, +}: { + signal: AbortSignal; +}): Promise<{ + rules_installed: number; + rules_not_installed: number; + rules_not_updated: number; +}> => { + const response = await fetch( + `${chrome.getBasePath()}${DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL}`, + { + method: 'GET', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-xsrf': 'true', + }, + signal, + } + ); + + await throwIfNotOk(response); + return response.json(); +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/index.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/index.ts index e9a0f27b3469..c7ecfb33cd90 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/index.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/index.ts @@ -10,4 +10,5 @@ export * from './persist_rule'; export * from './types'; export * from './use_rule'; export * from './use_rules'; +export * from './use_pre_packaged_rules'; export * from './use_rule_status'; 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 index 39efbde2ad5c..a493e471a9bf 100644 --- 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 @@ -16,3 +16,17 @@ export const RULE_ADD_FAILURE = i18n.translate( defaultMessage: 'Failed to add Rule', } ); + +export const RULE_PREPACKAGED_FAILURE = i18n.translate( + 'xpack.siem.containers.detectionEngine.createPrePackagedRuleFailDescription', + { + defaultMessage: 'Failed to installed pre-packaged rules from elastic', + } +); + +export const RULE_PREPACKAGED_SUCCESS = i18n.translate( + 'xpack.siem.containers.detectionEngine.createPrePackagedRuleSuccesDescription', + { + defaultMessage: 'Installed pre-packaged rules from elastic', + } +); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_create_packaged_rules.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_create_packaged_rules.tsx deleted file mode 100644 index 592419f87901..000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_create_packaged_rules.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { useEffect, useState } from 'react'; - -import { createPrepackagedRules } from './api'; - -type Return = [boolean, boolean | null]; - -interface UseCreatePackagedRules { - canUserCRUD: boolean | null; - hasIndexManage: boolean | null; - hasManageApiKey: boolean | null; - isAuthenticated: boolean | null; - isSignalIndexExists: boolean | null; -} - -/** - * Hook for creating the packages rules - * - * @param canUserCRUD boolean - * @param hasIndexManage boolean - * @param hasManageApiKey boolean - * @param isAuthenticated boolean - * @param isSignalIndexExists boolean - * - * @returns [loading, hasCreatedPackageRules] - */ -export const useCreatePackagedRules = ({ - canUserCRUD, - hasIndexManage, - hasManageApiKey, - isAuthenticated, - isSignalIndexExists, -}: UseCreatePackagedRules): Return => { - const [hasCreatedPackageRules, setHasCreatedPackageRules] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - let isSubscribed = true; - const abortCtrl = new AbortController(); - setLoading(true); - - async function createRules() { - try { - await createPrepackagedRules({ - signal: abortCtrl.signal, - }); - - if (isSubscribed) { - setHasCreatedPackageRules(true); - } - } catch (error) { - if (isSubscribed) { - setHasCreatedPackageRules(false); - } - } - if (isSubscribed) { - setLoading(false); - } - } - if ( - canUserCRUD && - hasIndexManage && - hasManageApiKey && - isAuthenticated && - isSignalIndexExists - ) { - createRules(); - } - return () => { - isSubscribed = false; - abortCtrl.abort(); - }; - }, [canUserCRUD, hasIndexManage, hasManageApiKey, isAuthenticated, isSignalIndexExists]); - - return [loading, hasCreatedPackageRules]; -}; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx new file mode 100644 index 000000000000..abb43f3570e6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx @@ -0,0 +1,166 @@ +/* + * 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, useRef } from 'react'; + +import { useStateToaster, displaySuccessToast } from '../../../components/toasters'; +import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; +import { getPrePackagedRulesStatus, createPrepackagedRules } from './api'; +import * as i18n from './translations'; + +type Func = () => void; +export type CreatePreBuiltRules = () => Promise; +interface Return { + createPrePackagedRules: null | CreatePreBuiltRules; + loading: boolean; + loadingCreatePrePackagedRules: boolean; + refetchPrePackagedRulesStatus: Func | null; + rulesInstalled: number | null; + rulesNotInstalled: number | null; + rulesNotUpdated: number | null; +} + +interface UsePrePackagedRuleProps { + canUserCRUD: boolean | null; + hasIndexManage: boolean | null; + hasManageApiKey: boolean | null; + isAuthenticated: boolean | null; + isSignalIndexExists: boolean | null; +} + +/** + * Hook for using to get status about pre-packaged Rules from the Detection Engine API + * + * @param hasIndexManage boolean + * @param hasManageApiKey boolean + * @param isAuthenticated boolean + * @param isSignalIndexExists boolean + * + */ +export const usePrePackagedRules = ({ + canUserCRUD, + hasIndexManage, + hasManageApiKey, + isAuthenticated, + isSignalIndexExists, +}: UsePrePackagedRuleProps): Return => { + const [rulesInstalled, setRulesInstalled] = useState(null); + const [rulesNotInstalled, setRulesNotInstalled] = useState(null); + const [rulesNotUpdated, setRulesNotUpdated] = useState(null); + const [loadingCreatePrePackagedRules, setLoadingCreatePrePackagedRules] = useState(false); + const [loading, setLoading] = useState(true); + const createPrePackagedRules = useRef(null); + const refetchPrePackagedRules = useRef(null); + const [, dispatchToaster] = useStateToaster(); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + + const fetchPrePackagedRules = async () => { + try { + setLoading(true); + const prePackagedRuleStatusResponse = await getPrePackagedRulesStatus({ + signal: abortCtrl.signal, + }); + + if (isSubscribed) { + setRulesInstalled(prePackagedRuleStatusResponse.rules_installed); + setRulesNotInstalled(prePackagedRuleStatusResponse.rules_not_installed); + setRulesNotUpdated(prePackagedRuleStatusResponse.rules_not_updated); + } + } catch (error) { + if (isSubscribed) { + setRulesInstalled(null); + setRulesNotInstalled(null); + setRulesNotUpdated(null); + errorToToaster({ title: i18n.RULE_FETCH_FAILURE, error, dispatchToaster }); + } + } + if (isSubscribed) { + setLoading(false); + } + }; + + const createElasticRules = async (): Promise => { + return new Promise(async resolve => { + try { + if ( + canUserCRUD && + hasIndexManage && + hasManageApiKey && + isAuthenticated && + isSignalIndexExists + ) { + setLoadingCreatePrePackagedRules(true); + await createPrepackagedRules({ + signal: abortCtrl.signal, + }); + + if (isSubscribed) { + let iterationTryOfFetchingPrePackagedCount = 0; + let timeoutId = -1; + const stopTimeOut = () => { + if (timeoutId !== -1) { + window.clearTimeout(timeoutId); + } + }; + const reFetch = () => + window.setTimeout(async () => { + iterationTryOfFetchingPrePackagedCount = + iterationTryOfFetchingPrePackagedCount + 1; + const prePackagedRuleStatusResponse = await getPrePackagedRulesStatus({ + signal: abortCtrl.signal, + }); + if ( + isSubscribed && + ((prePackagedRuleStatusResponse.rules_not_installed === 0 && + prePackagedRuleStatusResponse.rules_not_updated === 0) || + iterationTryOfFetchingPrePackagedCount > 100) + ) { + setLoadingCreatePrePackagedRules(false); + setRulesInstalled(prePackagedRuleStatusResponse.rules_installed); + setRulesNotInstalled(prePackagedRuleStatusResponse.rules_not_installed); + setRulesNotUpdated(prePackagedRuleStatusResponse.rules_not_updated); + displaySuccessToast(i18n.RULE_PREPACKAGED_SUCCESS, dispatchToaster); + stopTimeOut(); + resolve(true); + } else { + timeoutId = reFetch(); + } + }, 300); + timeoutId = reFetch(); + } + } + } catch (error) { + if (isSubscribed) { + setLoadingCreatePrePackagedRules(false); + errorToToaster({ title: i18n.RULE_PREPACKAGED_FAILURE, error, dispatchToaster }); + resolve(false); + } + } + }); + }; + + fetchPrePackagedRules(); + createPrePackagedRules.current = createElasticRules; + refetchPrePackagedRules.current = fetchPrePackagedRules; + return () => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, [canUserCRUD, hasIndexManage, hasManageApiKey, isAuthenticated, isSignalIndexExists]); + + return { + loading, + loadingCreatePrePackagedRules, + refetchPrePackagedRulesStatus: refetchPrePackagedRules.current, + rulesInstalled, + rulesNotInstalled, + rulesNotUpdated, + createPrePackagedRules: createPrePackagedRules.current, + }; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx index b49dd8d51d4f..04475ab1bc17 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect, useState } from 'react'; +import { useEffect, useState, useRef } from 'react'; import { FetchRulesResponse, FilterOptions, PaginationOptions } from './types'; import { useStateToaster } from '../../../components/toasters'; @@ -12,36 +12,33 @@ import { fetchRules } from './api'; import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; import * as i18n from './translations'; -type Return = [boolean, FetchRulesResponse]; +type Func = () => void; +type Return = [boolean, FetchRulesResponse, Func | null]; /** * 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 => { +export const useRules = (pagination: PaginationOptions, filterOptions: FilterOptions): Return => { const [rules, setRules] = useState({ page: 1, perPage: 20, total: 0, data: [], }); + const reFetchRules = useRef(null); const [loading, setLoading] = useState(true); const [, dispatchToaster] = useStateToaster(); useEffect(() => { let isSubscribed = true; const abortCtrl = new AbortController(); - setLoading(true); async function fetchData() { try { + setLoading(true); const fetchRulesResult = await fetchRules({ filterOptions, pagination, @@ -62,12 +59,12 @@ export const useRules = ( } fetchData(); + reFetchRules.current = fetchData; return () => { isSubscribed = false; abortCtrl.abort(); }; }, [ - refetchToggle, pagination.page, pagination.perPage, filterOptions.filter, @@ -75,5 +72,5 @@ export const useRules = ( filterOptions.sortOrder, ]); - return [loading, rules]; + return [loading, rules, reFetchRules.current]; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx index 7d0e331200d5..564cf224a9fc 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx @@ -59,6 +59,7 @@ export const usePrivilegeUser = (): Return => { setAuthenticated(false); setHasIndexManage(false); setHasIndexWrite(false); + setHasManageApiKey(false); errorToToaster({ title: i18n.PRIVILEGE_FETCH_FAILURE, error, dispatchToaster }); } } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx index 24e14473d40e..bbaccb788248 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx @@ -10,7 +10,6 @@ import React, { useEffect, useReducer, Dispatch, createContext, useContext } fro import { usePrivilegeUser } from '../../../../containers/detection_engine/signals/use_privilege_user'; import { useSignalIndex } from '../../../../containers/detection_engine/signals/use_signal_index'; import { useKibana } from '../../../../lib/kibana'; -import { useCreatePackagedRules } from '../../../../containers/detection_engine/rules/use_create_packaged_rules'; export interface State { canUserCRUD: boolean | null; @@ -162,14 +161,6 @@ export const useUserInfo = (): State => { createSignalIndex, ] = useSignalIndex(); - useCreatePackagedRules({ - canUserCRUD, - hasIndexManage, - hasManageApiKey, - isAuthenticated, - isSignalIndexExists, - }); - const uiCapabilities = useKibana().services.application.capabilities; const capabilitiesCanUserCRUD: boolean = typeof uiCapabilities.siem.crud === 'boolean' ? uiCapabilities.siem.crud : false; 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 5586749ce38d..0e37740fe4a3 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 @@ -90,23 +90,6 @@ const DetectionEngineComponent = React.memo( [setAbsoluteRangeDatePicker] ); - if (isUserAuthenticated != null && !isUserAuthenticated && !loading) { - return ( - - - - - ); - } - if (isSignalIndexExists != null && !isSignalIndexExists && !loading) { - return ( - - - - - ); - } - const tabs = useMemo( () => ( @@ -125,6 +108,23 @@ const DetectionEngineComponent = React.memo( [detectionsTabs, tabName] ); + if (isUserAuthenticated != null && !isUserAuthenticated && !loading) { + return ( + + + + + ); + } + if (isSignalIndexExists != null && !isSignalIndexExists && !loading) { + return ( + + + + + ); + } + return ( <> {hasIndexWrite != null && !hasIndexWrite && } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx index 435edcab433b..d6bf8643fff1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx @@ -64,13 +64,12 @@ export const deleteRulesAction = async ( onRuleDeleted?: () => void ) => { try { - dispatch({ type: 'updateLoading', ids, isLoading: true }); + dispatch({ type: 'loading', isLoading: true }); const response = await deleteRules({ ids }); - const { rules, errors } = bucketRulesResponse(response); - - dispatch({ type: 'deleteRules', rules }); + const { errors } = bucketRulesResponse(response); + dispatch({ type: 'refresh' }); if (errors.length > 0) { displayErrorToast( i18n.BATCH_ACTION_DELETE_SELECTED_ERROR(ids.length), diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx index 4aa6b778582f..677033b25883 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx @@ -11,10 +11,12 @@ import { EuiLoadingContent, EuiSpacer, } from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react'; import { useHistory } from 'react-router-dom'; - import uuid from 'uuid'; + +import { useRules, CreatePreBuiltRules } from '../../../../containers/detection_engine/rules'; import { HeaderSection } from '../../../../components/header_section'; import { UtilityBar, @@ -23,16 +25,17 @@ import { UtilityBarSection, UtilityBarText, } from '../../../../components/detection_engine/utility_bar'; -import { getColumns } from './columns'; -import { useRules } from '../../../../containers/detection_engine/rules'; +import { useStateToaster } from '../../../../components/toasters'; 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 { PrePackagedRulesPrompt } from '../components/pre_packaged_rules/load_empty_prompt'; import { RuleDownloader } from '../components/rule_downloader'; -import { useStateToaster } from '../../../../components/toasters'; +import { getPrePackagedRuleStatus } from '../helpers'; +import * as i18n from '../translations'; +import { EuiBasicTableOnChange, TableData } from '../types'; +import { getBatchItems } from './batch_actions'; +import { getColumns } from './columns'; +import { allRulesReducer, State } from './reducer'; const initialState: State = { isLoading: true, @@ -52,6 +55,19 @@ const initialState: State = { }, }; +interface AllRulesProps { + createPrePackagedRules: CreatePreBuiltRules | null; + hasNoPermissions: boolean; + importCompleteToggle: boolean; + loading: boolean; + loadingCreatePrePackagedRules: boolean; + refetchPrePackagedRulesStatus: () => void; + rulesInstalled: number | null; + rulesNotInstalled: number | null; + rulesNotUpdated: number | null; + setRefreshRulesData: (refreshRule: () => void) => void; +} + /** * 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: @@ -60,191 +76,248 @@ const initialState: State = { * * Delete * * Import/Export */ -export const AllRules = React.memo<{ - hasNoPermissions: boolean; - importCompleteToggle: boolean; - loading: boolean; -}>(({ hasNoPermissions, importCompleteToggle, loading }) => { - const [ - { - exportPayload, - filterOptions, - isLoading, - refreshToggle, - selectedItems, - tableData, - pagination, - }, - dispatch, - ] = useReducer(allRulesReducer, initialState); - const history = useHistory(); - const [isInitialLoad, setIsInitialLoad] = useState(true); - const [isLoadingRules, rulesData] = useRules(pagination, filterOptions, refreshToggle); - const [, dispatchToaster] = useStateToaster(); +export const AllRules = React.memo( + ({ + createPrePackagedRules, + hasNoPermissions, + importCompleteToggle, + loading, + loadingCreatePrePackagedRules, + refetchPrePackagedRulesStatus, + rulesInstalled, + rulesNotInstalled, + rulesNotUpdated, + setRefreshRulesData, + }) => { + const [ + { + exportPayload, + filterOptions, + isLoading, + refreshToggle, + selectedItems, + tableData, + pagination, + }, + dispatch, + ] = useReducer(allRulesReducer, initialState); + const history = useHistory(); + const [isInitialLoad, setIsInitialLoad] = useState(true); + const [isGlobalLoading, setIsGlobalLoad] = useState(false); + const [, dispatchToaster] = useStateToaster(); + const [isLoadingRules, rulesData, reFetchRulesData] = useRules(pagination, filterOptions); - const getBatchItemsPopoverContent = useCallback( - (closePopover: () => void) => ( - - ), - [selectedItems, dispatch, dispatchToaster, history] - ); + const prePackagedRuleStatus = getPrePackagedRuleStatus( + rulesInstalled, + rulesNotInstalled, + rulesNotUpdated + ); - const tableOnChangeCallback = useCallback( - ({ page, sort }: EuiBasicTableOnChange) => { + const getBatchItemsPopoverContent = useCallback( + (closePopover: () => void) => ( + + ), + [selectedItems, dispatch, dispatchToaster, history] + ); + + const tableOnChangeCallback = useCallback( + ({ page, sort }: EuiBasicTableOnChange) => { + 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 ?? 'desc', + }, + }); + }, + [dispatch, filterOptions, pagination] + ); + + const columns = useMemo(() => { + return getColumns(dispatch, dispatchToaster, history, hasNoPermissions); + }, [dispatch, dispatchToaster, history]); + + useEffect(() => { + dispatch({ type: 'loading', isLoading: isLoadingRules }); + }, [isLoadingRules]); + + useEffect(() => { + if (!isLoadingRules && !loading && isInitialLoad) { + setIsInitialLoad(false); + } + }, [isInitialLoad, isLoadingRules, loading]); + + useEffect(() => { + if (!isGlobalLoading && (isLoadingRules || isLoading)) { + setIsGlobalLoad(true); + } else if (isGlobalLoading && !isLoadingRules && !isLoading) { + setIsGlobalLoad(false); + } + }, [setIsGlobalLoad, isGlobalLoading, isLoadingRules, isLoading]); + + useEffect(() => { + if (!isInitialLoad) { + dispatch({ type: 'refresh' }); + } + }, [importCompleteToggle]); + + useEffect(() => { + if (reFetchRulesData != null) { + reFetchRulesData(); + } + refetchPrePackagedRulesStatus(); + }, [refreshToggle, reFetchRulesData, refetchPrePackagedRulesStatus]); + + useEffect(() => { + if (reFetchRulesData != null) { + setRefreshRulesData(reFetchRulesData); + } + }, [reFetchRulesData, setRefreshRulesData]); + + useEffect(() => { 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 ?? 'desc', + type: 'updateRules', + rules: rulesData.data, + pagination: { + page: rulesData.page, + perPage: rulesData.perPage, + total: rulesData.total, }, }); - }, - [dispatch, filterOptions, pagination] - ); + }, [rulesData]); - const columns = useMemo(() => { - return getColumns(dispatch, dispatchToaster, history, hasNoPermissions); - }, [dispatch, dispatchToaster, history]); + const handleCreatePrePackagedRules = useCallback(async () => { + if (createPrePackagedRules != null) { + await createPrePackagedRules(); + dispatch({ type: 'refresh' }); + } + }, [createPrePackagedRules]); - useEffect(() => { - dispatch({ type: 'loading', isLoading: isLoadingRules }); + const euiBasicTableSelectionProps = useMemo( + () => ({ + selectable: (item: TableData) => !item.isLoading, + onSelectionChange: (selected: TableData[]) => + dispatch({ type: 'setSelected', selectedItems: selected }), + }), + [] + ); - if (!isLoadingRules) { - setIsInitialLoad(false); - } - }, [isLoadingRules]); + return ( + <> + { + dispatchToaster({ + type: 'addToaster', + toast: { + id: uuid.v4(), + title: i18n.SUCCESSFULLY_EXPORTED_RULES(exportCount), + color: 'success', + iconType: 'check', + }, + }); + }} + /> + - 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]); - - const euiBasicTableSelectionProps = useMemo( - () => ({ - selectable: (item: TableData) => !item.isLoading, - onSelectionChange: (selected: TableData[]) => - dispatch({ type: 'setSelected', selectedItems: selected }), - }), - [] - ); - - 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, - }, - }); - dispatch({ - type: 'updatePagination', - pagination: { ...pagination, page: 1 }, - }); - }} - /> - - - - - - {i18n.SHOWING_RULES(pagination.total ?? 0)} - - - - {i18n.SELECTED_RULES(selectedItems.length)} - {!hasNoPermissions && ( - - {i18n.BATCH_ACTIONS} - - )} - dispatch({ type: 'refresh' })} - > - {i18n.REFRESH} - - - - - - - {(isLoading || loading) && ( + {rulesInstalled != null && rulesInstalled > 0 && ( + + { + dispatch({ + type: 'updateFilterOptions', + filterOptions: { + ...filterOptions, + filter: filterString, + }, + }); + dispatch({ + type: 'updatePagination', + pagination: { ...pagination, page: 1 }, + }); + }} + /> + + )} + {isInitialLoad && isEmpty(tableData) && ( + + )} + {isGlobalLoading && !isEmpty(tableData) && ( )} + {isEmpty(tableData) && prePackagedRuleStatus === 'ruleNotInstalled' && ( + + )} + {!isEmpty(tableData) && ( + <> + + + + {i18n.SHOWING_RULES(pagination.total ?? 0)} + + + + {i18n.SELECTED_RULES(selectedItems.length)} + {!hasNoPermissions && ( + + {i18n.BATCH_ACTIONS} + + )} + dispatch({ type: 'refresh' })} + > + {i18n.REFRESH} + + + + + + + + )} - )} - - - ); -}); + + + ); + } +); AllRules.displayName = 'AllRules'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/reducer.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/reducer.ts index 22d6ca2195fe..74ce8f2847fa 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/reducer.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/reducer.ts @@ -58,19 +58,21 @@ export const allRulesReducer = (state: State, action: Action): State => { const ruleIds = state.rules.map(r => r.rule_id); const appendIdx = action.appendRuleId != null ? state.rules.findIndex(r => r.id === action.appendRuleId) : -1; - const updatedRules = action.rules.reduce( - (rules, updatedRule) => - ruleIds.includes(updatedRule.rule_id) - ? rules.map(r => (updatedRule.rule_id === r.rule_id ? updatedRule : r)) - : appendIdx !== -1 - ? [ - ...rules.slice(0, appendIdx + 1), - updatedRule, - ...rules.slice(appendIdx + 1, rules.length - 1), - ] - : [...rules, updatedRule], - [...state.rules] - ); + const updatedRules = action.rules.reverse().reduce((rules, updatedRule) => { + let newRules = rules; + if (ruleIds.includes(updatedRule.rule_id)) { + newRules = newRules.map(r => (updatedRule.rule_id === r.rule_id ? updatedRule : r)); + } else if (appendIdx !== -1) { + newRules = [ + ...newRules.slice(0, appendIdx + 1), + updatedRule, + ...newRules.slice(appendIdx + 1, newRules.length), + ]; + } else { + newRules = [...newRules, updatedRule]; + } + return newRules; + }, state.rules); // Update enabled on selectedItems so that batch actions show correct available actions const updatedRuleIdToState = action.rules.reduce>( @@ -88,6 +90,13 @@ export const allRulesReducer = (state: State, action: Action): State => { rules: updatedRules, tableData: formatRules(updatedRules), selectedItems: updatedSelectedItems, + pagination: { + ...state.pagination, + total: + action.appendRuleId != null + ? state.pagination.total + action.rules.length + : state.pagination.total, + }, }; } case 'updatePagination': { @@ -112,6 +121,7 @@ export const allRulesReducer = (state: State, action: Action): State => { ...state, rules: updatedRules, tableData: formatRules(updatedRules), + refreshToggle: !state.refreshToggle, }; } case 'setSelected': { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.tsx new file mode 100644 index 000000000000..41b7fafd6bec --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.tsx @@ -0,0 +1,65 @@ +/* + * 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 { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; +import React, { memo, useCallback } from 'react'; +import styled from 'styled-components'; + +import { DETECTION_ENGINE_PAGE_NAME } from '../../../../../components/link_to/redirect_to_detection_engine'; +import * as i18n from './translations'; + +const EmptyPrompt = styled(EuiEmptyPrompt)` + align-self: center; /* Corrects horizontal centering in IE11 */ +`; + +interface PrePackagedRulesPromptProps { + createPrePackagedRules: () => void; + loading: boolean; + userHasNoPermissions: boolean; +} + +const PrePackagedRulesPromptComponent: React.FC = ({ + createPrePackagedRules, + loading = false, + userHasNoPermissions = true, +}) => { + const handlePreBuiltCreation = useCallback(() => { + createPrePackagedRules(); + }, [createPrePackagedRules]); + return ( + {i18n.PRE_BUILT_TITLE}} + body={

{i18n.PRE_BUILT_MSG}

} + actions={ + + + + {i18n.PRE_BUILT_ACTION} + + + + + {i18n.CREATE_RULE_ACTION} + + + + } + /> + ); +}; + +export const PrePackagedRulesPrompt = memo(PrePackagedRulesPromptComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/translations.ts new file mode 100644 index 000000000000..5f89bd072ebe --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/translations.ts @@ -0,0 +1,57 @@ +/* + * 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 PRE_BUILT_TITLE = i18n.translate( + 'xpack.siem.detectionEngine.rules.prePackagedRules.emptyPromptTitle', + { + defaultMessage: 'Load Elastic prebuilt detection rules', + } +); + +export const PRE_BUILT_MSG = i18n.translate( + 'xpack.siem.detectionEngine.rules.prePackagedRules.emptyPromptMessage', + { + defaultMessage: + 'Elastic SIEM comes with prebuilt detection rules that run in the background and create signals when their conditions are met.By default, all prebuilt rules are disabled and you select which rules you want to activate', + } +); + +export const PRE_BUILT_ACTION = i18n.translate( + 'xpack.siem.detectionEngine.rules.prePackagedRules.loadPreBuiltButton', + { + defaultMessage: 'Load prebuilt detection rules', + } +); + +export const CREATE_RULE_ACTION = i18n.translate( + 'xpack.siem.detectionEngine.rules.prePackagedRules.createOwnRuletButton', + { + defaultMessage: 'Create your own rules', + } +); + +export const UPDATE_PREPACKAGED_RULES_TITLE = i18n.translate( + 'xpack.siem.detectionEngine.rules.updatePrePackagedRulesTitle', + { + defaultMessage: 'Update available for Elastic prebuilt rules', + } +); + +export const UPDATE_PREPACKAGED_RULES_MSG = (updateRules: number) => + i18n.translate('xpack.siem.detectionEngine.rules.updatePrePackagedRulesMsg', { + values: { updateRules }, + defaultMessage: + 'You can update {updateRules} Elastic prebuilt {updateRules, plural, =1 {rule} other {rules}}. Note that this will reload deleted Elastic prebuilt rules.', + }); + +export const UPDATE_PREPACKAGED_RULES = (updateRules: number) => + i18n.translate('xpack.siem.detectionEngine.rules.updatePrePackagedRulesButton', { + values: { updateRules }, + defaultMessage: + 'Update {updateRules} Elastic prebuilt {updateRules, plural, =1 {rule} other {rules}} ', + }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.tsx new file mode 100644 index 000000000000..80a120ebc63e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.tsx @@ -0,0 +1,31 @@ +/* + * 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, { memo } from 'react'; + +import { EuiCallOut, EuiButton } from '@elastic/eui'; +import * as i18n from './translations'; + +interface UpdatePrePackagedRulesCallOutProps { + loading: boolean; + numberOfUpdatedRules: number; + updateRules: () => void; +} + +const UpdatePrePackagedRulesCallOutComponent: React.FC = ({ + loading, + numberOfUpdatedRules, + updateRules, +}) => ( + +

{i18n.UPDATE_PREPACKAGED_RULES_MSG(numberOfUpdatedRules)}

+ + {i18n.UPDATE_PREPACKAGED_RULES(numberOfUpdatedRules)} + +
+); + +export const UpdatePrePackagedRulesCallOut = memo(UpdatePrePackagedRulesCallOutComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx index cbc60015d9c8..6eaaf37c0668 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx @@ -29,10 +29,10 @@ import * as i18n from './translations'; const stepsRuleOrder = [RuleStep.defineRule, RuleStep.aboutRule, RuleStep.scheduleRule]; const MyEuiPanel = styled(EuiPanel)<{ - zIndex?: number; + zindex?: number; }>` position: relative; - z-index: ${props => props.zIndex}; /* ugly fix to allow searchBar to overflow the EuiPanel */ + z-index: ${props => props.zindex}; /* ugly fix to allow searchBar to overflow the EuiPanel */ .euiAccordion__iconWrapper { display: none; @@ -80,16 +80,6 @@ export const CreateRuleComponent = React.memo(() => { const userHasNoPermissions = canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false; - if ( - isSignalIndexExists != null && - isAuthenticated != null && - (!isSignalIndexExists || !isAuthenticated) - ) { - return ; - } else if (userHasNoPermissions) { - return ; - } - const setStepData = useCallback( (step: RuleStep, data: unknown, isValid: boolean) => { stepsData.current[step] = { ...stepsData.current[step], data, isValid }; @@ -228,6 +218,16 @@ export const CreateRuleComponent = React.memo(() => { return ; } + if ( + isSignalIndexExists != null && + isAuthenticated != null && + (!isSignalIndexExists || !isAuthenticated) + ) { + return ; + } else if (userHasNoPermissions) { + return ; + } + return ( <> @@ -237,7 +237,7 @@ export const CreateRuleComponent = React.memo(() => { isLoading={isLoading || loading} title={i18n.PAGE_TITLE} /> - + { - + { - + ( const userHasNoPermissions = canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false; - if ( - isSignalIndexExists != null && - isAuthenticated != null && - (!isSignalIndexExists || !isAuthenticated) - ) { - return ; - } - const title = isLoading === true || rule === null ? : rule.name; const subTitle = useMemo( () => @@ -228,6 +220,14 @@ const RuleDetailsComponent = memo( [ruleEnabled, setRuleEnabled] ); + if ( + isSignalIndexExists != null && + isAuthenticated != null && + (!isSignalIndexExists || !isAuthenticated) + ) { + return ; + } + return ( <> {hasIndexWrite != null && !hasIndexWrite && } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx index be56e916ae6c..8c0b78e66847 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx @@ -62,15 +62,6 @@ export const EditRuleComponent = memo(() => { const userHasNoPermissions = canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false; - if ( - isSignalIndexExists != null && - isAuthenticated != null && - (!isSignalIndexExists || !isAuthenticated) - ) { - return ; - } else if (userHasNoPermissions) { - return ; - } const [initForm, setInitForm] = useState(false); const [myAboutRuleForm, setMyAboutRuleForm] = useState({ @@ -277,6 +268,16 @@ export const EditRuleComponent = memo(() => { return ; } + if ( + isSignalIndexExists != null && + isAuthenticated != null && + (!isSignalIndexExists || !isAuthenticated) + ) { + return ; + } else if (userHasNoPermissions) { + return ; + } + return ( <> diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx index cfe6cb8da1cb..4cbaa38e1feb 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx @@ -69,6 +69,52 @@ export const getStepsData = ({ export const useQuery = () => new URLSearchParams(useLocation().search); +export type PrePackagedRuleStatus = + | 'ruleInstalled' + | 'ruleNotInstalled' + | 'ruleNeedUpdate' + | 'someRuleUninstall' + | 'unknown'; + +export const getPrePackagedRuleStatus = ( + rulesInstalled: number | null, + rulesNotInstalled: number | null, + rulesNotUpdated: number | null +): PrePackagedRuleStatus => { + if ( + rulesNotInstalled != null && + rulesInstalled === 0 && + rulesNotInstalled > 0 && + rulesNotUpdated === 0 + ) { + return 'ruleNotInstalled'; + } else if ( + rulesInstalled != null && + rulesInstalled > 0 && + rulesNotInstalled === 0 && + rulesNotUpdated === 0 + ) { + return 'ruleInstalled'; + } else if ( + rulesInstalled != null && + rulesNotInstalled != null && + rulesInstalled > 0 && + rulesNotInstalled > 0 && + rulesNotUpdated === 0 + ) { + return 'someRuleUninstall'; + } else if ( + rulesInstalled != null && + rulesNotInstalled != null && + rulesNotUpdated != null && + rulesInstalled > 0 && + rulesNotInstalled >= 0 && + rulesNotUpdated > 0 + ) { + return 'ruleNeedUpdate'; + } + return 'unknown'; +}; export const setFieldValue = ( form: FormHook, schema: FormSchema, 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 dd46b33ca725..5cdc7a1d4fa6 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 @@ -6,32 +6,81 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useState } from 'react'; +import React, { useCallback, useRef, useState } from 'react'; import { Redirect } from 'react-router-dom'; +import { usePrePackagedRules } from '../../../containers/detection_engine/rules'; import { DETECTION_ENGINE_PAGE_NAME } from '../../../components/link_to/redirect_to_detection_engine'; import { FormattedRelativePreferenceDate } from '../../../components/formatted_date'; import { getEmptyTagValue } from '../../../components/empty_value'; import { HeaderPage } from '../../../components/header_page'; import { WrapperPage } from '../../../components/wrapper_page'; import { SpyRoute } from '../../../utils/route/spy_routes'; - +import { useUserInfo } from '../components/user_info'; import { AllRules } from './all'; import { ImportRuleModal } from './components/import_rule_modal'; import { ReadOnlyCallOut } from './components/read_only_callout'; -import { useUserInfo } from '../components/user_info'; +import { UpdatePrePackagedRulesCallOut } from './components/pre_packaged_rules/update_callout'; +import { getPrePackagedRuleStatus } from './helpers'; import * as i18n from './translations'; +type Func = () => void; + export const RulesComponent = React.memo(() => { const [showImportModal, setShowImportModal] = useState(false); const [importCompleteToggle, setImportCompleteToggle] = useState(false); + const refreshRulesData = useRef(null); const { loading, isSignalIndexExists, isAuthenticated, canUserCRUD, + hasIndexManage, hasManageApiKey, } = useUserInfo(); + const { + createPrePackagedRules, + loading: prePackagedRuleLoading, + loadingCreatePrePackagedRules, + refetchPrePackagedRulesStatus, + rulesInstalled, + rulesNotInstalled, + rulesNotUpdated, + } = usePrePackagedRules({ + canUserCRUD, + hasIndexManage, + hasManageApiKey, + isSignalIndexExists, + isAuthenticated, + }); + const prePackagedRuleStatus = getPrePackagedRuleStatus( + rulesInstalled, + rulesNotInstalled, + rulesNotUpdated + ); + + const userHasNoPermissions = + canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false; + const lastCompletedRun = undefined; + + const handleCreatePrePackagedRules = useCallback(async () => { + if (createPrePackagedRules != null) { + await createPrePackagedRules(); + if (refreshRulesData.current != null) { + refreshRulesData.current(); + } + } + }, [createPrePackagedRules, refreshRulesData]); + + const handleRefetchPrePackagedRulesStatus = useCallback(() => { + if (refetchPrePackagedRulesStatus != null) { + refetchPrePackagedRulesStatus(); + } + }, [refetchPrePackagedRulesStatus]); + + const handleSetRefreshRulesData = useCallback((refreshRule: Func) => { + refreshRulesData.current = refreshRule; + }, []); if ( isSignalIndexExists != null && @@ -40,9 +89,7 @@ export const RulesComponent = React.memo(() => { ) { return ; } - const userHasNoPermissions = - canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false; - const lastCompletedRun = undefined; + return ( <> {userHasNoPermissions && } @@ -73,6 +120,30 @@ export const RulesComponent = React.memo(() => { title={i18n.PAGE_TITLE} > + {prePackagedRuleStatus === 'ruleNotInstalled' && ( + + + {i18n.LOAD_PREPACKAGED_RULES} + + + )} + {prePackagedRuleStatus === 'someRuleUninstall' && ( + + + {i18n.RELOAD_MISSING_PREPACKAGED_RULES(rulesNotInstalled ?? 0)} + + + )} { + {prePackagedRuleStatus === 'ruleNeedUpdate' && ( + + )} 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 d144a6d56a16..672aab8ef831 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 @@ -310,3 +310,17 @@ export const UPDATE = i18n.translate('xpack.siem.detectionEngine.rules.updateBut export const DELETE = i18n.translate('xpack.siem.detectionEngine.rules.deleteDescription', { defaultMessage: 'Delete', }); + +export const LOAD_PREPACKAGED_RULES = i18n.translate( + 'xpack.siem.detectionEngine.rules.loadPrePackagedRulesButton', + { + defaultMessage: 'Load Elastic prebuilt rules', + } +); + +export const RELOAD_MISSING_PREPACKAGED_RULES = (missingRules: number) => + i18n.translate('xpack.siem.detectionEngine.rules.reloadMissingPrePackagedRulesButton', { + values: { missingRules }, + defaultMessage: + 'Reload {missingRules} deleted Elastic prebuilt {missingRules, plural, =1 {rule} other {rules}} ', + });