mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[SIEM] [Detection Engine] Rule activity monitoring (#60816)
* backend rule monitoring with gap desc, last look back date, and time duration of search after and bulk create operations * adds new properties to mocked request_response status saved object * first pass at UI table * migrate rule monitoring backend to work with refactor of rule executor, fix some formatting stuff on the frontend, update the mapping for gap to be a string instead of a float * trying to write a test for rules statuses hook * fixed hooks tests * fixes merge conflicts from rebase with master * add columns for indexing and query time lapse * i18n * i18n for tabs * don't change the mappings in ml es_archives * remove accidental commit of interval change for shell script detection engine rule * removes inline object from prop * fix merge conflicts * backend changes from pr comments * updates ui changes from pr feedback * fix tests and add formatting for dates * remove null from rulesStatuses initial state and replace with empty array
This commit is contained in:
parent
29a3f55985
commit
96852249e8
25 changed files with 759 additions and 121 deletions
|
@ -52,6 +52,36 @@ export const getRuleStatusById = async ({
|
|||
last_success_at: 'mm/dd/yyyyTHH:MM:sssz',
|
||||
last_failure_message: null,
|
||||
last_success_message: 'it is a success',
|
||||
gap: null,
|
||||
bulk_create_time_durations: ['2235.01'],
|
||||
search_after_time_durations: ['616.97'],
|
||||
last_look_back_date: '2020-03-19T00:32:07.996Z',
|
||||
},
|
||||
failures: [],
|
||||
},
|
||||
});
|
||||
|
||||
export const getRulesStatusByIds = async ({
|
||||
ids,
|
||||
signal,
|
||||
}: {
|
||||
ids: string[];
|
||||
signal: AbortSignal;
|
||||
}): Promise<RuleStatusResponse> =>
|
||||
Promise.resolve({
|
||||
'12345678987654321': {
|
||||
current_status: {
|
||||
alert_id: 'alertId',
|
||||
status_date: 'mm/dd/yyyyTHH:MM:sssz',
|
||||
status: 'succeeded',
|
||||
last_failure_at: null,
|
||||
last_success_at: 'mm/dd/yyyyTHH:MM:sssz',
|
||||
last_failure_message: null,
|
||||
last_success_message: 'it is a success',
|
||||
gap: null,
|
||||
bulk_create_time_durations: ['2235.01'],
|
||||
search_after_time_durations: ['616.97'],
|
||||
last_look_back_date: '2020-03-19T00:32:07.996Z',
|
||||
},
|
||||
failures: [],
|
||||
},
|
||||
|
|
|
@ -271,6 +271,32 @@ export const getRuleStatusById = async ({
|
|||
signal,
|
||||
});
|
||||
|
||||
/**
|
||||
* Return rule statuses given list of alert ids
|
||||
*
|
||||
* @param ids array of string of Rule ID's (not rule_id)
|
||||
* @param signal AbortSignal for cancelling request
|
||||
*
|
||||
* @throws An error if response is not OK
|
||||
*/
|
||||
export const getRulesStatusByIds = async ({
|
||||
ids,
|
||||
signal,
|
||||
}: {
|
||||
ids: string[];
|
||||
signal: AbortSignal;
|
||||
}): Promise<RuleStatusResponse> => {
|
||||
const res = await KibanaServices.get().http.fetch<RuleStatusResponse>(
|
||||
DETECTION_ENGINE_RULES_STATUS_URL,
|
||||
{
|
||||
method: 'GET',
|
||||
query: { ids: JSON.stringify(ids) },
|
||||
signal,
|
||||
}
|
||||
);
|
||||
return res;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch all unique Tags used by Rules
|
||||
*
|
||||
|
|
|
@ -235,6 +235,10 @@ export interface RuleInfoStatus {
|
|||
last_success_at: string | null;
|
||||
last_failure_message: string | null;
|
||||
last_success_message: string | null;
|
||||
last_look_back_date: string | null | undefined;
|
||||
gap: string | null | undefined;
|
||||
bulk_create_time_durations: string[] | null | undefined;
|
||||
search_after_time_durations: string[] | null | undefined;
|
||||
}
|
||||
|
||||
export type RuleStatusResponse = Record<string, RuleStatus>;
|
||||
|
|
|
@ -4,13 +4,63 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { useRuleStatus, ReturnRuleStatus } from './use_rule_status';
|
||||
import { renderHook, act, cleanup } from '@testing-library/react-hooks';
|
||||
import {
|
||||
useRuleStatus,
|
||||
ReturnRuleStatus,
|
||||
useRulesStatuses,
|
||||
ReturnRulesStatuses,
|
||||
} from './use_rule_status';
|
||||
import * as api from './api';
|
||||
import { RuleType } from '../rules/types';
|
||||
|
||||
jest.mock('./api');
|
||||
|
||||
const testRule = {
|
||||
created_at: 'mm/dd/yyyyTHH:MM:sssz',
|
||||
created_by: 'mockUser',
|
||||
description: 'some desc',
|
||||
enabled: true,
|
||||
false_positives: [],
|
||||
filters: [],
|
||||
from: 'now-360s',
|
||||
id: '12345678987654321',
|
||||
immutable: false,
|
||||
index: [
|
||||
'apm-*-transaction*',
|
||||
'auditbeat-*',
|
||||
'endgame-*',
|
||||
'filebeat-*',
|
||||
'packetbeat-*',
|
||||
'winlogbeat-*',
|
||||
],
|
||||
interval: '5m',
|
||||
language: 'kuery',
|
||||
name: 'Test rule',
|
||||
max_signals: 100,
|
||||
query: "user.email: 'root@elastic.co'",
|
||||
references: [],
|
||||
risk_score: 75,
|
||||
rule_id: 'bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf',
|
||||
severity: 'high',
|
||||
tags: ['APM'],
|
||||
threat: [],
|
||||
to: 'now',
|
||||
type: 'query' as RuleType,
|
||||
updated_at: 'mm/dd/yyyyTHH:MM:sssz',
|
||||
updated_by: 'mockUser',
|
||||
};
|
||||
|
||||
describe('useRuleStatus', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
afterEach(async () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test('init', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, ReturnRuleStatus>(() =>
|
||||
|
@ -39,6 +89,10 @@ describe('useRuleStatus', () => {
|
|||
last_success_message: 'it is a success',
|
||||
status: 'succeeded',
|
||||
status_date: 'mm/dd/yyyyTHH:MM:sssz',
|
||||
gap: null,
|
||||
bulk_create_time_durations: ['2235.01'],
|
||||
search_after_time_durations: ['616.97'],
|
||||
last_look_back_date: '2020-03-19T00:32:07.996Z',
|
||||
},
|
||||
failures: [],
|
||||
},
|
||||
|
@ -62,4 +116,50 @@ describe('useRuleStatus', () => {
|
|||
expect(spyOngetRuleStatusById).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
test('init rules statuses', async () => {
|
||||
const payload = [testRule];
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, ReturnRulesStatuses>(() =>
|
||||
useRulesStatuses(payload)
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({ loading: false, rulesStatuses: [] });
|
||||
});
|
||||
});
|
||||
|
||||
test('fetch rules statuses', async () => {
|
||||
const payload = [testRule];
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, ReturnRulesStatuses>(() =>
|
||||
useRulesStatuses(payload)
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({
|
||||
loading: false,
|
||||
rulesStatuses: [
|
||||
{
|
||||
current_status: {
|
||||
alert_id: 'alertId',
|
||||
bulk_create_time_durations: ['2235.01'],
|
||||
gap: null,
|
||||
last_failure_at: null,
|
||||
last_failure_message: null,
|
||||
last_look_back_date: '2020-03-19T00:32:07.996Z',
|
||||
last_success_at: 'mm/dd/yyyyTHH:MM:sssz',
|
||||
last_success_message: 'it is a success',
|
||||
search_after_time_durations: ['616.97'],
|
||||
status: 'succeeded',
|
||||
status_date: 'mm/dd/yyyyTHH:MM:sssz',
|
||||
},
|
||||
failures: [],
|
||||
id: '12345678987654321',
|
||||
activate: true,
|
||||
name: 'Test rule',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,12 +7,17 @@
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { errorToToaster, useStateToaster } from '../../../components/toasters';
|
||||
import { getRuleStatusById } from './api';
|
||||
import { RuleStatusRowItemType } from '../../../pages/detection_engine/rules/all/columns';
|
||||
import { getRuleStatusById, getRulesStatusByIds } from './api';
|
||||
import * as i18n from './translations';
|
||||
import { RuleStatus } from './types';
|
||||
import { RuleStatus, Rules } from './types';
|
||||
|
||||
type Func = (ruleId: string) => void;
|
||||
export type ReturnRuleStatus = [boolean, RuleStatus | null, Func | null];
|
||||
export interface ReturnRulesStatuses {
|
||||
loading: boolean;
|
||||
rulesStatuses: RuleStatusRowItemType[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for using to get a Rule from the Detection Engine API
|
||||
|
@ -33,7 +38,6 @@ export const useRuleStatus = (id: string | undefined | null): ReturnRuleStatus =
|
|||
const fetchData = async (idToFetch: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const ruleStatusResponse = await getRuleStatusById({
|
||||
id: idToFetch,
|
||||
signal: abortCtrl.signal,
|
||||
|
@ -64,3 +68,58 @@ export const useRuleStatus = (id: string | undefined | null): ReturnRuleStatus =
|
|||
|
||||
return [loading, ruleStatus, fetchRuleStatus.current];
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook for using to get all the statuses for all given rule ids
|
||||
*
|
||||
* @param ids desired Rule ID's (not rule_id)
|
||||
*
|
||||
*/
|
||||
export const useRulesStatuses = (rules: Rules): ReturnRulesStatuses => {
|
||||
const [rulesStatuses, setRuleStatuses] = useState<RuleStatusRowItemType[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
|
||||
useEffect(() => {
|
||||
let isSubscribed = true;
|
||||
const abortCtrl = new AbortController();
|
||||
|
||||
const fetchData = async (ids: string[]) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const ruleStatusesResponse = await getRulesStatusByIds({
|
||||
ids,
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
||||
if (isSubscribed) {
|
||||
setRuleStatuses(
|
||||
rules.map(rule => ({
|
||||
id: rule.id,
|
||||
activate: rule.enabled,
|
||||
name: rule.name,
|
||||
...ruleStatusesResponse[rule.id],
|
||||
}))
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (isSubscribed) {
|
||||
setRuleStatuses([]);
|
||||
errorToToaster({ title: i18n.RULE_FETCH_FAILURE, error, dispatchToaster });
|
||||
}
|
||||
}
|
||||
if (isSubscribed) {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
if (rules != null && rules.length > 0) {
|
||||
fetchData(rules.map(r => r.id));
|
||||
}
|
||||
return () => {
|
||||
isSubscribed = false;
|
||||
abortCtrl.abort();
|
||||
};
|
||||
}, [rules]);
|
||||
|
||||
return { loading, rulesStatuses };
|
||||
};
|
||||
|
|
|
@ -14,10 +14,11 @@ import {
|
|||
EuiText,
|
||||
EuiHealth,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedRelative } from '@kbn/i18n/react';
|
||||
import * as H from 'history';
|
||||
import React, { Dispatch } from 'react';
|
||||
|
||||
import { Rule } from '../../../../containers/detection_engine/rules';
|
||||
import { Rule, RuleStatus } from '../../../../containers/detection_engine/rules';
|
||||
import { getEmptyTagValue } from '../../../../components/empty_value';
|
||||
import { FormattedDate } from '../../../../components/formatted_date';
|
||||
import { getRuleDetailsUrl } from '../../../../components/link_to/redirect_to_detection_engine';
|
||||
|
@ -34,6 +35,7 @@ import {
|
|||
exportRulesAction,
|
||||
} from './actions';
|
||||
import { Action } from './reducer';
|
||||
import { LocalizedDateTooltip } from '../../../../components/localized_date_tooltip';
|
||||
|
||||
export const getActions = (
|
||||
dispatch: React.Dispatch<Action>,
|
||||
|
@ -75,7 +77,12 @@ export const getActions = (
|
|||
},
|
||||
];
|
||||
|
||||
export type RuleStatusRowItemType = RuleStatus & {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
type RulesColumns = EuiBasicTableColumn<Rule> | EuiTableActionsColumnType<Rule>;
|
||||
type RulesStatusesColumns = EuiBasicTableColumn<RuleStatusRowItemType>;
|
||||
|
||||
interface GetColumns {
|
||||
dispatch: React.Dispatch<Action>;
|
||||
|
@ -132,7 +139,9 @@ export const getColumns = ({
|
|||
return value == null ? (
|
||||
getEmptyTagValue()
|
||||
) : (
|
||||
<FormattedDate value={value} fieldName={i18n.COLUMN_LAST_COMPLETE_RUN} />
|
||||
<LocalizedDateTooltip fieldName={i18n.COLUMN_LAST_COMPLETE_RUN} date={new Date(value)}>
|
||||
<FormattedRelative value={value} />
|
||||
</LocalizedDateTooltip>
|
||||
);
|
||||
},
|
||||
sortable: true,
|
||||
|
@ -196,3 +205,114 @@ export const getColumns = ({
|
|||
|
||||
return hasNoPermissions ? cols : [...cols, ...actions];
|
||||
};
|
||||
|
||||
export const getMonitoringColumns = (): RulesStatusesColumns[] => {
|
||||
const cols: RulesStatusesColumns[] = [
|
||||
{
|
||||
field: 'name',
|
||||
name: i18n.COLUMN_RULE,
|
||||
render: (value: RuleStatus['current_status']['status'], item: RuleStatusRowItemType) => {
|
||||
return (
|
||||
<EuiLink data-test-subj="ruleName" href={getRuleDetailsUrl(item.id)}>
|
||||
{value}
|
||||
</EuiLink>
|
||||
);
|
||||
},
|
||||
truncateText: true,
|
||||
width: '24%',
|
||||
},
|
||||
{
|
||||
field: 'current_status.bulk_create_time_durations',
|
||||
name: i18n.COLUMN_INDEXING_TIMES,
|
||||
render: (value: RuleStatus['current_status']['bulk_create_time_durations']) => (
|
||||
<EuiText data-test-subj="bulk_create_time_durations" size="s">
|
||||
{value != null && value.length > 0
|
||||
? Math.max(...value?.map(item => Number.parseFloat(item)))
|
||||
: null}
|
||||
</EuiText>
|
||||
),
|
||||
truncateText: true,
|
||||
width: '14%',
|
||||
},
|
||||
{
|
||||
field: 'current_status.search_after_time_durations',
|
||||
name: i18n.COLUMN_QUERY_TIMES,
|
||||
render: (value: RuleStatus['current_status']['search_after_time_durations']) => (
|
||||
<EuiText data-test-subj="search_after_time_durations" size="s">
|
||||
{value != null && value.length > 0
|
||||
? Math.max(...value?.map(item => Number.parseFloat(item)))
|
||||
: null}
|
||||
</EuiText>
|
||||
),
|
||||
truncateText: true,
|
||||
width: '14%',
|
||||
},
|
||||
{
|
||||
field: 'current_status.gap',
|
||||
name: i18n.COLUMN_GAP,
|
||||
render: (value: RuleStatus['current_status']['gap']) => (
|
||||
<EuiText data-test-subj="gap" size="s">
|
||||
{value}
|
||||
</EuiText>
|
||||
),
|
||||
truncateText: true,
|
||||
width: '14%',
|
||||
},
|
||||
{
|
||||
field: 'current_status.last_look_back_date',
|
||||
name: i18n.COLUMN_LAST_LOOKBACK_DATE,
|
||||
render: (value: RuleStatus['current_status']['last_look_back_date']) => {
|
||||
return value == null ? (
|
||||
getEmptyTagValue()
|
||||
) : (
|
||||
<FormattedDate value={value} fieldName={'last look back date'} />
|
||||
);
|
||||
},
|
||||
truncateText: true,
|
||||
width: '16%',
|
||||
},
|
||||
{
|
||||
field: 'current_status.status_date',
|
||||
name: i18n.COLUMN_LAST_COMPLETE_RUN,
|
||||
render: (value: RuleStatus['current_status']['status_date']) => {
|
||||
return value == null ? (
|
||||
getEmptyTagValue()
|
||||
) : (
|
||||
<LocalizedDateTooltip fieldName={i18n.COLUMN_LAST_COMPLETE_RUN} date={new Date(value)}>
|
||||
<FormattedRelative value={value} />
|
||||
</LocalizedDateTooltip>
|
||||
);
|
||||
},
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
field: 'current_status.status',
|
||||
name: i18n.COLUMN_LAST_RESPONSE,
|
||||
render: (value: RuleStatus['current_status']['status']) => {
|
||||
return (
|
||||
<>
|
||||
<EuiHealth color={getStatusColor(value ?? null)}>
|
||||
{value ?? getEmptyTagValue()}
|
||||
</EuiHealth>
|
||||
</>
|
||||
);
|
||||
},
|
||||
width: '16%',
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
field: 'activate',
|
||||
name: i18n.COLUMN_ACTIVATE,
|
||||
render: (value: Rule['enabled']) => (
|
||||
<EuiText data-test-subj="search_after_time_durations" size="s">
|
||||
{value ? i18n.ACTIVE : i18n.INACTIVE}
|
||||
</EuiText>
|
||||
),
|
||||
width: '95px',
|
||||
},
|
||||
];
|
||||
|
||||
return cols;
|
||||
};
|
||||
|
|
|
@ -4,20 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiContextMenuPanel,
|
||||
EuiEmptyPrompt,
|
||||
EuiLoadingContent,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { EuiBasicTable, EuiContextMenuPanel, EuiLoadingContent, EuiSpacer } from '@elastic/eui';
|
||||
import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import uuid from 'uuid';
|
||||
|
||||
import {
|
||||
useRules,
|
||||
useRulesStatuses,
|
||||
CreatePreBuiltRules,
|
||||
FilterOptions,
|
||||
Rule,
|
||||
|
@ -37,20 +31,16 @@ import { Loader } from '../../../../components/loader';
|
|||
import { Panel } from '../../../../components/panel';
|
||||
import { PrePackagedRulesPrompt } from '../components/pre_packaged_rules/load_empty_prompt';
|
||||
import { GenericDownloader } from '../../../../components/generic_downloader';
|
||||
import { AllRulesTables } from '../components/all_rules_tables';
|
||||
import { getPrePackagedRuleStatus } from '../helpers';
|
||||
import * as i18n from '../translations';
|
||||
import { EuiBasicTableOnChange } from '../types';
|
||||
import { getBatchItems } from './batch_actions';
|
||||
import { getColumns } from './columns';
|
||||
import { getColumns, getMonitoringColumns } from './columns';
|
||||
import { showRulesTable } from './helpers';
|
||||
import { allRulesReducer, State } from './reducer';
|
||||
import { RulesTableFilters } from './rules_table_filters/rules_table_filters';
|
||||
|
||||
// EuiBasicTable give me a hardtime with adding the ref attributes so I went the easy way
|
||||
// after few hours of fight with typescript !!!! I lost :(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const MyEuiBasicTable = styled(EuiBasicTable as any)`` as any;
|
||||
|
||||
const initialState: State = {
|
||||
exportRuleIds: [],
|
||||
filterOptions: {
|
||||
|
@ -117,6 +107,7 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
},
|
||||
dispatch,
|
||||
] = useReducer(allRulesReducer(tableRef), initialState);
|
||||
const { loading: isLoadingRulesStatuses, rulesStatuses } = useRulesStatuses(rules);
|
||||
const history = useHistory();
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
|
||||
|
@ -135,6 +126,13 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
dispatchRulesInReducer: setRules,
|
||||
});
|
||||
|
||||
const sorting = useMemo(
|
||||
() => ({
|
||||
sort: { field: 'enabled', direction: filterOptions.sortOrder },
|
||||
}),
|
||||
[filterOptions.sortOrder]
|
||||
);
|
||||
|
||||
const prePackagedRuleStatus = getPrePackagedRuleStatus(
|
||||
rulesInstalled,
|
||||
rulesNotInstalled,
|
||||
|
@ -158,6 +156,16 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
[dispatch, dispatchToaster, loadingRuleIds, reFetchRulesData, rules, selectedRuleIds]
|
||||
);
|
||||
|
||||
const paginationMemo = useMemo(
|
||||
() => ({
|
||||
pageIndex: pagination.page - 1,
|
||||
pageSize: pagination.perPage,
|
||||
totalItemCount: pagination.total,
|
||||
pageSizeOptions: [5, 10, 20, 50, 100, 200, 300],
|
||||
}),
|
||||
[pagination]
|
||||
);
|
||||
|
||||
const tableOnChangeCallback = useCallback(
|
||||
({ page, sort }: EuiBasicTableOnChange) => {
|
||||
dispatch({
|
||||
|
@ -172,7 +180,7 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
[dispatch]
|
||||
);
|
||||
|
||||
const columns = useMemo(() => {
|
||||
const rulesColumns = useMemo(() => {
|
||||
return getColumns({
|
||||
dispatch,
|
||||
dispatchToaster,
|
||||
|
@ -187,6 +195,8 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
});
|
||||
}, [dispatch, dispatchToaster, history, loadingRuleIds, loadingRulesAction, reFetchRulesData]);
|
||||
|
||||
const monitoringColumns = useMemo(() => getMonitoringColumns(), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (reFetchRulesData != null) {
|
||||
setRefreshRulesData(reFetchRulesData);
|
||||
|
@ -194,10 +204,10 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
}, [reFetchRulesData, setRefreshRulesData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initLoading && !loading && !isLoadingRules) {
|
||||
if (initLoading && !loading && !isLoadingRules && !isLoadingRulesStatuses) {
|
||||
setInitLoading(false);
|
||||
}
|
||||
}, [initLoading, loading, isLoadingRules]);
|
||||
}, [initLoading, loading, isLoadingRules, isLoadingRulesStatuses]);
|
||||
|
||||
const handleCreatePrePackagedRules = useCallback(async () => {
|
||||
if (createPrePackagedRules != null && reFetchRulesData != null) {
|
||||
|
@ -225,12 +235,6 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
});
|
||||
}, []);
|
||||
|
||||
const emptyPrompt = useMemo(() => {
|
||||
return (
|
||||
<EuiEmptyPrompt title={<h3>{i18n.NO_RULES}</h3>} titleSize="xs" body={i18n.NO_RULES_BODY} />
|
||||
);
|
||||
}, []);
|
||||
|
||||
const isLoadingAnActionOnRule = useMemo(() => {
|
||||
if (
|
||||
loadingRuleIds.length > 0 &&
|
||||
|
@ -264,7 +268,7 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
/>
|
||||
<EuiSpacer />
|
||||
|
||||
<Panel loading={loading || isLoadingRules}>
|
||||
<Panel loading={loading || isLoadingRules || isLoadingRulesStatuses}>
|
||||
<>
|
||||
<HeaderSection split title={i18n.ALL_RULES}>
|
||||
<RulesTableFilters
|
||||
|
@ -274,12 +278,14 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
/>
|
||||
</HeaderSection>
|
||||
|
||||
{(loading || isLoadingRules || isLoadingAnActionOnRule) && !initLoading && (
|
||||
<Loader data-test-subj="loadingPanelAllRulesTable" overlay size="xl" />
|
||||
)}
|
||||
{(loading || isLoadingRules || isLoadingAnActionOnRule || isLoadingRulesStatuses) &&
|
||||
!initLoading && (
|
||||
<Loader data-test-subj="loadingPanelAllRulesTable" overlay size="xl" />
|
||||
)}
|
||||
{rulesCustomInstalled != null &&
|
||||
rulesCustomInstalled === 0 &&
|
||||
prePackagedRuleStatus === 'ruleNotInstalled' && (
|
||||
prePackagedRuleStatus === 'ruleNotInstalled' &&
|
||||
!initLoading && (
|
||||
<PrePackagedRulesPrompt
|
||||
createPrePackagedRules={handleCreatePrePackagedRules}
|
||||
loading={loadingCreatePrePackagedRules}
|
||||
|
@ -318,23 +324,17 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
</UtilityBarGroup>
|
||||
</UtilityBarSection>
|
||||
</UtilityBar>
|
||||
<MyEuiBasicTable
|
||||
data-test-subj="rules-table"
|
||||
columns={columns}
|
||||
isSelectable={!hasNoPermissions ?? false}
|
||||
itemId="id"
|
||||
items={rules ?? []}
|
||||
noItemsMessage={emptyPrompt}
|
||||
onChange={tableOnChangeCallback}
|
||||
pagination={{
|
||||
pageIndex: pagination.page - 1,
|
||||
pageSize: pagination.perPage,
|
||||
totalItemCount: pagination.total,
|
||||
pageSizeOptions: [5, 10, 20, 50, 100, 200, 300],
|
||||
}}
|
||||
ref={tableRef}
|
||||
sorting={{ sort: { field: 'enabled', direction: filterOptions.sortOrder } }}
|
||||
selection={hasNoPermissions ? undefined : euiBasicTableSelectionProps}
|
||||
<AllRulesTables
|
||||
euiBasicTableSelectionProps={euiBasicTableSelectionProps}
|
||||
hasNoPermissions={hasNoPermissions}
|
||||
monitoringColumns={monitoringColumns}
|
||||
paginationMemo={paginationMemo}
|
||||
rules={rules}
|
||||
rulesColumns={rulesColumns}
|
||||
rulesStatuses={rulesStatuses}
|
||||
sorting={sorting}
|
||||
tableOnChangeCallback={tableOnChangeCallback}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiBasicTable, EuiTab, EuiTabs, EuiEmptyPrompt } from '@elastic/eui';
|
||||
import React, { useMemo, memo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import * as i18n from '../../translations';
|
||||
import { RuleStatusRowItemType } from '../../../../../pages/detection_engine/rules/all/columns';
|
||||
import { Rules } from '../../../../../containers/detection_engine/rules';
|
||||
|
||||
// EuiBasicTable give me a hardtime with adding the ref attributes so I went the easy way
|
||||
// after few hours of fight with typescript !!!! I lost :(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const MyEuiBasicTable = styled(EuiBasicTable as any)`` as any;
|
||||
|
||||
interface AllRulesTablesProps {
|
||||
euiBasicTableSelectionProps: unknown;
|
||||
hasNoPermissions: boolean;
|
||||
monitoringColumns: unknown;
|
||||
paginationMemo: unknown;
|
||||
rules: Rules;
|
||||
rulesColumns: unknown;
|
||||
rulesStatuses: RuleStatusRowItemType[] | null;
|
||||
sorting: unknown;
|
||||
tableOnChangeCallback: unknown;
|
||||
tableRef?: unknown;
|
||||
}
|
||||
|
||||
enum AllRulesTabs {
|
||||
rules = 'rules',
|
||||
monitoring = 'monitoring',
|
||||
}
|
||||
|
||||
const allRulesTabs = [
|
||||
{
|
||||
id: AllRulesTabs.rules,
|
||||
name: i18n.RULES_TAB,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
id: AllRulesTabs.monitoring,
|
||||
name: i18n.MONITORING_TAB,
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
const AllRulesTablesComponent: React.FC<AllRulesTablesProps> = ({
|
||||
euiBasicTableSelectionProps,
|
||||
hasNoPermissions,
|
||||
monitoringColumns,
|
||||
paginationMemo,
|
||||
rules,
|
||||
rulesColumns,
|
||||
rulesStatuses,
|
||||
sorting,
|
||||
tableOnChangeCallback,
|
||||
tableRef,
|
||||
}) => {
|
||||
const [allRulesTab, setAllRulesTab] = useState(AllRulesTabs.rules);
|
||||
const emptyPrompt = useMemo(() => {
|
||||
return (
|
||||
<EuiEmptyPrompt title={<h3>{i18n.NO_RULES}</h3>} titleSize="xs" body={i18n.NO_RULES_BODY} />
|
||||
);
|
||||
}, []);
|
||||
const tabs = useMemo(
|
||||
() => (
|
||||
<EuiTabs>
|
||||
{allRulesTabs.map(tab => (
|
||||
<EuiTab
|
||||
onClick={() => setAllRulesTab(tab.id)}
|
||||
isSelected={tab.id === allRulesTab}
|
||||
disabled={tab.disabled}
|
||||
key={tab.id}
|
||||
>
|
||||
{tab.name}
|
||||
</EuiTab>
|
||||
))}
|
||||
</EuiTabs>
|
||||
),
|
||||
[allRulesTabs, allRulesTab, setAllRulesTab]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{tabs}
|
||||
{allRulesTab === AllRulesTabs.rules && (
|
||||
<MyEuiBasicTable
|
||||
data-test-subj="rules-table"
|
||||
columns={rulesColumns}
|
||||
isSelectable={!hasNoPermissions ?? false}
|
||||
itemId="id"
|
||||
items={rules ?? []}
|
||||
noItemsMessage={emptyPrompt}
|
||||
onChange={tableOnChangeCallback}
|
||||
pagination={paginationMemo}
|
||||
ref={tableRef}
|
||||
{...sorting}
|
||||
selection={hasNoPermissions ? undefined : euiBasicTableSelectionProps}
|
||||
/>
|
||||
)}
|
||||
{allRulesTab === AllRulesTabs.monitoring && (
|
||||
<MyEuiBasicTable
|
||||
data-test-subj="monitoring-table"
|
||||
columns={monitoringColumns}
|
||||
isSelectable={!hasNoPermissions ?? false}
|
||||
itemId="id"
|
||||
items={rulesStatuses}
|
||||
noItemsMessage={emptyPrompt}
|
||||
onChange={tableOnChangeCallback}
|
||||
pagination={paginationMemo}
|
||||
{...sorting}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const AllRulesTables = memo(AllRulesTablesComponent);
|
|
@ -44,6 +44,20 @@ export const BATCH_ACTIONS = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const ACTIVE = i18n.translate(
|
||||
'xpack.siem.detectionEngine.rules.allRules.activeRuleDescription',
|
||||
{
|
||||
defaultMessage: 'active',
|
||||
}
|
||||
);
|
||||
|
||||
export const INACTIVE = i18n.translate(
|
||||
'xpack.siem.detectionEngine.rules.allRules.inactiveRuleDescription',
|
||||
{
|
||||
defaultMessage: 'inactive',
|
||||
}
|
||||
);
|
||||
|
||||
export const BATCH_ACTION_ACTIVATE_SELECTED = i18n.translate(
|
||||
'xpack.siem.detectionEngine.rules.allRules.batchActions.activateSelectedTitle',
|
||||
{
|
||||
|
@ -255,6 +269,42 @@ export const COLUMN_ACTIVATE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const COLUMN_INDEXING_TIMES = i18n.translate(
|
||||
'xpack.siem.detectionEngine.rules.allRules.columns.indexingTimes',
|
||||
{
|
||||
defaultMessage: 'Indexing Time (ms)',
|
||||
}
|
||||
);
|
||||
|
||||
export const COLUMN_QUERY_TIMES = i18n.translate(
|
||||
'xpack.siem.detectionEngine.rules.allRules.columns.queryTimes',
|
||||
{
|
||||
defaultMessage: 'Query Time (ms)',
|
||||
}
|
||||
);
|
||||
|
||||
export const COLUMN_GAP = i18n.translate('xpack.siem.detectionEngine.rules.allRules.columns.gap', {
|
||||
defaultMessage: 'Gap (if any)',
|
||||
});
|
||||
|
||||
export const COLUMN_LAST_LOOKBACK_DATE = i18n.translate(
|
||||
'xpack.siem.detectionEngine.rules.allRules.columns.lastLookBackDate',
|
||||
{
|
||||
defaultMessage: 'Last Look-Back Date',
|
||||
}
|
||||
);
|
||||
|
||||
export const RULES_TAB = i18n.translate('xpack.siem.detectionEngine.rules.allRules.tabs.rules', {
|
||||
defaultMessage: 'Rules',
|
||||
});
|
||||
|
||||
export const MONITORING_TAB = i18n.translate(
|
||||
'xpack.siem.detectionEngine.rules.allRules.tabs.monitoring',
|
||||
{
|
||||
defaultMessage: 'Monitoring',
|
||||
}
|
||||
);
|
||||
|
||||
export const CUSTOM_RULES = i18n.translate(
|
||||
'xpack.siem.detectionEngine.rules.allRules.filters.customRulesTitle',
|
||||
{
|
||||
|
|
|
@ -592,6 +592,10 @@ export const getFindResultStatus = (): SavedObjectsFindResponse<IRuleSavedAttrib
|
|||
lastSuccessAt: '2020-02-18T15:26:49.783Z',
|
||||
lastFailureMessage: null,
|
||||
lastSuccessMessage: 'succeeded',
|
||||
lastLookBackDate: new Date('2020-02-18T15:14:58.806Z').toISOString(),
|
||||
gap: '500.32',
|
||||
searchAfterTimeDurations: ['200.00'],
|
||||
bulkCreateTimeDurations: ['800.43'],
|
||||
},
|
||||
references: [],
|
||||
updated_at: '2020-02-18T15:26:51.333Z',
|
||||
|
@ -609,6 +613,10 @@ export const getFindResultStatus = (): SavedObjectsFindResponse<IRuleSavedAttrib
|
|||
lastFailureMessage:
|
||||
'Signal rule name: "Query with a rule id Number 1", id: "1ea5a820-4da1-4e82-92a1-2b43a7bece08", rule_id: "query-rule-id-1" has a time gap of 5 days (412682928ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.',
|
||||
lastSuccessMessage: 'succeeded',
|
||||
lastLookBackDate: new Date('2020-02-18T15:14:58.806Z').toISOString(),
|
||||
gap: '500.32',
|
||||
searchAfterTimeDurations: ['200.00'],
|
||||
bulkCreateTimeDurations: ['800.43'],
|
||||
},
|
||||
references: [],
|
||||
updated_at: '2020-02-18T15:15:58.860Z',
|
||||
|
|
|
@ -4,14 +4,9 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ElasticsearchMappingOf } from '../../../utils/typed_elasticsearch_mappings';
|
||||
import { IRuleStatusAttributes } from './types';
|
||||
|
||||
export const ruleStatusSavedObjectType = 'siem-detection-engine-rule-status';
|
||||
|
||||
export const ruleStatusSavedObjectMappings: {
|
||||
[ruleStatusSavedObjectType]: ElasticsearchMappingOf<IRuleStatusAttributes>;
|
||||
} = {
|
||||
export const ruleStatusSavedObjectMappings = {
|
||||
[ruleStatusSavedObjectType]: {
|
||||
properties: {
|
||||
alertId: {
|
||||
|
@ -35,6 +30,18 @@ export const ruleStatusSavedObjectMappings: {
|
|||
lastSuccessMessage: {
|
||||
type: 'text',
|
||||
},
|
||||
lastLookBackDate: {
|
||||
type: 'date',
|
||||
},
|
||||
gap: {
|
||||
type: 'text',
|
||||
},
|
||||
bulkCreateTimeDurations: {
|
||||
type: 'float',
|
||||
},
|
||||
searchAfterTimeDurations: {
|
||||
type: 'float',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -60,6 +60,10 @@ export interface IRuleStatusAttributes extends Record<string, any> {
|
|||
lastSuccessAt: string | null | undefined;
|
||||
lastSuccessMessage: string | null | undefined;
|
||||
status: RuleStatusString | null | undefined;
|
||||
lastLookBackDate: string | null | undefined;
|
||||
gap: string | null | undefined;
|
||||
bulkCreateTimeDurations: string[] | null | undefined;
|
||||
searchAfterTimeDurations: string[] | null | undefined;
|
||||
}
|
||||
|
||||
export interface RuleStatusResponse {
|
||||
|
|
|
@ -11,7 +11,7 @@ import { Logger } from '../../../../../../../../src/core/server';
|
|||
import { AlertServices } from '../../../../../../../plugins/alerting/server';
|
||||
import { RuleAlertAction } from '../../../../common/detection_engine/types';
|
||||
import { RuleTypeParams } from '../types';
|
||||
import { singleBulkCreate } from './single_bulk_create';
|
||||
import { singleBulkCreate, SingleBulkCreateResponse } from './single_bulk_create';
|
||||
import { AnomalyResults, Anomaly } from '../../machine_learning';
|
||||
|
||||
interface BulkCreateMlSignalsParams {
|
||||
|
@ -75,7 +75,9 @@ const transformAnomalyResultsToEcs = (results: AnomalyResults): SearchResponse<E
|
|||
};
|
||||
};
|
||||
|
||||
export const bulkCreateMlSignals = async (params: BulkCreateMlSignalsParams) => {
|
||||
export const bulkCreateMlSignals = async (
|
||||
params: BulkCreateMlSignalsParams
|
||||
): Promise<SingleBulkCreateResponse> => {
|
||||
const anomalyResults = params.someResult;
|
||||
const ecsResults = transformAnomalyResultsToEcs(anomalyResults);
|
||||
|
||||
|
|
|
@ -36,6 +36,10 @@ export const getCurrentStatusSavedObject = async ({
|
|||
lastSuccessAt: null,
|
||||
lastFailureMessage: null,
|
||||
lastSuccessMessage: null,
|
||||
gap: null,
|
||||
bulkCreateTimeDurations: [],
|
||||
searchAfterTimeDurations: [],
|
||||
lastLookBackDate: null,
|
||||
});
|
||||
return currentStatusSavedObject;
|
||||
} else {
|
||||
|
|
|
@ -34,7 +34,7 @@ describe('searchAfterAndBulkCreate', () => {
|
|||
|
||||
test('if successful with empty search results', async () => {
|
||||
const sampleParams = sampleRuleAlertParams();
|
||||
const result = await searchAfterAndBulkCreate({
|
||||
const { success } = await searchAfterAndBulkCreate({
|
||||
someResult: sampleEmptyDocSearchResults(),
|
||||
ruleParams: sampleParams,
|
||||
services: mockService,
|
||||
|
@ -56,7 +56,7 @@ describe('searchAfterAndBulkCreate', () => {
|
|||
throttle: null,
|
||||
});
|
||||
expect(mockService.callCluster).toHaveBeenCalledTimes(0);
|
||||
expect(result).toEqual(true);
|
||||
expect(success).toEqual(true);
|
||||
});
|
||||
|
||||
test('if successful iteration of while loop with maxDocs', async () => {
|
||||
|
@ -92,7 +92,7 @@ describe('searchAfterAndBulkCreate', () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
const result = await searchAfterAndBulkCreate({
|
||||
const { success } = await searchAfterAndBulkCreate({
|
||||
someResult: repeatedSearchResultsWithSortId(3, 1, someGuids.slice(6, 9)),
|
||||
ruleParams: sampleParams,
|
||||
services: mockService,
|
||||
|
@ -114,14 +114,14 @@ describe('searchAfterAndBulkCreate', () => {
|
|||
throttle: null,
|
||||
});
|
||||
expect(mockService.callCluster).toHaveBeenCalledTimes(5);
|
||||
expect(result).toEqual(true);
|
||||
expect(success).toEqual(true);
|
||||
});
|
||||
|
||||
test('if unsuccessful first bulk create', async () => {
|
||||
const someGuids = Array.from({ length: 4 }).map(x => uuid.v4());
|
||||
const sampleParams = sampleRuleAlertParams(10);
|
||||
mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult);
|
||||
const result = await searchAfterAndBulkCreate({
|
||||
const { success } = await searchAfterAndBulkCreate({
|
||||
someResult: repeatedSearchResultsWithSortId(4, 1, someGuids),
|
||||
ruleParams: sampleParams,
|
||||
services: mockService,
|
||||
|
@ -143,7 +143,7 @@ describe('searchAfterAndBulkCreate', () => {
|
|||
throttle: null,
|
||||
});
|
||||
expect(mockLogger.error).toHaveBeenCalled();
|
||||
expect(result).toEqual(false);
|
||||
expect(success).toEqual(false);
|
||||
});
|
||||
|
||||
test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids', async () => {
|
||||
|
@ -157,7 +157,7 @@ describe('searchAfterAndBulkCreate', () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
const result = await searchAfterAndBulkCreate({
|
||||
const { success } = await searchAfterAndBulkCreate({
|
||||
someResult: sampleDocSearchResultsNoSortId(),
|
||||
ruleParams: sampleParams,
|
||||
services: mockService,
|
||||
|
@ -179,7 +179,7 @@ describe('searchAfterAndBulkCreate', () => {
|
|||
throttle: null,
|
||||
});
|
||||
expect(mockLogger.error).toHaveBeenCalled();
|
||||
expect(result).toEqual(false);
|
||||
expect(success).toEqual(false);
|
||||
});
|
||||
|
||||
test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids and 0 total hits', async () => {
|
||||
|
@ -193,7 +193,7 @@ describe('searchAfterAndBulkCreate', () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
const result = await searchAfterAndBulkCreate({
|
||||
const { success } = await searchAfterAndBulkCreate({
|
||||
someResult: sampleDocSearchResultsNoSortIdNoHits(),
|
||||
ruleParams: sampleParams,
|
||||
services: mockService,
|
||||
|
@ -214,7 +214,7 @@ describe('searchAfterAndBulkCreate', () => {
|
|||
tags: ['some fake tag 1', 'some fake tag 2'],
|
||||
throttle: null,
|
||||
});
|
||||
expect(result).toEqual(true);
|
||||
expect(success).toEqual(true);
|
||||
});
|
||||
|
||||
test('if successful iteration of while loop with maxDocs and search after returns results with no sort ids', async () => {
|
||||
|
@ -231,7 +231,7 @@ describe('searchAfterAndBulkCreate', () => {
|
|||
],
|
||||
})
|
||||
.mockReturnValueOnce(sampleDocSearchResultsNoSortId());
|
||||
const result = await searchAfterAndBulkCreate({
|
||||
const { success } = await searchAfterAndBulkCreate({
|
||||
someResult: repeatedSearchResultsWithSortId(4, 1, someGuids),
|
||||
ruleParams: sampleParams,
|
||||
services: mockService,
|
||||
|
@ -252,7 +252,7 @@ describe('searchAfterAndBulkCreate', () => {
|
|||
tags: ['some fake tag 1', 'some fake tag 2'],
|
||||
throttle: null,
|
||||
});
|
||||
expect(result).toEqual(true);
|
||||
expect(success).toEqual(true);
|
||||
});
|
||||
|
||||
test('if successful iteration of while loop with maxDocs and search after returns empty results with no sort ids', async () => {
|
||||
|
@ -269,7 +269,7 @@ describe('searchAfterAndBulkCreate', () => {
|
|||
],
|
||||
})
|
||||
.mockReturnValueOnce(sampleEmptyDocSearchResults());
|
||||
const result = await searchAfterAndBulkCreate({
|
||||
const { success } = await searchAfterAndBulkCreate({
|
||||
someResult: repeatedSearchResultsWithSortId(4, 1, someGuids),
|
||||
ruleParams: sampleParams,
|
||||
services: mockService,
|
||||
|
@ -290,7 +290,7 @@ describe('searchAfterAndBulkCreate', () => {
|
|||
tags: ['some fake tag 1', 'some fake tag 2'],
|
||||
throttle: null,
|
||||
});
|
||||
expect(result).toEqual(true);
|
||||
expect(success).toEqual(true);
|
||||
});
|
||||
|
||||
test('if returns false when singleSearchAfter throws an exception', async () => {
|
||||
|
@ -309,7 +309,7 @@ describe('searchAfterAndBulkCreate', () => {
|
|||
.mockImplementation(() => {
|
||||
throw Error('Fake Error');
|
||||
});
|
||||
const result = await searchAfterAndBulkCreate({
|
||||
const { success } = await searchAfterAndBulkCreate({
|
||||
someResult: repeatedSearchResultsWithSortId(4, 1, someGuids),
|
||||
ruleParams: sampleParams,
|
||||
services: mockService,
|
||||
|
@ -330,6 +330,6 @@ describe('searchAfterAndBulkCreate', () => {
|
|||
tags: ['some fake tag 1', 'some fake tag 2'],
|
||||
throttle: null,
|
||||
});
|
||||
expect(result).toEqual(false);
|
||||
expect(success).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -34,6 +34,13 @@ interface SearchAfterAndBulkCreateParams {
|
|||
throttle: string | null;
|
||||
}
|
||||
|
||||
export interface SearchAfterAndBulkCreateReturnType {
|
||||
success: boolean;
|
||||
searchAfterTimes: string[];
|
||||
bulkCreateTimes: string[];
|
||||
lastLookBackDate: Date | null | undefined;
|
||||
}
|
||||
|
||||
// search_after through documents and re-index using bulk endpoint.
|
||||
export const searchAfterAndBulkCreate = async ({
|
||||
someResult,
|
||||
|
@ -55,13 +62,20 @@ export const searchAfterAndBulkCreate = async ({
|
|||
pageSize,
|
||||
tags,
|
||||
throttle,
|
||||
}: SearchAfterAndBulkCreateParams): Promise<boolean> => {
|
||||
}: SearchAfterAndBulkCreateParams): Promise<SearchAfterAndBulkCreateReturnType> => {
|
||||
const toReturn: SearchAfterAndBulkCreateReturnType = {
|
||||
success: false,
|
||||
searchAfterTimes: [],
|
||||
bulkCreateTimes: [],
|
||||
lastLookBackDate: null,
|
||||
};
|
||||
if (someResult.hits.hits.length === 0) {
|
||||
return true;
|
||||
toReturn.success = true;
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
logger.debug('[+] starting bulk insertion');
|
||||
await singleBulkCreate({
|
||||
const { bulkCreateDuration } = await singleBulkCreate({
|
||||
someResult,
|
||||
ruleParams,
|
||||
services,
|
||||
|
@ -79,6 +93,13 @@ export const searchAfterAndBulkCreate = async ({
|
|||
tags,
|
||||
throttle,
|
||||
});
|
||||
toReturn.lastLookBackDate =
|
||||
someResult.hits.hits.length > 0
|
||||
? new Date(someResult.hits.hits[someResult.hits.hits.length - 1]?._source['@timestamp'])
|
||||
: null;
|
||||
if (bulkCreateDuration) {
|
||||
toReturn.bulkCreateTimes.push(bulkCreateDuration);
|
||||
}
|
||||
const totalHits =
|
||||
typeof someResult.hits.total === 'number' ? someResult.hits.total : someResult.hits.total.value;
|
||||
// maxTotalHitsSize represents the total number of docs to
|
||||
|
@ -94,9 +115,11 @@ export const searchAfterAndBulkCreate = async ({
|
|||
let sortIds = someResult.hits.hits[0].sort;
|
||||
if (sortIds == null && totalHits > 0) {
|
||||
logger.error('sortIds was empty on first search but expected more');
|
||||
return false;
|
||||
toReturn.success = false;
|
||||
return toReturn;
|
||||
} else if (sortIds == null && totalHits === 0) {
|
||||
return true;
|
||||
toReturn.success = true;
|
||||
return toReturn;
|
||||
}
|
||||
let sortId;
|
||||
if (sortIds != null) {
|
||||
|
@ -105,7 +128,10 @@ export const searchAfterAndBulkCreate = async ({
|
|||
while (hitsSize < maxTotalHitsSize && hitsSize !== 0) {
|
||||
try {
|
||||
logger.debug(`sortIds: ${sortIds}`);
|
||||
const searchAfterResult: SignalSearchResponse = await singleSearchAfter({
|
||||
const {
|
||||
searchResult,
|
||||
searchDuration,
|
||||
}: { searchResult: SignalSearchResponse; searchDuration: string } = await singleSearchAfter({
|
||||
searchAfterSortId: sortId,
|
||||
index: inputIndexPattern,
|
||||
from: ruleParams.from,
|
||||
|
@ -115,20 +141,23 @@ export const searchAfterAndBulkCreate = async ({
|
|||
filter,
|
||||
pageSize, // maximum number of docs to receive per search result.
|
||||
});
|
||||
if (searchAfterResult.hits.hits.length === 0) {
|
||||
return true;
|
||||
toReturn.searchAfterTimes.push(searchDuration);
|
||||
if (searchResult.hits.hits.length === 0) {
|
||||
toReturn.success = true;
|
||||
return toReturn;
|
||||
}
|
||||
hitsSize += searchAfterResult.hits.hits.length;
|
||||
hitsSize += searchResult.hits.hits.length;
|
||||
logger.debug(`size adjusted: ${hitsSize}`);
|
||||
sortIds = searchAfterResult.hits.hits[0].sort;
|
||||
sortIds = searchResult.hits.hits[0].sort;
|
||||
if (sortIds == null) {
|
||||
logger.debug('sortIds was empty on search');
|
||||
return true; // no more search results
|
||||
toReturn.success = true;
|
||||
return toReturn; // no more search results
|
||||
}
|
||||
sortId = sortIds[0];
|
||||
logger.debug('next bulk index');
|
||||
await singleBulkCreate({
|
||||
someResult: searchAfterResult,
|
||||
const { bulkCreateDuration: bulkDuration } = await singleBulkCreate({
|
||||
someResult: searchResult,
|
||||
ruleParams,
|
||||
services,
|
||||
logger,
|
||||
|
@ -146,11 +175,16 @@ export const searchAfterAndBulkCreate = async ({
|
|||
throttle,
|
||||
});
|
||||
logger.debug('finished next bulk index');
|
||||
if (bulkDuration) {
|
||||
toReturn.bulkCreateTimes.push(bulkDuration);
|
||||
}
|
||||
} catch (exc) {
|
||||
logger.error(`[-] search_after and bulk threw an error ${exc}`);
|
||||
return false;
|
||||
toReturn.success = false;
|
||||
return toReturn;
|
||||
}
|
||||
}
|
||||
logger.debug(`[+] completed bulk index of ${maxTotalHitsSize}`);
|
||||
return true;
|
||||
toReturn.success = true;
|
||||
return toReturn;
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { performance } from 'perf_hooks';
|
||||
import { Logger } from 'src/core/server';
|
||||
import {
|
||||
SIGNALS_ID,
|
||||
|
@ -13,10 +14,13 @@ import {
|
|||
|
||||
import { buildEventsSearchQuery } from './build_events_query';
|
||||
import { getInputIndex } from './get_input_output_index';
|
||||
import { searchAfterAndBulkCreate } from './search_after_bulk_create';
|
||||
import {
|
||||
searchAfterAndBulkCreate,
|
||||
SearchAfterAndBulkCreateReturnType,
|
||||
} from './search_after_bulk_create';
|
||||
import { getFilter } from './get_filter';
|
||||
import { SignalRuleAlertTypeDefinition, RuleAlertAttributes } from './types';
|
||||
import { getGapBetweenRuns } from './utils';
|
||||
import { getGapBetweenRuns, makeFloatString } from './utils';
|
||||
import { writeSignalRuleExceptionToSavedObject } from './write_signal_rule_exception_to_saved_object';
|
||||
import { signalParamsSchema } from './signal_params_schema';
|
||||
import { siemRuleActionGroups } from './siem_rule_action_groups';
|
||||
|
@ -92,7 +96,6 @@ export const signalRulesAlertType = ({
|
|||
const updatedAt = savedObject.updated_at ?? '';
|
||||
|
||||
const gap = getGapBetweenRuns({ previousStartedAt, interval, from, to });
|
||||
|
||||
await writeGapErrorToSavedObject({
|
||||
alertId,
|
||||
logger,
|
||||
|
@ -105,7 +108,12 @@ export const signalRulesAlertType = ({
|
|||
});
|
||||
|
||||
const searchAfterSize = Math.min(params.maxSignals, DEFAULT_SEARCH_AFTER_PAGE_SIZE);
|
||||
let creationSucceeded = false;
|
||||
let creationSucceeded: SearchAfterAndBulkCreateReturnType = {
|
||||
success: false,
|
||||
bulkCreateTimes: [],
|
||||
searchAfterTimes: [],
|
||||
lastLookBackDate: null,
|
||||
};
|
||||
|
||||
try {
|
||||
if (type === 'machine_learning') {
|
||||
|
@ -130,7 +138,7 @@ export const signalRulesAlertType = ({
|
|||
);
|
||||
}
|
||||
|
||||
creationSucceeded = await bulkCreateMlSignals({
|
||||
const { success, bulkCreateDuration } = await bulkCreateMlSignals({
|
||||
actions,
|
||||
throttle,
|
||||
someResult: anomalyResults,
|
||||
|
@ -148,6 +156,10 @@ export const signalRulesAlertType = ({
|
|||
enabled,
|
||||
tags,
|
||||
});
|
||||
creationSucceeded.success = success;
|
||||
if (bulkCreateDuration) {
|
||||
creationSucceeded.bulkCreateTimes.push(bulkCreateDuration);
|
||||
}
|
||||
} else {
|
||||
const inputIndex = await getInputIndex(services, version, index);
|
||||
const esFilter = await getFilter({
|
||||
|
@ -175,7 +187,10 @@ export const signalRulesAlertType = ({
|
|||
logger.debug(
|
||||
`[+] Initial search call of signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}"`
|
||||
);
|
||||
const start = performance.now();
|
||||
const noReIndexResult = await services.callCluster('search', noReIndex);
|
||||
const end = performance.now();
|
||||
|
||||
if (noReIndexResult.hits.total.value !== 0) {
|
||||
logger.info(
|
||||
`Found ${
|
||||
|
@ -207,9 +222,10 @@ export const signalRulesAlertType = ({
|
|||
tags,
|
||||
throttle,
|
||||
});
|
||||
creationSucceeded.searchAfterTimes.push(makeFloatString(end - start));
|
||||
}
|
||||
|
||||
if (creationSucceeded) {
|
||||
if (creationSucceeded.success) {
|
||||
if (meta?.throttle === NOTIFICATION_THROTTLE_RULE && actions.length) {
|
||||
const notificationRuleParams = {
|
||||
...ruleParams,
|
||||
|
@ -242,11 +258,14 @@ export const signalRulesAlertType = ({
|
|||
}
|
||||
|
||||
logger.debug(
|
||||
`Finished signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}", output_index: "${outputIndex}"`
|
||||
`Finished signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}"`
|
||||
);
|
||||
await writeCurrentStatusSucceeded({
|
||||
services,
|
||||
currentStatusSavedObject,
|
||||
bulkCreateTimes: creationSucceeded.bulkCreateTimes,
|
||||
searchAfterTimes: creationSucceeded.searchAfterTimes,
|
||||
lastLookBackDate: creationSucceeded.lastLookBackDate?.toISOString() ?? null,
|
||||
});
|
||||
} else {
|
||||
await writeSignalRuleExceptionToSavedObject({
|
||||
|
@ -254,22 +273,28 @@ export const signalRulesAlertType = ({
|
|||
alertId,
|
||||
currentStatusSavedObject,
|
||||
logger,
|
||||
message: `Bulk Indexing signals failed. Check logs for further details Rule name: "${name}" id: "${alertId}" rule_id: "${ruleId}" output_index: "${outputIndex}"`,
|
||||
message: `Bulk Indexing signals failed. Check logs for further details \nRule name: "${name}"\nid: "${alertId}"\nrule_id: "${ruleId}"\n`,
|
||||
services,
|
||||
ruleStatusSavedObjects,
|
||||
ruleId: ruleId ?? '(unknown rule id)',
|
||||
bulkCreateTimes: creationSucceeded.bulkCreateTimes,
|
||||
searchAfterTimes: creationSucceeded.searchAfterTimes,
|
||||
lastLookBackDate: creationSucceeded.lastLookBackDate?.toISOString() ?? null,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (err) {
|
||||
await writeSignalRuleExceptionToSavedObject({
|
||||
name,
|
||||
alertId,
|
||||
currentStatusSavedObject,
|
||||
logger,
|
||||
message: error?.message ?? '(no error message given)',
|
||||
message: `Bulk Indexing signals failed. Check logs for further details \nRule name: "${name}"\nid: "${alertId}"\nrule_id: "${ruleId}"\n`,
|
||||
services,
|
||||
ruleStatusSavedObjects,
|
||||
ruleId: ruleId ?? '(unknown rule id)',
|
||||
bulkCreateTimes: creationSucceeded.bulkCreateTimes,
|
||||
searchAfterTimes: creationSucceeded.searchAfterTimes,
|
||||
lastLookBackDate: creationSucceeded.lastLookBackDate?.toISOString() ?? null,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
@ -144,7 +144,7 @@ describe('singleBulkCreate', () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
const successfulsingleBulkCreate = await singleBulkCreate({
|
||||
const { success } = await singleBulkCreate({
|
||||
someResult: sampleDocSearchResultsNoSortId(),
|
||||
ruleParams: sampleParams,
|
||||
services: mockService,
|
||||
|
@ -162,7 +162,7 @@ describe('singleBulkCreate', () => {
|
|||
tags: ['some fake tag 1', 'some fake tag 2'],
|
||||
throttle: null,
|
||||
});
|
||||
expect(successfulsingleBulkCreate).toEqual(true);
|
||||
expect(success).toEqual(true);
|
||||
});
|
||||
|
||||
test('create successful bulk create with docs with no versioning', async () => {
|
||||
|
@ -176,7 +176,7 @@ describe('singleBulkCreate', () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
const successfulsingleBulkCreate = await singleBulkCreate({
|
||||
const { success } = await singleBulkCreate({
|
||||
someResult: sampleDocSearchResultsNoSortIdNoVersion(),
|
||||
ruleParams: sampleParams,
|
||||
services: mockService,
|
||||
|
@ -194,13 +194,13 @@ describe('singleBulkCreate', () => {
|
|||
tags: ['some fake tag 1', 'some fake tag 2'],
|
||||
throttle: null,
|
||||
});
|
||||
expect(successfulsingleBulkCreate).toEqual(true);
|
||||
expect(success).toEqual(true);
|
||||
});
|
||||
|
||||
test('create unsuccessful bulk create due to empty search results', async () => {
|
||||
const sampleParams = sampleRuleAlertParams();
|
||||
mockService.callCluster.mockReturnValue(false);
|
||||
const successfulsingleBulkCreate = await singleBulkCreate({
|
||||
const { success } = await singleBulkCreate({
|
||||
someResult: sampleEmptyDocSearchResults(),
|
||||
ruleParams: sampleParams,
|
||||
services: mockService,
|
||||
|
@ -218,14 +218,14 @@ describe('singleBulkCreate', () => {
|
|||
tags: ['some fake tag 1', 'some fake tag 2'],
|
||||
throttle: null,
|
||||
});
|
||||
expect(successfulsingleBulkCreate).toEqual(true);
|
||||
expect(success).toEqual(true);
|
||||
});
|
||||
|
||||
test('create successful bulk create when bulk create has duplicate errors', async () => {
|
||||
const sampleParams = sampleRuleAlertParams();
|
||||
const sampleSearchResult = sampleDocSearchResultsNoSortId;
|
||||
mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult);
|
||||
const successfulsingleBulkCreate = await singleBulkCreate({
|
||||
const { success } = await singleBulkCreate({
|
||||
someResult: sampleSearchResult(),
|
||||
ruleParams: sampleParams,
|
||||
services: mockService,
|
||||
|
@ -245,14 +245,14 @@ describe('singleBulkCreate', () => {
|
|||
});
|
||||
|
||||
expect(mockLogger.error).not.toHaveBeenCalled();
|
||||
expect(successfulsingleBulkCreate).toEqual(true);
|
||||
expect(success).toEqual(true);
|
||||
});
|
||||
|
||||
test('create successful bulk create when bulk create has multiple error statuses', async () => {
|
||||
const sampleParams = sampleRuleAlertParams();
|
||||
const sampleSearchResult = sampleDocSearchResultsNoSortId;
|
||||
mockService.callCluster.mockReturnValue(sampleBulkCreateErrorResult);
|
||||
const successfulsingleBulkCreate = await singleBulkCreate({
|
||||
const { success } = await singleBulkCreate({
|
||||
someResult: sampleSearchResult(),
|
||||
ruleParams: sampleParams,
|
||||
services: mockService,
|
||||
|
@ -272,7 +272,7 @@ describe('singleBulkCreate', () => {
|
|||
});
|
||||
|
||||
expect(mockLogger.error).toHaveBeenCalled();
|
||||
expect(successfulsingleBulkCreate).toEqual(true);
|
||||
expect(success).toEqual(true);
|
||||
});
|
||||
|
||||
test('filter duplicate rules will return an empty array given an empty array', () => {
|
||||
|
|
|
@ -10,7 +10,7 @@ import { AlertServices } from '../../../../../../../plugins/alerting/server';
|
|||
import { SignalSearchResponse, BulkResponse } from './types';
|
||||
import { RuleAlertAction } from '../../../../common/detection_engine/types';
|
||||
import { RuleTypeParams } from '../types';
|
||||
import { generateId } from './utils';
|
||||
import { generateId, makeFloatString } from './utils';
|
||||
import { buildBulkBody } from './build_bulk_body';
|
||||
import { Logger } from '../../../../../../../../src/core/server';
|
||||
|
||||
|
@ -55,6 +55,11 @@ export const filterDuplicateRules = (
|
|||
});
|
||||
};
|
||||
|
||||
export interface SingleBulkCreateResponse {
|
||||
success: boolean;
|
||||
bulkCreateDuration?: string;
|
||||
}
|
||||
|
||||
// Bulk Index documents.
|
||||
export const singleBulkCreate = async ({
|
||||
someResult,
|
||||
|
@ -73,11 +78,10 @@ export const singleBulkCreate = async ({
|
|||
enabled,
|
||||
tags,
|
||||
throttle,
|
||||
}: SingleBulkCreateParams): Promise<boolean> => {
|
||||
}: SingleBulkCreateParams): Promise<SingleBulkCreateResponse> => {
|
||||
someResult.hits.hits = filterDuplicateRules(id, someResult);
|
||||
|
||||
if (someResult.hits.hits.length === 0) {
|
||||
return true;
|
||||
return { success: true };
|
||||
}
|
||||
// index documents after creating an ID based on the
|
||||
// source documents' originating index, and the original
|
||||
|
@ -123,7 +127,7 @@ export const singleBulkCreate = async ({
|
|||
body: bulkBody,
|
||||
});
|
||||
const end = performance.now();
|
||||
logger.debug(`individual bulk process time took: ${Number(end - start).toFixed(2)} milliseconds`);
|
||||
logger.debug(`individual bulk process time took: ${makeFloatString(end - start)} milliseconds`);
|
||||
logger.debug(`took property says bulk took: ${response.took} milliseconds`);
|
||||
|
||||
if (response.errors) {
|
||||
|
@ -141,5 +145,5 @@ export const singleBulkCreate = async ({
|
|||
);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return { success: true, bulkCreateDuration: makeFloatString(end - start) };
|
||||
};
|
||||
|
|
|
@ -42,7 +42,7 @@ describe('singleSearchAfter', () => {
|
|||
test('if singleSearchAfter works with a given sort id', async () => {
|
||||
const searchAfterSortId = '1234567891111';
|
||||
mockService.callCluster.mockReturnValue(sampleDocSearchResultsWithSortId);
|
||||
const searchAfterResult = await singleSearchAfter({
|
||||
const { searchResult } = await singleSearchAfter({
|
||||
searchAfterSortId,
|
||||
index: [],
|
||||
from: 'now-360s',
|
||||
|
@ -52,7 +52,7 @@ describe('singleSearchAfter', () => {
|
|||
pageSize: 1,
|
||||
filter: undefined,
|
||||
});
|
||||
expect(searchAfterResult).toEqual(sampleDocSearchResultsWithSortId);
|
||||
expect(searchResult).toEqual(sampleDocSearchResultsWithSortId);
|
||||
});
|
||||
test('if singleSearchAfter throws error', async () => {
|
||||
const searchAfterSortId = '1234567891111';
|
||||
|
|
|
@ -4,10 +4,12 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { performance } from 'perf_hooks';
|
||||
import { AlertServices } from '../../../../../../../plugins/alerting/server';
|
||||
import { Logger } from '../../../../../../../../src/core/server';
|
||||
import { SignalSearchResponse } from './types';
|
||||
import { buildEventsSearchQuery } from './build_events_query';
|
||||
import { makeFloatString } from './utils';
|
||||
|
||||
interface SingleSearchAfterParams {
|
||||
searchAfterSortId: string | undefined;
|
||||
|
@ -30,7 +32,10 @@ export const singleSearchAfter = async ({
|
|||
filter,
|
||||
logger,
|
||||
pageSize,
|
||||
}: SingleSearchAfterParams): Promise<SignalSearchResponse> => {
|
||||
}: SingleSearchAfterParams): Promise<{
|
||||
searchResult: SignalSearchResponse;
|
||||
searchDuration: string;
|
||||
}> => {
|
||||
if (searchAfterSortId == null) {
|
||||
throw Error('Attempted to search after with empty sort id');
|
||||
}
|
||||
|
@ -43,11 +48,13 @@ export const singleSearchAfter = async ({
|
|||
size: pageSize,
|
||||
searchAfterSortId,
|
||||
});
|
||||
const start = performance.now();
|
||||
const nextSearchAfterResult: SignalSearchResponse = await services.callCluster(
|
||||
'search',
|
||||
searchAfterQuery
|
||||
);
|
||||
return nextSearchAfterResult;
|
||||
const end = performance.now();
|
||||
return { searchResult: nextSearchAfterResult, searchDuration: makeFloatString(end - start) };
|
||||
} catch (exc) {
|
||||
logger.error(`[-] nextSearchAfter threw an error ${exc}`);
|
||||
throw exc;
|
||||
|
|
|
@ -89,3 +89,5 @@ export const getGapBetweenRuns = ({
|
|||
const drift = diff.subtract(intervalDuration);
|
||||
return drift.subtract(driftTolerance);
|
||||
};
|
||||
|
||||
export const makeFloatString = (num: number): string => Number(num).toFixed(2);
|
||||
|
|
|
@ -13,17 +13,32 @@ import { IRuleSavedAttributesSavedObjectAttributes } from '../rules/types';
|
|||
interface GetRuleStatusSavedObject {
|
||||
services: AlertServices;
|
||||
currentStatusSavedObject: SavedObject<IRuleSavedAttributesSavedObjectAttributes>;
|
||||
lastLookBackDate: string | null | undefined;
|
||||
bulkCreateTimes: string[] | null | undefined;
|
||||
searchAfterTimes: string[] | null | undefined;
|
||||
}
|
||||
|
||||
export const writeCurrentStatusSucceeded = async ({
|
||||
services,
|
||||
currentStatusSavedObject,
|
||||
lastLookBackDate,
|
||||
bulkCreateTimes,
|
||||
searchAfterTimes,
|
||||
}: GetRuleStatusSavedObject): Promise<void> => {
|
||||
const sDate = new Date().toISOString();
|
||||
currentStatusSavedObject.attributes.status = 'succeeded';
|
||||
currentStatusSavedObject.attributes.statusDate = sDate;
|
||||
currentStatusSavedObject.attributes.lastSuccessAt = sDate;
|
||||
currentStatusSavedObject.attributes.lastSuccessMessage = 'succeeded';
|
||||
if (lastLookBackDate != null) {
|
||||
currentStatusSavedObject.attributes.lastLookBackDate = lastLookBackDate;
|
||||
}
|
||||
if (bulkCreateTimes != null) {
|
||||
currentStatusSavedObject.attributes.bulkCreateTimeDurations = bulkCreateTimes;
|
||||
}
|
||||
if (searchAfterTimes != null) {
|
||||
currentStatusSavedObject.attributes.searchAfterTimeDurations = searchAfterTimes;
|
||||
}
|
||||
await services.savedObjectsClient.update(ruleStatusSavedObjectType, currentStatusSavedObject.id, {
|
||||
...currentStatusSavedObject.attributes,
|
||||
});
|
||||
|
|
|
@ -48,6 +48,7 @@ export const writeGapErrorToSavedObject = async ({
|
|||
lastSuccessAt: currentStatusSavedObject.attributes.lastSuccessAt,
|
||||
lastFailureMessage: `Signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}" has a time gap of ${gap.humanize()} (${gap.asMilliseconds()}ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.`,
|
||||
lastSuccessMessage: currentStatusSavedObject.attributes.lastSuccessMessage,
|
||||
gap: gap.humanize(),
|
||||
});
|
||||
|
||||
if (ruleStatusSavedObjects.saved_objects.length >= 6) {
|
||||
|
|
|
@ -19,6 +19,9 @@ interface SignalRuleExceptionParams {
|
|||
message: string;
|
||||
services: AlertServices;
|
||||
name: string;
|
||||
lastLookBackDate?: string | null | undefined;
|
||||
bulkCreateTimes?: string[] | null | undefined;
|
||||
searchAfterTimes?: string[] | null | undefined;
|
||||
}
|
||||
|
||||
export const writeSignalRuleExceptionToSavedObject = async ({
|
||||
|
@ -30,6 +33,9 @@ export const writeSignalRuleExceptionToSavedObject = async ({
|
|||
ruleStatusSavedObjects,
|
||||
ruleId,
|
||||
name,
|
||||
lastLookBackDate,
|
||||
bulkCreateTimes,
|
||||
searchAfterTimes,
|
||||
}: SignalRuleExceptionParams): Promise<void> => {
|
||||
logger.error(
|
||||
`Error from signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}" message: ${message}`
|
||||
|
@ -39,6 +45,15 @@ export const writeSignalRuleExceptionToSavedObject = async ({
|
|||
currentStatusSavedObject.attributes.statusDate = sDate;
|
||||
currentStatusSavedObject.attributes.lastFailureAt = sDate;
|
||||
currentStatusSavedObject.attributes.lastFailureMessage = message;
|
||||
if (lastLookBackDate) {
|
||||
currentStatusSavedObject.attributes.lastLookBackDate = lastLookBackDate;
|
||||
}
|
||||
if (bulkCreateTimes) {
|
||||
currentStatusSavedObject.attributes.bulkCreateTimeDurations = bulkCreateTimes;
|
||||
}
|
||||
if (searchAfterTimes) {
|
||||
currentStatusSavedObject.attributes.searchAfterTimeDurations = searchAfterTimes;
|
||||
}
|
||||
// current status is failing
|
||||
await services.savedObjectsClient.update(ruleStatusSavedObjectType, currentStatusSavedObject.id, {
|
||||
...currentStatusSavedObject.attributes,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue