From 9567cca7d0a2def8df05994a981cd1c7f6f59fe2 Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Sat, 18 Jan 2020 12:41:47 -0500 Subject: [PATCH] [SIEM] [Detection Engine] Update status on rule details page (#55201) * adds logic for returning / updating status when a rule is switched from enabled to disabled and vice versa. * update response for find rules statuses to include current status and failures * update status on demand and on enable/disable * adds ternary to allow removal of 'let' * adds savedObjectsClient to the add and upate prepackaged rules and import rules route. * fix bug where convertToSnakeCase would throw error 'cannot convert null or undefined to object' if passed null * genericize snake_case converter and updates isAuthorized to snake_case (different situation) * renaming to 'going to run' instead of executing because when task manager exits because of api key error it won't write the error status so the actual status is 'going to run' on the next interval. This is more accurate than being stuck on 'executing' because of an error we don't control and can't write a status for. * fix missed merge conflict Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> --- .../containers/detection_engine/rules/api.ts | 2 +- .../detection_engine/rules/index.ts | 1 + .../detection_engine/rules/types.ts | 8 +- .../rules/use_rule_status.tsx | 15 +-- .../detection_engine/signals/types.ts | 2 +- .../signals/use_privilege_user.tsx | 2 +- .../detection_engine/rules/all/columns.tsx | 15 +-- .../rules/components/rule_status/helpers.ts | 18 ++++ .../rules/components/rule_status/index.tsx | 99 +++++++++++++++++++ .../components/rule_status/translations.ts | 29 ++++++ .../rules/components/rule_switch/index.tsx | 5 + .../rules/details/failure_history.tsx | 9 +- .../detection_engine/rules/details/index.tsx | 58 +++-------- .../rules/details/translations.ts | 18 ---- .../routes/__mocks__/request_responses.ts | 2 +- .../privileges/read_privileges_route.ts | 2 +- .../rules/add_prepackaged_rules_route.ts | 14 ++- .../routes/rules/find_rules_status_route.ts | 30 ++++-- .../routes/rules/import_rules_route.ts | 7 +- .../routes/rules/update_rules_bulk_route.ts | 25 ++++- .../routes/rules/update_rules_route.ts | 1 + .../lib/detection_engine/rules/types.ts | 20 +++- .../rules/update_prepacked_rules.ts | 3 + .../detection_engine/rules/update_rules.ts | 33 ++++++- .../signals/signal_rule_alert_type.ts | 4 +- 25 files changed, 310 insertions(+), 112 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/translations.ts 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 8cd3e8f2d45c..a83e874437c1 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 @@ -322,7 +322,7 @@ export const getRuleStatusById = async ({ }: { id: string; signal: AbortSignal; -}): Promise> => { +}): Promise> => { const response = await fetch( `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_STATUS}?ids=${encodeURIComponent( JSON.stringify([id]) 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 a61cbabd8062..e9a0f27b3469 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,3 +10,4 @@ export * from './persist_rule'; export * from './types'; export * from './use_rule'; export * from './use_rules'; +export * from './use_rule_status'; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts index 334daa8d1d02..0dcd0da5be8f 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts @@ -181,9 +181,15 @@ export interface ExportRulesProps { } export interface RuleStatus { + current_status: RuleInfoStatus; + failures: RuleInfoStatus[]; +} + +export type RuleStatusType = 'executing' | 'failed' | 'going to run' | 'succeeded'; +export interface RuleInfoStatus { alert_id: string; status_date: string; - status: string; + status: RuleStatusType | null; last_failure_at: string | null; last_success_at: string | null; last_failure_message: string | null; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx index 216fbcea861a..466c2cddac97 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useStateToaster } from '../../../components/toasters'; import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; @@ -12,7 +12,8 @@ import { getRuleStatusById } from './api'; import * as i18n from './translations'; import { RuleStatus } from './types'; -type Return = [boolean, RuleStatus[] | null]; +type Func = (ruleId: string) => void; +type Return = [boolean, RuleStatus | null, Func | null]; /** * Hook for using to get a Rule from the Detection Engine API @@ -21,7 +22,8 @@ type Return = [boolean, RuleStatus[] | null]; * */ export const useRuleStatus = (id: string | undefined | null): Return => { - const [ruleStatus, setRuleStatus] = useState(null); + const [ruleStatus, setRuleStatus] = useState(null); + const fetchRuleStatus = useRef(null); const [loading, setLoading] = useState(true); const [, dispatchToaster] = useStateToaster(); @@ -29,7 +31,7 @@ export const useRuleStatus = (id: string | undefined | null): Return => { let isSubscribed = true; const abortCtrl = new AbortController(); - async function fetchData(idToFetch: string) { + const fetchData = async (idToFetch: string) => { try { setLoading(true); const ruleStatusResponse = await getRuleStatusById({ @@ -49,15 +51,16 @@ export const useRuleStatus = (id: string | undefined | null): Return => { if (isSubscribed) { setLoading(false); } - } + }; if (id != null) { fetchData(id); } + fetchRuleStatus.current = fetchData; return () => { isSubscribed = false; abortCtrl.abort(); }; }, [id]); - return [loading, ruleStatus]; + return [loading, ruleStatus, fetchRuleStatus.current]; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts index 34cb7684a039..ea4860dafd40 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts @@ -96,5 +96,5 @@ export interface Privilege { write: boolean; }; }; - isAuthenticated: boolean; + is_authenticated: boolean; } 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 792ff29ad248..7d0e331200d5 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 @@ -42,7 +42,7 @@ export const usePrivilegeUser = (): Return => { }); if (isSubscribed && privilege != null) { - setAuthenticated(privilege.isAuthenticated); + setAuthenticated(privilege.is_authenticated); if (privilege.index != null && Object.keys(privilege.index).length > 0) { const indexName = Object.keys(privilege.index)[0]; setHasIndexManage(privilege.index[indexName].manage); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx index f62343f77c49..d546c4edb55d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx @@ -30,6 +30,7 @@ import { FormattedDate } from '../../../../components/formatted_date'; import { RuleSwitch } from '../components/rule_switch'; import { SeverityBadge } from '../components/severity_badge'; import { ActionToaster } from '../../../../components/toasters'; +import { getStatusColor } from '../components/rule_status/helpers'; const getActions = ( dispatch: React.Dispatch, @@ -113,19 +114,11 @@ export const getColumns = ( field: 'status', name: i18n.COLUMN_LAST_RESPONSE, render: (value: TableData['status']) => { - const color = - value == null - ? 'subdued' - : value === 'succeeded' - ? 'success' - : value === 'failed' - ? 'danger' - : value === 'executing' - ? 'warning' - : 'subdued'; return ( <> - {value ?? getEmptyTagValue()} + + {value ?? getEmptyTagValue()} + ); }, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.ts new file mode 100644 index 000000000000..263f602251ea --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RuleStatusType } from '../../../../../containers/detection_engine/rules'; + +export const getStatusColor = (status: RuleStatusType | string | null) => + status == null + ? 'subdued' + : status === 'succeeded' + ? 'success' + : status === 'failed' + ? 'danger' + : status === 'executing' || status === 'going to run' + ? 'warning' + : 'subdued'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx new file mode 100644 index 000000000000..2c9173cbeb69 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx @@ -0,0 +1,99 @@ +/* + * 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 { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiHealth, + EuiLoadingSpinner, + EuiText, +} from '@elastic/eui'; +import { isEqual } from 'lodash/fp'; +import React, { memo, useCallback, useEffect, useState } from 'react'; + +import { useRuleStatus, RuleInfoStatus } from '../../../../../containers/detection_engine/rules'; +import { FormattedDate } from '../../../../../components/formatted_date'; +import { getEmptyTagValue } from '../../../../../components/empty_value'; +import { getStatusColor } from './helpers'; +import * as i18n from './translations'; + +interface RuleStatusProps { + ruleId: string | null; + ruleEnabled?: boolean | null; +} + +const RuleStatusComponent: React.FC = ({ ruleId, ruleEnabled }) => { + const [loading, ruleStatus, fetchRuleStatus] = useRuleStatus(ruleId); + const [myRuleEnabled, setMyRuleEnabled] = useState(ruleEnabled ?? null); + const [currentStatus, setCurrentStatus] = useState( + ruleStatus?.current_status ?? null + ); + + useEffect(() => { + if (myRuleEnabled !== ruleEnabled && fetchRuleStatus != null && ruleId != null) { + fetchRuleStatus(ruleId); + if (myRuleEnabled !== ruleEnabled) { + setMyRuleEnabled(ruleEnabled ?? null); + } + } + }, [fetchRuleStatus, myRuleEnabled, ruleId, ruleEnabled, setMyRuleEnabled]); + + useEffect(() => { + if (!isEqual(currentStatus, ruleStatus?.current_status)) { + setCurrentStatus(ruleStatus?.current_status ?? null); + } + }, [currentStatus, ruleStatus, setCurrentStatus]); + + const handleRefresh = useCallback(() => { + if (fetchRuleStatus != null && ruleId != null) { + fetchRuleStatus(ruleId); + } + }, [fetchRuleStatus, ruleId]); + + return ( + + + {i18n.STATUS} + {':'} + + {loading && ( + + + + )} + {!loading && ( + <> + + + {currentStatus?.status ?? getEmptyTagValue()} + + + {currentStatus?.status_date != null && currentStatus?.status != null && ( + <> + + <>{i18n.STATUS_AT} + + + + + + )} + + + + + )} + + ); +}; + +export const RuleStatus = memo(RuleStatusComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/translations.ts new file mode 100644 index 000000000000..e03cc252ad72 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/translations.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const STATUS = i18n.translate('xpack.siem.detectionEngine.ruleStatus.statusDescription', { + defaultMessage: 'Status', +}); + +export const STATUS_AT = i18n.translate( + 'xpack.siem.detectionEngine.ruleStatus.statusAtDescription', + { + defaultMessage: 'at', + } +); + +export const STATUS_DATE = i18n.translate( + 'xpack.siem.detectionEngine.ruleStatus.statusDateDescription', + { + defaultMessage: 'Status date', + } +); + +export const REFRESH = i18n.translate('xpack.siem.detectionEngine.ruleStatus.refreshButton', { + defaultMessage: 'Refresh', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx index 9cb0323ed898..09b7ecc9df98 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx @@ -36,6 +36,7 @@ export interface RuleSwitchProps { isDisabled?: boolean; isLoading?: boolean; optionLabel?: string; + onChange?: (enabled: boolean) => void; } /** @@ -48,6 +49,7 @@ export const RuleSwitchComponent = ({ isLoading, enabled, optionLabel, + onChange, }: RuleSwitchProps) => { const [myIsLoading, setMyIsLoading] = useState(false); const [myEnabled, setMyEnabled] = useState(enabled ?? false); @@ -65,6 +67,9 @@ export const RuleSwitchComponent = ({ enabled: event.target.checked!, }); setMyEnabled(updatedRules[0].enabled); + if (onChange != null) { + onChange(updatedRules[0].enabled); + } } catch { setMyIsLoading(false); } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.tsx index 3b49cd30c9aa..f660c1763d5e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.tsx @@ -15,8 +15,7 @@ import { } from '@elastic/eui'; import React, { memo } from 'react'; -import { useRuleStatus } from '../../../../containers/detection_engine/rules/use_rule_status'; -import { RuleStatus } from '../../../../containers/detection_engine/rules'; +import { useRuleStatus, RuleInfoStatus } from '../../../../containers/detection_engine/rules'; import { HeaderSection } from '../../../../components/header_section'; import * as i18n from './translations'; import { FormattedDate } from '../../../../components/formatted_date'; @@ -35,7 +34,7 @@ const FailureHistoryComponent: React.FC = ({ id }) => { ); } - const columns: Array> = [ + const columns: Array> = [ { name: i18n.COLUMN_STATUS_TYPE, render: () => {i18n.TYPE_FAILED}, @@ -65,7 +64,9 @@ const FailureHistoryComponent: React.FC = ({ id }) => { rs.last_failure_at != null) : []} + items={ + ruleStatus != null ? ruleStatus?.failures.filter(rs => rs.last_failure_at != null) : [] + } sorting={{ sort: { field: 'status_date', direction: 'desc' } }} /> diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx index 32e40c754740..a23c681a5aab 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx @@ -10,9 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, - EuiHealth, EuiTab, - EuiText, EuiTabs, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -62,10 +60,10 @@ import { inputsSelectors } from '../../../../store/inputs'; import { State } from '../../../../store'; import { InputsRange } from '../../../../store/inputs/model'; import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../../../store/inputs/actions'; -import { getEmptyTagValue } from '../../../../components/empty_value'; +import { RuleActionsOverflow } from '../components/rule_actions_overflow'; import { RuleStatusFailedCallOut } from './status_failed_callout'; import { FailureHistory } from './failure_history'; -import { RuleActionsOverflow } from '../components/rule_actions_overflow'; +import { RuleStatus } from '../components/rule_status'; interface ReduxProps { filters: esFilters.Filter[]; @@ -113,6 +111,8 @@ const RuleDetailsComponent = memo( } = useUserInfo(); const { ruleId } = useParams(); const [isLoading, rule] = useRule(ruleId); + // This is used to re-trigger api rule status when user de/activate rule + const [ruleEnabled, setRuleEnabled] = useState(null); const [ruleDetailTab, setRuleDetailTab] = useState(RuleDetailTabs.signals); const { aboutRuleData, defineRuleData, scheduleRuleData } = getStepsData({ rule, @@ -182,17 +182,6 @@ const RuleDetailsComponent = memo( filters, ]); - const statusColor = - rule?.status == null - ? 'subdued' - : rule?.status === 'succeeded' - ? 'success' - : rule?.status === 'failed' - ? 'danger' - : rule?.status === 'executing' - ? 'warning' - : 'subdued'; - const tabs = useMemo( () => ( @@ -230,6 +219,15 @@ const RuleDetailsComponent = memo( [setAbsoluteRangeDatePicker] ); + const handleOnChangeEnabledRule = useCallback( + (enabled: boolean) => { + if (ruleEnabled == null || enabled !== ruleEnabled) { + setRuleEnabled(enabled); + } + }, + [ruleEnabled, setRuleEnabled] + ); + return ( <> {hasIndexWrite != null && !hasIndexWrite && } @@ -262,34 +260,7 @@ const RuleDetailsComponent = memo( , ] : []), - - - {i18n.STATUS} - {':'} - - - - {rule?.status ?? getEmptyTagValue()} - - - {rule?.status_date && ( - <> - - <>{i18n.STATUS_AT} - - - - - - )} - , + , ]} title={title} > @@ -300,6 +271,7 @@ const RuleDetailsComponent = memo( isDisabled={userHasNoPermissions} enabled={rule?.enabled ?? false} optionLabel={i18n.ACTIVATE_RULE} + onChange={handleOnChangeEnabledRule} /> diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/translations.ts index 7b349ec646ba..46b6984ab323 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/translations.ts @@ -35,24 +35,6 @@ export const UNKNOWN = i18n.translate('xpack.siem.detectionEngine.ruleDetails.un defaultMessage: 'Unknown', }); -export const STATUS = i18n.translate('xpack.siem.detectionEngine.ruleDetails.statusDescription', { - defaultMessage: 'Status', -}); - -export const STATUS_AT = i18n.translate( - 'xpack.siem.detectionEngine.ruleDetails.statusAtDescription', - { - defaultMessage: 'at', - } -); - -export const STATUS_DATE = i18n.translate( - 'xpack.siem.detectionEngine.ruleDetails.statusDateDescription', - { - defaultMessage: 'Status date', - } -); - export const ERROR_CALLOUT_TITLE = i18n.translate( 'xpack.siem.detectionEngine.ruleDetails.errorCalloutTitle', { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 0c6ab1c82bcb..a84fcb64d9ff 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -391,7 +391,7 @@ export const getMockPrivileges = () => ({ }, }, application: {}, - isAuthenticated: false, + is_authenticated: false, }); export const getFindResultStatus = (): SavedObjectsFindResponse => ({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts index 240200af8b58..803d9d645aad 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts @@ -30,7 +30,7 @@ export const createReadPrivilegesRulesRoute = (server: ServerFacade): Hapi.Serve const index = getIndex(request, server); const permissions = await readPrivileges(callWithRequest, index); return merge(permissions, { - isAuthenticated: request?.auth?.isAuthenticated ?? false, + is_authenticated: request?.auth?.isAuthenticated ?? false, }); } catch (err) { return transformError(err); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index 5ceecdb058e5..3c9cad8dc4d4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -36,8 +36,10 @@ export const createAddPrepackedRulesRoute = (server: ServerFacade): Hapi.ServerR const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; - - if (!alertsClient || !actionsClient) { + const savedObjectsClient = isFunction(request.getSavedObjectsClient) + ? request.getSavedObjectsClient() + : null; + if (!alertsClient || !actionsClient || !savedObjectsClient) { return headers.response().code(404); } @@ -59,7 +61,13 @@ export const createAddPrepackedRulesRoute = (server: ServerFacade): Hapi.ServerR } } await installPrepackagedRules(alertsClient, actionsClient, rulesToInstall, spaceIndex); - await updatePrepackagedRules(alertsClient, actionsClient, rulesToUpdate, spaceIndex); + await updatePrepackagedRules( + alertsClient, + actionsClient, + savedObjectsClient, + rulesToUpdate, + spaceIndex + ); return { rules_installed: rulesToInstall.length, rules_updated: rulesToUpdate.length, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts index e56c440f5a41..545c2e488b1c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts @@ -13,10 +13,16 @@ import { findRulesStatusesSchema } from '../schemas/find_rules_statuses_schema'; import { FindRulesStatusesRequest, IRuleSavedAttributesSavedObjectAttributes, + RuleStatusResponse, + IRuleStatusAttributes, } from '../../rules/types'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; -const convertToSnakeCase = (obj: IRuleSavedAttributesSavedObjectAttributes) => { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const convertToSnakeCase = >(obj: T): Partial | null => { + if (!obj) { + return null; + } return Object.keys(obj).reduce((acc, item) => { const newKey = snakeCase(item); return { ...acc, [newKey]: obj[item] }; @@ -53,7 +59,7 @@ export const createFindRulesStatusRoute: Hapi.ServerRoute = { "anotherAlertId": ... } */ - const statuses = await query.ids.reduce(async (acc, id) => { + const statuses = await query.ids.reduce>(async (acc, id) => { const lastFiveErrorsForId = await savedObjectsClient.find< IRuleSavedAttributesSavedObjectAttributes >({ @@ -64,15 +70,21 @@ export const createFindRulesStatusRoute: Hapi.ServerRoute = { search: id, searchFields: ['alertId'], }); - const toDisplay = - lastFiveErrorsForId.saved_objects.length <= 5 - ? lastFiveErrorsForId.saved_objects - : lastFiveErrorsForId.saved_objects.slice(1); + const accumulated = await acc; + const currentStatus = convertToSnakeCase( + lastFiveErrorsForId.saved_objects[0]?.attributes + ); + const failures = lastFiveErrorsForId.saved_objects + .slice(1) + .map(errorItem => convertToSnakeCase(errorItem.attributes)); return { - ...(await acc), - [id]: toDisplay.map(errorItem => convertToSnakeCase(errorItem.attributes)), + ...accumulated, + [id]: { + current_status: currentStatus, + failures, + }, }; - }, {}); + }, Promise.resolve({})); return statuses; }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index e312b5fc6bb1..6efaa1fea60d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -52,8 +52,10 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; - - if (!alertsClient || !actionsClient) { + const savedObjectsClient = isFunction(request.getSavedObjectsClient) + ? request.getSavedObjectsClient() + : null; + if (!alertsClient || !actionsClient || !savedObjectsClient) { return headers.response().code(404); } const { filename } = request.payload.file.hapi; @@ -161,6 +163,7 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = const updatedRule = await updateRules({ alertsClient, actionsClient, + savedObjectsClient, description, enabled, falsePositives, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index b01108f0de21..e0d2672cf356 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -7,12 +7,16 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { BulkUpdateRulesRequest } from '../../rules/types'; +import { + BulkUpdateRulesRequest, + IRuleSavedAttributesSavedObjectAttributes, +} from '../../rules/types'; import { ServerFacade } from '../../../../types'; import { transformOrBulkError, getIdBulkError } from './utils'; import { transformBulkError } from '../utils'; import { updateRulesBulkSchema } from '../schemas/update_rules_bulk_schema'; import { updateRules } from '../../rules/update_rules'; +import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRoute => { return { @@ -32,8 +36,10 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; - - if (!alertsClient || !actionsClient) { + const savedObjectsClient = isFunction(request.getSavedObjectsClient) + ? request.getSavedObjectsClient() + : null; + if (!alertsClient || !actionsClient || !savedObjectsClient) { return headers.response().code(404); } @@ -80,6 +86,7 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou language, outputIndex, savedId, + savedObjectsClient, timelineId, timelineTitle, meta, @@ -100,7 +107,17 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou version, }); if (rule != null) { - return transformOrBulkError(rule.id, rule); + const ruleStatuses = await savedObjectsClient.find< + IRuleSavedAttributesSavedObjectAttributes + >({ + type: ruleStatusSavedObjectType, + perPage: 1, + sortField: 'statusDate', + sortOrder: 'desc', + search: rule.id, + searchFields: ['alertId'], + }); + return transformOrBulkError(rule.id, rule, ruleStatuses.saved_objects[0]); } else { return getIdBulkError({ id, ruleId }); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index 533fe9b72494..49c9304ae2d2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -78,6 +78,7 @@ export const createUpdateRulesRoute: Hapi.ServerRoute = { language, outputIndex, savedId, + savedObjectsClient, timelineId, timelineTitle, meta, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts index 5a3f19c0bf0e..e238e6398845 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts @@ -7,7 +7,12 @@ import { get } from 'lodash/fp'; import { Readable } from 'stream'; -import { SavedObject, SavedObjectAttributes, SavedObjectsFindResponse } from 'kibana/server'; +import { + SavedObject, + SavedObjectAttributes, + SavedObjectsFindResponse, + SavedObjectsClientContract, +} from 'kibana/server'; import { SIGNALS_ID } from '../../../../common/constants'; import { AlertsClient } from '../../../../../alerting/server/alerts_client'; import { ActionsClient } from '../../../../../actions/server/actions_client'; @@ -41,14 +46,22 @@ export interface RuleAlertType extends Alert { params: RuleTypeParams; } -export interface IRuleStatusAttributes { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface IRuleStatusAttributes extends Record { alertId: string; // created alert id. statusDate: string; lastFailureAt: string | null | undefined; lastFailureMessage: string | null | undefined; lastSuccessAt: string | null | undefined; lastSuccessMessage: string | null | undefined; - status: RuleStatusString; + status: RuleStatusString | null | undefined; +} + +export interface RuleStatusResponse { + [key: string]: { + current_status: IRuleStatusAttributes | null | undefined; + failures: IRuleStatusAttributes[] | null | undefined; + }; } export interface IRuleSavedAttributesSavedObjectAttributes @@ -142,6 +155,7 @@ export interface Clients { export type UpdateRuleParams = Partial & { id: string | undefined | null; + savedObjectsClient: SavedObjectsClientContract; } & Clients; export type DeleteRuleParams = Clients & { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts index 756634c8fa04..0d7fb7918b67 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SavedObjectsClientContract } from 'kibana/server'; import { ActionsClient } from '../../../../../actions'; import { AlertsClient } from '../../../../../alerting'; import { updateRules } from './update_rules'; @@ -12,6 +13,7 @@ import { PrepackagedRules } from '../types'; export const updatePrepackagedRules = async ( alertsClient: AlertsClient, actionsClient: ActionsClient, + savedObjectsClient: SavedObjectsClientContract, rules: PrepackagedRules[], outputIndex: string ): Promise => { @@ -55,6 +57,7 @@ export const updatePrepackagedRules = async ( outputIndex, id: undefined, // We never have an id when updating from pre-packaged rules savedId, + savedObjectsClient, meta, filters, ruleId, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts index 0fe4b15437af..e2632791f859 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts @@ -7,8 +7,9 @@ import { defaults } from 'lodash/fp'; import { AlertAction, IntervalSchedule } from '../../../../../alerting/server/types'; import { readRules } from './read_rules'; -import { UpdateRuleParams } from './types'; +import { UpdateRuleParams, IRuleSavedAttributesSavedObjectAttributes } from './types'; import { addTags } from './add_tags'; +import { ruleStatusSavedObjectType } from './saved_object_mappings'; export const calculateInterval = ( interval: string | undefined, @@ -66,6 +67,7 @@ export const calculateName = ({ export const updateRules = async ({ alertsClient, actionsClient, // TODO: Use this whenever we add feature support for different action types + savedObjectsClient, description, falsePositives, enabled, @@ -135,10 +137,39 @@ export const updateRules = async ({ } ); + const ruleCurrentStatus = savedObjectsClient + ? await savedObjectsClient.find({ + type: ruleStatusSavedObjectType, + perPage: 1, + sortField: 'statusDate', + sortOrder: 'desc', + search: rule.id, + searchFields: ['alertId'], + }) + : null; + if (rule.enabled && enabled === false) { await alertsClient.disable({ id: rule.id }); + // set current status for this rule to null to represent disabled, + // but keep last_success_at / last_failure_at properties intact for + // use on frontend while rule is disabled. + if (ruleCurrentStatus && ruleCurrentStatus.saved_objects.length > 0) { + const currentStatusToDisable = ruleCurrentStatus.saved_objects[0]; + currentStatusToDisable.attributes.status = null; + await savedObjectsClient?.update(ruleStatusSavedObjectType, currentStatusToDisable.id, { + ...currentStatusToDisable.attributes, + }); + } } else if (!rule.enabled && enabled === true) { await alertsClient.enable({ id: rule.id }); + // set current status for this rule to be 'going to run' + if (ruleCurrentStatus && ruleCurrentStatus.saved_objects.length > 0) { + const currentStatusToDisable = ruleCurrentStatus.saved_objects[0]; + currentStatusToDisable.attributes.status = 'going to run'; + await savedObjectsClient?.update(ruleStatusSavedObjectType, currentStatusToDisable.id, { + ...currentStatusToDisable.attributes, + }); + } } else { // enabled is null or undefined and we do not touch the rule } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts index d80eadd2c088..32f2c8691477 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -96,7 +96,7 @@ export const signalRulesAlertType = ({ >(ruleStatusSavedObjectType, { alertId, // do a search for this id. statusDate: date, - status: 'executing', + status: 'going to run', lastFailureAt: null, lastSuccessAt: null, lastFailureMessage: null, @@ -106,7 +106,7 @@ export const signalRulesAlertType = ({ // update 0th to executing. currentStatusSavedObject = ruleStatusSavedObjects.saved_objects[0]; const sDate = new Date().toISOString(); - currentStatusSavedObject.attributes.status = 'executing'; + currentStatusSavedObject.attributes.status = 'going to run'; currentStatusSavedObject.attributes.statusDate = sDate; await services.savedObjectsClient.update( ruleStatusSavedObjectType,