[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>
This commit is contained in:
parent
8ae51abe19
commit
9567cca7d0
|
@ -322,7 +322,7 @@ export const getRuleStatusById = async ({
|
|||
}: {
|
||||
id: string;
|
||||
signal: AbortSignal;
|
||||
}): Promise<Record<string, RuleStatus[]>> => {
|
||||
}): Promise<Record<string, RuleStatus>> => {
|
||||
const response = await fetch(
|
||||
`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_STATUS}?ids=${encodeURIComponent(
|
||||
JSON.stringify([id])
|
||||
|
|
|
@ -10,3 +10,4 @@ export * from './persist_rule';
|
|||
export * from './types';
|
||||
export * from './use_rule';
|
||||
export * from './use_rules';
|
||||
export * from './use_rule_status';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<RuleStatus[] | null>(null);
|
||||
const [ruleStatus, setRuleStatus] = useState<RuleStatus | null>(null);
|
||||
const fetchRuleStatus = useRef<Func | null>(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];
|
||||
};
|
||||
|
|
|
@ -96,5 +96,5 @@ export interface Privilege {
|
|||
write: boolean;
|
||||
};
|
||||
};
|
||||
isAuthenticated: boolean;
|
||||
is_authenticated: boolean;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<Action>,
|
||||
|
@ -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 (
|
||||
<>
|
||||
<EuiHealth color={color}>{value ?? getEmptyTagValue()}</EuiHealth>
|
||||
<EuiHealth color={getStatusColor(value ?? null)}>
|
||||
{value ?? getEmptyTagValue()}
|
||||
</EuiHealth>
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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';
|
|
@ -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<RuleStatusProps> = ({ ruleId, ruleEnabled }) => {
|
||||
const [loading, ruleStatus, fetchRuleStatus] = useRuleStatus(ruleId);
|
||||
const [myRuleEnabled, setMyRuleEnabled] = useState<boolean | null>(ruleEnabled ?? null);
|
||||
const [currentStatus, setCurrentStatus] = useState<RuleInfoStatus | null>(
|
||||
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 (
|
||||
<EuiFlexGroup gutterSize="xs" alignItems="center" justifyContent="flexStart">
|
||||
<EuiFlexItem grow={false}>
|
||||
{i18n.STATUS}
|
||||
{':'}
|
||||
</EuiFlexItem>
|
||||
{loading && (
|
||||
<EuiFlexItem>
|
||||
<EuiLoadingSpinner size="m" data-test-subj="rule-status-loader" />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{!loading && (
|
||||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiHealth color={getStatusColor(currentStatus?.status ?? null)}>
|
||||
<EuiText size="xs">{currentStatus?.status ?? getEmptyTagValue()}</EuiText>
|
||||
</EuiHealth>
|
||||
</EuiFlexItem>
|
||||
{currentStatus?.status_date != null && currentStatus?.status != null && (
|
||||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
<>{i18n.STATUS_AT}</>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={true}>
|
||||
<FormattedDate value={currentStatus?.status_date} fieldName={i18n.STATUS_DATE} />
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
color="primary"
|
||||
onClick={handleRefresh}
|
||||
iconType="refresh"
|
||||
aria-label={i18n.REFRESH}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export const RuleStatus = memo(RuleStatusComponent);
|
|
@ -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',
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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<FailureHistoryProps> = ({ id }) => {
|
|||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
const columns: Array<EuiBasicTableColumn<RuleStatus>> = [
|
||||
const columns: Array<EuiBasicTableColumn<RuleInfoStatus>> = [
|
||||
{
|
||||
name: i18n.COLUMN_STATUS_TYPE,
|
||||
render: () => <EuiHealth color="danger">{i18n.TYPE_FAILED}</EuiHealth>,
|
||||
|
@ -65,7 +64,9 @@ const FailureHistoryComponent: React.FC<FailureHistoryProps> = ({ id }) => {
|
|||
<EuiBasicTable
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
items={ruleStatus != null ? ruleStatus?.filter(rs => rs.last_failure_at != null) : []}
|
||||
items={
|
||||
ruleStatus != null ? ruleStatus?.failures.filter(rs => rs.last_failure_at != null) : []
|
||||
}
|
||||
sorting={{ sort: { field: 'status_date', direction: 'desc' } }}
|
||||
/>
|
||||
</EuiPanel>
|
||||
|
|
|
@ -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<RuleDetailsComponentProps>(
|
|||
} = 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<boolean | null>(null);
|
||||
const [ruleDetailTab, setRuleDetailTab] = useState(RuleDetailTabs.signals);
|
||||
const { aboutRuleData, defineRuleData, scheduleRuleData } = getStepsData({
|
||||
rule,
|
||||
|
@ -182,17 +182,6 @@ const RuleDetailsComponent = memo<RuleDetailsComponentProps>(
|
|||
filters,
|
||||
]);
|
||||
|
||||
const statusColor =
|
||||
rule?.status == null
|
||||
? 'subdued'
|
||||
: rule?.status === 'succeeded'
|
||||
? 'success'
|
||||
: rule?.status === 'failed'
|
||||
? 'danger'
|
||||
: rule?.status === 'executing'
|
||||
? 'warning'
|
||||
: 'subdued';
|
||||
|
||||
const tabs = useMemo(
|
||||
() => (
|
||||
<EuiTabs>
|
||||
|
@ -230,6 +219,15 @@ const RuleDetailsComponent = memo<RuleDetailsComponentProps>(
|
|||
[setAbsoluteRangeDatePicker]
|
||||
);
|
||||
|
||||
const handleOnChangeEnabledRule = useCallback(
|
||||
(enabled: boolean) => {
|
||||
if (ruleEnabled == null || enabled !== ruleEnabled) {
|
||||
setRuleEnabled(enabled);
|
||||
}
|
||||
},
|
||||
[ruleEnabled, setRuleEnabled]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasIndexWrite != null && !hasIndexWrite && <NoWriteSignalsCallOut />}
|
||||
|
@ -262,34 +260,7 @@ const RuleDetailsComponent = memo<RuleDetailsComponentProps>(
|
|||
</>,
|
||||
]
|
||||
: []),
|
||||
<EuiFlexGroup
|
||||
gutterSize="xs"
|
||||
alignItems="center"
|
||||
justifyContent="flexStart"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
{i18n.STATUS}
|
||||
{':'}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiHealth color={statusColor}>
|
||||
<EuiText size="xs">{rule?.status ?? getEmptyTagValue()}</EuiText>
|
||||
</EuiHealth>
|
||||
</EuiFlexItem>
|
||||
{rule?.status_date && (
|
||||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
<>{i18n.STATUS_AT}</>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={true}>
|
||||
<FormattedDate
|
||||
value={rule?.status_date}
|
||||
fieldName={i18n.STATUS_DATE}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
</EuiFlexGroup>,
|
||||
<RuleStatus ruleId={ruleId ?? null} ruleEnabled={ruleEnabled} />,
|
||||
]}
|
||||
title={title}
|
||||
>
|
||||
|
@ -300,6 +271,7 @@ const RuleDetailsComponent = memo<RuleDetailsComponentProps>(
|
|||
isDisabled={userHasNoPermissions}
|
||||
enabled={rule?.enabled ?? false}
|
||||
optionLabel={i18n.ACTIVATE_RULE}
|
||||
onChange={handleOnChangeEnabledRule}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
|
|
|
@ -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',
|
||||
{
|
||||
|
|
|
@ -391,7 +391,7 @@ export const getMockPrivileges = () => ({
|
|||
},
|
||||
},
|
||||
application: {},
|
||||
isAuthenticated: false,
|
||||
is_authenticated: false,
|
||||
});
|
||||
|
||||
export const getFindResultStatus = (): SavedObjectsFindResponse<IRuleSavedAttributesSavedObjectAttributes> => ({
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 = <T extends Record<string, any>>(obj: T): Partial<T> | 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<Promise<RuleStatusResponse | {}>>(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<IRuleStatusAttributes>(
|
||||
lastFiveErrorsForId.saved_objects[0]?.attributes
|
||||
);
|
||||
const failures = lastFiveErrorsForId.saved_objects
|
||||
.slice(1)
|
||||
.map(errorItem => convertToSnakeCase<IRuleStatusAttributes>(errorItem.attributes));
|
||||
return {
|
||||
...(await acc),
|
||||
[id]: toDisplay.map(errorItem => convertToSnakeCase(errorItem.attributes)),
|
||||
...accumulated,
|
||||
[id]: {
|
||||
current_status: currentStatus,
|
||||
failures,
|
||||
},
|
||||
};
|
||||
}, {});
|
||||
}, Promise.resolve<RuleStatusResponse>({}));
|
||||
return statuses;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -78,6 +78,7 @@ export const createUpdateRulesRoute: Hapi.ServerRoute = {
|
|||
language,
|
||||
outputIndex,
|
||||
savedId,
|
||||
savedObjectsClient,
|
||||
timelineId,
|
||||
timelineTitle,
|
||||
meta,
|
||||
|
|
|
@ -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<string, any> {
|
||||
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<RuleAlertParams> & {
|
||||
id: string | undefined | null;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
} & Clients;
|
||||
|
||||
export type DeleteRuleParams = Clients & {
|
||||
|
|
|
@ -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<void> => {
|
||||
|
@ -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,
|
||||
|
|
|
@ -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<IRuleSavedAttributesSavedObjectAttributes>({
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue