[SIEM] [DETECTION ENGINE] Details and Edit view for a rule (#53252) (#53698)

* re-structure detection engine + change routing name

* add editing/details feature for a rule
add feature to not edit immutable rule

* review I

* review II

* change constant

* review III
This commit is contained in:
Xavier Mouligneau 2019-12-20 12:49:00 -05:00 committed by GitHub
parent 6ac17261f6
commit 0a574034b5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
85 changed files with 2189 additions and 1529 deletions

View file

@ -14,6 +14,7 @@ import {
FetchRulesResponse,
NewRule,
Rule,
FetchRuleProps,
} from './types';
import { throwIfNotOk } from '../../../hooks/api/api';
import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants';
@ -27,7 +28,7 @@ import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants';
*/
export const addRule = async ({ rule, kbnVersion, signal }: AddRulesProps): Promise<NewRule> => {
const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}`, {
method: 'POST',
method: rule.id != null ? 'PUT' : 'POST',
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
@ -96,6 +97,28 @@ export const fetchRules = async ({
: response.json();
};
/**
* Fetch a Rule by providing a Rule ID
*
* @param id Rule ID's (not rule_id)
* @param kbnVersion current Kibana Version to use for headers
*/
export const fetchRuleById = async ({ id, kbnVersion, signal }: FetchRuleProps): Promise<Rule> => {
const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}?id=${id}`, {
method: 'GET',
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
'kbn-version': kbnVersion,
'kbn-xsrf': kbnVersion,
},
signal,
});
await throwIfNotOk(response);
const rule: Rule = await response.json();
return rule;
};
/**
* Enables/Disables provided Rule ID's
*
@ -177,11 +200,14 @@ export const duplicateRules = async ({
body: JSON.stringify({
...rule,
name: `${rule.name} [Duplicate]`,
created_at: undefined,
created_by: undefined,
id: undefined,
rule_id: undefined,
updated_at: undefined,
updated_by: undefined,
enabled: rule.enabled,
immutable: false,
}),
})
);

View file

@ -4,10 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { isEmpty, get } from 'lodash/fp';
import { isEmpty, isEqual, get } from 'lodash/fp';
import { useEffect, useState, Dispatch, SetStateAction } from 'react';
import { IIndexPattern } from 'src/plugins/data/public';
import { IIndexPattern } from '../../../../../../../../src/plugins/data/public';
import {
BrowserFields,
getBrowserFields,
@ -40,6 +40,12 @@ export const useFetchIndexPatterns = (defaultIndices: string[] = []): Return =>
const [isLoading, setIsLoading] = useState(false);
const [, dispatchToaster] = useStateToaster();
useEffect(() => {
if (!isEqual(defaultIndices, indices)) {
setIndices(defaultIndices);
}
}, [defaultIndices, indices]);
useEffect(() => {
let isSubscribed = true;
const abortCtrl = new AbortController();

View file

@ -0,0 +1,12 @@
/*
* 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.
*/
export * from './api';
export * from './fetch_index_patterns';
export * from './persist_rule';
export * from './types';
export * from './use_rule';
export * from './use_rules';

View file

@ -10,11 +10,13 @@ export const NewRuleSchema = t.intersection([
t.type({
description: t.string,
enabled: t.boolean,
filters: t.array(t.unknown),
index: t.array(t.string),
interval: t.string,
language: t.string,
name: t.string,
query: t.string,
risk_score: t.number,
severity: t.string,
type: t.union([t.literal('query'), t.literal('saved_query')]),
}),
@ -26,7 +28,9 @@ export const NewRuleSchema = t.intersection([
max_signals: t.number,
references: t.array(t.string),
rule_id: t.string,
saved_id: t.string,
tags: t.array(t.string),
threats: t.array(t.unknown),
to: t.string,
updated_by: t.string,
}),
@ -41,29 +45,41 @@ export interface AddRulesProps {
signal: AbortSignal;
}
const MetaRule = t.type({
from: t.string,
});
export const RuleSchema = t.intersection([
t.type({
created_at: t.string,
created_by: t.string,
description: t.string,
enabled: t.boolean,
false_positives: t.array(t.string),
filters: t.array(t.unknown),
from: t.string,
id: t.string,
index: t.array(t.string),
interval: t.string,
immutable: t.boolean,
language: t.string,
name: t.string,
max_signals: t.number,
meta: MetaRule,
query: t.string,
references: t.array(t.string),
risk_score: t.number,
rule_id: t.string,
severity: t.string,
type: t.string,
tags: t.array(t.string),
to: t.string,
threats: t.array(t.unknown),
updated_at: t.string,
updated_by: t.string,
}),
t.partial({
false_positives: t.array(t.string),
from: t.string,
max_signals: t.number,
references: t.array(t.string),
tags: t.array(t.string),
to: t.string,
saved_id: t.string,
}),
]);
@ -99,6 +115,12 @@ export interface FetchRulesResponse {
data: Rule[];
}
export interface FetchRuleProps {
id: string;
kbnVersion: string;
signal: AbortSignal;
}
export interface EnableRulesProps {
ids: string[];
enabled: boolean;

View file

@ -0,0 +1,67 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { useEffect, useState } from 'react';
import { useKibanaUiSetting } from '../../../lib/settings/use_kibana_ui_setting';
import { DEFAULT_KBN_VERSION } from '../../../../common/constants';
import { useStateToaster } from '../../../components/toasters';
import { errorToToaster } from '../../../components/ml/api/error_to_toaster';
import { fetchRuleById } from './api';
import * as i18n from './translations';
import { Rule } from './types';
type Return = [boolean, Rule | null];
/**
* Hook for using to get a Rule from the Detection Engine API
*
* @param id desired Rule ID's (not rule_id)
*
*/
export const useRule = (id: string | undefined): Return => {
const [rule, setRule] = useState<Rule | null>(null);
const [loading, setLoading] = useState(true);
const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION);
const [, dispatchToaster] = useStateToaster();
useEffect(() => {
let isSubscribed = true;
const abortCtrl = new AbortController();
async function fetchData(idToFetch: string) {
try {
setLoading(true);
const ruleResponse = await fetchRuleById({
id: idToFetch,
kbnVersion,
signal: abortCtrl.signal,
});
if (isSubscribed) {
setRule(ruleResponse);
}
} catch (error) {
if (isSubscribed) {
setRule(null);
errorToToaster({ title: i18n.RULE_FETCH_FAILURE, error, dispatchToaster });
}
}
if (isSubscribed) {
setLoading(false);
}
}
if (id != null) {
fetchData(id);
}
return () => {
isSubscribed = false;
abortCtrl.abort();
};
}, [id]);
return [loading, rule];
};

View file

@ -5,15 +5,46 @@
*/
import chrome from 'ui/chrome';
import { UpdateSignalStatusProps } from './types';
import { throwIfNotOk } from '../../../hooks/api/api';
import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '../../../../common/constants';
import {
DETECTION_ENGINE_QUERY_SIGNALS_URL,
DETECTION_ENGINE_SIGNALS_STATUS_URL,
} from '../../../../common/constants';
import { QuerySignals, SignalSearchResponse, UpdateSignalStatusProps } from './types';
/**
* Fetch Signals by providing a query
*
* @param query String to match a dsl
* @param kbnVersion current Kibana Version to use for headers
*/
export const fetchQuerySignals = async <Hit, Aggregations>({
query,
kbnVersion,
signal,
}: QuerySignals): Promise<SignalSearchResponse<Hit, Aggregations>> => {
const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_QUERY_SIGNALS_URL}`, {
method: 'POST',
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
'kbn-version': kbnVersion,
'kbn-xsrf': kbnVersion,
},
body: query,
signal,
});
await throwIfNotOk(response);
const signals = await response.json();
return signals;
};
/**
* Update signal status by query
*
* @param query of signals to update
* @param status to update to ('open' / 'closed')
* @param status to update to('open' / 'closed')
* @param kbnVersion current Kibana Version to use for headers
* @param signal to cancel request
*/

View file

@ -6,6 +6,9 @@
import { i18n } from '@kbn/i18n';
export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.editRule.pageTitle', {
defaultMessage: 'Edit rule settings',
});
export const SIGNAL_FETCH_FAILURE = i18n.translate(
'xpack.siem.containers.detectionEngine.signals.errorFetchingSignalsDescription',
{
defaultMessage: 'Failed to query signals',
}
);

View file

@ -4,6 +4,34 @@
* you may not use this file except in compliance with the Elastic License.
*/
export interface QuerySignals {
query: string;
kbnVersion: string;
signal: AbortSignal;
}
export interface SignalsResponse {
took: number;
timeout: boolean;
}
export interface SignalSearchResponse<Hit = {}, Aggregations = undefined> extends SignalsResponse {
_shards: {
total: number;
successful: number;
skipped: number;
failed: number;
};
aggregations?: Aggregations;
hits: {
total: {
value: number;
relation: string;
};
hits: Hit[];
};
}
export interface UpdateSignalStatusProps {
query: object;
status: 'open' | 'closed';

View file

@ -0,0 +1,67 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { useEffect, useState } from 'react';
import { useKibanaUiSetting } from '../../../lib/settings/use_kibana_ui_setting';
import { DEFAULT_KBN_VERSION } from '../../../../common/constants';
import { errorToToaster } from '../../../components/ml/api/error_to_toaster';
import { useStateToaster } from '../../../components/toasters';
import { fetchQuerySignals } from './api';
import * as i18n from './translations';
import { SignalSearchResponse } from './types';
type Return<Hit, Aggs> = [boolean, SignalSearchResponse<Hit, Aggs> | null];
/**
* Hook for using to get a Signals from the Detection Engine API
*
* @param query convert a dsl into string
*
*/
export const useQuerySignals = <Hit, Aggs>(query: string): Return<Hit, Aggs> => {
const [signals, setSignals] = useState<SignalSearchResponse<Hit, Aggs> | null>(null);
const [loading, setLoading] = useState(true);
const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION);
const [, dispatchToaster] = useStateToaster();
useEffect(() => {
let isSubscribed = true;
const abortCtrl = new AbortController();
setLoading(true);
async function fetchData() {
try {
const signalResponse = await fetchQuerySignals<Hit, Aggs>({
query,
kbnVersion,
signal: abortCtrl.signal,
});
if (isSubscribed) {
setSignals(signalResponse);
}
} catch (error) {
if (isSubscribed) {
setSignals(null);
errorToToaster({ title: i18n.SIGNAL_FETCH_FAILURE, error, dispatchToaster });
}
}
if (isSubscribed) {
setLoading(false);
}
}
fetchData();
return () => {
isSubscribed = false;
abortCtrl.abort();
};
}, [query]);
return [loading, signals];
};

View file

@ -6,9 +6,9 @@
import moment from 'moment';
import { updateSignalStatus } from '../../../containers/detection_engine/signals/api';
import { updateSignalStatus } from '../../../../containers/detection_engine/signals/api';
import { SendSignalsToTimelineActionProps, UpdateSignalStatusActionProps } from './types';
import { TimelineNonEcsData } from '../../../graphql/types';
import { TimelineNonEcsData } from '../../../../graphql/types';
export const getUpdateSignalsQuery = (eventIds: Readonly<string[]>) => {
return {

View file

@ -6,20 +6,20 @@
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import React from 'react';
import { ColumnHeader } from '../../../components/timeline/body/column_headers/column_header';
import { defaultColumnHeaderType } from '../../../components/timeline/body/column_headers/default_headers';
import { esFilters } from '../../../../../../../../../src/plugins/data/common/es_query';
import { ColumnHeader } from '../../../../components/timeline/body/column_headers/column_header';
import { TimelineAction, TimelineActionProps } from '../../../../components/timeline/body/actions';
import { defaultColumnHeaderType } from '../../../../components/timeline/body/column_headers/default_headers';
import {
DEFAULT_COLUMN_MIN_WIDTH,
DEFAULT_DATE_COLUMN_MIN_WIDTH,
} from '../../../components/timeline/body/helpers';
} from '../../../../components/timeline/body/helpers';
import { SubsetTimelineModel, timelineDefaults } from '../../../../store/timeline/model';
import * as i18n from './translations';
import { SubsetTimelineModel, timelineDefaults } from '../../../store/timeline/model';
import { esFilters } from '../../../../../../../../src/plugins/data/common/es_query';
import { FILTER_OPEN } from './signals_filter_group';
import { sendSignalsToTimelineAction, updateSignalStatusAction } from './actions';
import { FILTER_OPEN } from './components/signals_filter_group/signals_filter_group';
import { TimelineAction, TimelineActionProps } from '../../../components/timeline/body/actions';
import * as i18n from './translations';
import { CreateTimeline, SetEventsDeletedProps, SetEventsLoadingProps } from './types';
export const signalsOpenFilters: esFilters.Filter[] = [
@ -62,6 +62,26 @@ export const signalsClosedFilters: esFilters.Filter[] = [
},
];
export const buildSignalsRuleIdFilter = (ruleId: string): esFilters.Filter[] => [
{
meta: {
alias: null,
negate: false,
disabled: false,
type: 'phrase',
key: 'signal.rule.id',
params: {
query: ruleId,
},
},
query: {
match_phrase: {
'signal.rule.id': ruleId,
},
},
},
];
export const signalsHeaders: ColumnHeader[] = [
{
columnHeaderType: defaultColumnHeaderType,

View file

@ -7,8 +7,8 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { connect } from 'react-redux';
import { ActionCreator } from 'typescript-fsa';
import { SignalsUtilityBar } from './components/signals_utility_bar';
import { StatefulEventsViewer } from '../../../components/events_viewer';
import { SignalsUtilityBar } from './signals_utility_bar';
import { StatefulEventsViewer } from '../../../../components/events_viewer';
import * as i18n from './translations';
import {
getSignalsActions,
@ -17,21 +17,21 @@ import {
signalsDefaultModel,
signalsOpenFilters,
} from './default_config';
import { timelineActions, timelineSelectors } from '../../../store/timeline';
import { timelineDefaults, TimelineModel } from '../../../store/timeline/model';
import { timelineActions, timelineSelectors } from '../../../../store/timeline';
import { timelineDefaults, TimelineModel } from '../../../../store/timeline/model';
import {
FILTER_CLOSED,
FILTER_OPEN,
SignalFilterOption,
SignalsTableFilterGroup,
} from './components/signals_filter_group/signals_filter_group';
import { useKibanaUiSetting } from '../../../lib/settings/use_kibana_ui_setting';
import { DEFAULT_KBN_VERSION, DEFAULT_SIGNALS_INDEX } from '../../../../common/constants';
import { defaultHeaders } from '../../../components/timeline/body/column_headers/default_headers';
import { ColumnHeader } from '../../../components/timeline/body/column_headers/column_header';
import { esFilters, esQuery } from '../../../../../../../../src/plugins/data/common/es_query';
import { TimelineNonEcsData } from '../../../graphql/types';
import { inputsSelectors, SerializedFilterQuery, State } from '../../../store';
} from './signals_filter_group';
import { useKibanaUiSetting } from '../../../../lib/settings/use_kibana_ui_setting';
import { DEFAULT_KBN_VERSION, DEFAULT_SIGNALS_INDEX } from '../../../../../common/constants';
import { defaultHeaders } from '../../../../components/timeline/body/column_headers/default_headers';
import { ColumnHeader } from '../../../../components/timeline/body/column_headers/column_header';
import { esFilters, esQuery } from '../../../../../../../../../src/plugins/data/common/es_query';
import { TimelineNonEcsData } from '../../../../graphql/types';
import { inputsSelectors, SerializedFilterQuery, State } from '../../../../store';
import { sendSignalsToTimelineAction, updateSignalStatusAction } from './actions';
import {
CreateTimelineProps,
@ -41,12 +41,12 @@ import {
UpdateSignalsStatus,
UpdateSignalsStatusProps,
} from './types';
import { inputsActions } from '../../../store/inputs';
import { combineQueries } from '../../../components/timeline/helpers';
import { useKibanaCore } from '../../../lib/compose/kibana_core';
import { useFetchIndexPatterns } from '../../../containers/detection_engine/rules/fetch_index_patterns';
import { InputsRange } from '../../../store/inputs/model';
import { Query } from '../../../../../../../../src/plugins/data/common/query';
import { inputsActions } from '../../../../store/inputs';
import { combineQueries } from '../../../../components/timeline/helpers';
import { useKibanaCore } from '../../../../lib/compose/kibana_core';
import { useFetchIndexPatterns } from '../../../../containers/detection_engine/rules/fetch_index_patterns';
import { InputsRange } from '../../../../store/inputs/model';
import { Query } from '../../../../../../../../../src/plugins/data/common/query';
const SIGNALS_PAGE_TIMELINE_ID = 'signals-page';

View file

@ -5,7 +5,7 @@
*/
import { EuiFilterButton, EuiFilterGroup } from '@elastic/eui';
import React, { useCallback, useState } from 'react';
import * as i18n from '../../translations';
import * as i18n from '../translations';
export const FILTER_OPEN = 'open';
export const FILTER_CLOSED = 'closed';

View file

@ -8,8 +8,8 @@ import { EuiContextMenuItem } from '@elastic/eui';
import React from 'react';
import * as i18n from './translations';
import { TimelineNonEcsData } from '../../../../../graphql/types';
import { SendSignalsToTimeline, UpdateSignalsStatus } from '../../types';
import { FILTER_CLOSED, FILTER_OPEN } from '../signals_filter_group/signals_filter_group';
import { SendSignalsToTimeline, UpdateSignalsStatus } from '../types';
import { FILTER_CLOSED, FILTER_OPEN } from '../signals_filter_group';
/**
* Returns ViewInTimeline / UpdateSignalStatus actions to be display within an EuiContextMenuPanel

View file

@ -19,7 +19,7 @@ import { getBatchItems } from './batch_actions';
import { useKibanaUiSetting } from '../../../../../lib/settings/use_kibana_ui_setting';
import { DEFAULT_NUMBER_FORMAT } from '../../../../../../common/constants';
import { TimelineNonEcsData } from '../../../../../graphql/types';
import { SendSignalsToTimeline, UpdateSignalsStatus } from '../../types';
import { SendSignalsToTimeline, UpdateSignalsStatus } from '../types';
interface SignalsUtilityBarProps {
areEventsLoading: boolean;

View file

@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { esFilters } from '../../../../../../../../src/plugins/data/common/es_query';
import { TimelineNonEcsData } from '../../../graphql/types';
import { KueryFilterQuery, SerializedFilterQuery } from '../../../store';
import { esFilters } from '../../../../../../../../../src/plugins/data/common/es_query';
import { TimelineNonEcsData } from '../../../../graphql/types';
import { KueryFilterQuery, SerializedFilterQuery } from '../../../../store';
export interface SetEventsLoadingProps {
eventIds: string[];

View file

@ -0,0 +1,39 @@
/*
* 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 { EuiPanel, EuiSelect } from '@elastic/eui';
import { noop } from 'lodash/fp';
import React, { memo } from 'react';
import { HeaderSection } from '../../../../components/header_section';
import { HistogramSignals } from '../../../../components/page/detection_engine/histogram_signals';
export const sampleChartOptions = [
{ text: 'Risk scores', value: 'risk_scores' },
{ text: 'Severities', value: 'severities' },
{ text: 'Top destination IPs', value: 'destination_ips' },
{ text: 'Top event actions', value: 'event_actions' },
{ text: 'Top event categories', value: 'event_categories' },
{ text: 'Top host names', value: 'host_names' },
{ text: 'Top rule types', value: 'rule_types' },
{ text: 'Top rules', value: 'rules' },
{ text: 'Top source IPs', value: 'source_ips' },
{ text: 'Top users', value: 'users' },
];
export const SignalsCharts = memo(() => (
<EuiPanel>
<HeaderSection title="Signal detection frequency">
<EuiSelect
options={sampleChartOptions}
onChange={() => noop}
prepend="Stack by"
value={sampleChartOptions[0].value}
/>
</HeaderSection>
<HistogramSignals />
</EuiPanel>
));

View file

@ -0,0 +1,53 @@
/*
* 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 { EuiLoadingSpinner } from '@elastic/eui';
import { FormattedRelative } from '@kbn/i18n/react';
import React, { useState, useEffect } from 'react';
import { useQuerySignals } from '../../../../containers/detection_engine/signals/use_query';
import { buildlastSignalsQuery } from './query.dsl';
import { Aggs } from './types';
interface SignalInfo {
ruleId?: string | null;
}
type Return = [React.ReactNode, React.ReactNode];
export const useSignalInfo = ({ ruleId = null }: SignalInfo): Return => {
const [lastSignals, setLastSignals] = useState<React.ReactElement | null>(
<EuiLoadingSpinner size="m" />
);
const [totalSignals, setTotalSignals] = useState<React.ReactElement>(
<EuiLoadingSpinner size="m" />
);
let query = '';
try {
query = JSON.stringify(buildlastSignalsQuery(ruleId));
} catch {
query = '';
}
const [, signals] = useQuerySignals<unknown, Aggs>(query);
useEffect(() => {
if (signals != null) {
const mySignals = signals;
setLastSignals(
mySignals.aggregations?.lastSeen.value != null ? (
<FormattedRelative
value={new Date(mySignals.aggregations?.lastSeen.value_as_string ?? '')}
/>
) : null
);
setTotalSignals(<>{mySignals.hits.total.value}</>);
}
}, [signals]);
return [lastSignals, totalSignals];
};

View file

@ -0,0 +1,36 @@
/*
* 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.
*/
export const buildlastSignalsQuery = (ruleId: string | undefined | null) => {
const queryFilter = [
{
bool: { should: [{ match: { 'signal.status': 'open' } }], minimum_should_match: 1 },
},
];
return {
aggs: {
lastSeen: { max: { field: '@timestamp' } },
},
query: {
bool: {
filter:
ruleId != null
? [
...queryFilter,
{
bool: {
should: [{ match: { 'signal.rule.id': ruleId } }],
minimum_should_match: 1,
},
},
]
: queryFilter,
},
},
size: 0,
track_total_hits: true,
};
};

View file

@ -0,0 +1,12 @@
/*
* 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.
*/
export interface Aggs {
lastSeen: {
value: number;
value_as_string: string;
};
}

View file

@ -1,161 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiButton, EuiHorizontalRule, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { memo, useCallback, useState } from 'react';
import { RuleStepProps, RuleStep, AboutStepRule } from '../../types';
import * as CreateRuleI18n from '../../translations';
import { Field, Form, FormDataProvider, getUseField, UseField, useForm } from '../shared_imports';
import { AddItem } from '../add_item_form';
import { defaultRiskScoreBySeverity, severityOptions, SeverityValue } from './data';
import { defaultValue } from './default_value';
import { schema } from './schema';
import * as I18n from './translations';
import { StepRuleDescription } from '../description_step';
import { AddMitreThreat } from '../mitre';
const CommonUseField = getUseField({ component: Field });
export const StepAboutRule = memo<RuleStepProps>(({ isEditView, isLoading, setStepData }) => {
const [myStepData, setMyStepData] = useState<AboutStepRule>(defaultValue);
const { form } = useForm({
defaultValue: myStepData,
options: { stripEmptyFields: false },
schema,
});
const onSubmit = useCallback(async () => {
const { isValid, data } = await form.submit();
if (isValid) {
setStepData(RuleStep.aboutRule, data, isValid);
setMyStepData({ ...data, isNew: false } as AboutStepRule);
}
}, [form]);
return isEditView && myStepData != null ? (
<StepRuleDescription schema={schema} data={myStepData} />
) : (
<>
<Form form={form} data-test-subj="stepAboutRule">
<CommonUseField
path="name"
componentProps={{
idAria: 'detectionEngineStepAboutRuleName',
'data-test-subj': 'detectionEngineStepAboutRuleName',
euiFieldProps: {
compressed: true,
fullWidth: false,
disabled: isLoading,
},
}}
/>
<CommonUseField
path="description"
componentProps={{
compressed: true,
idAria: 'detectionEngineStepAboutRuleDescription',
'data-test-subj': 'detectionEngineStepAboutRuleDescription',
euiFieldProps: {
compressed: true,
disabled: isLoading,
},
}}
/>
<CommonUseField
path="severity"
componentProps={{
idAria: 'detectionEngineStepAboutRuleSeverity',
'data-test-subj': 'detectionEngineStepAboutRuleSeverity',
euiFieldProps: {
compressed: true,
fullWidth: false,
disabled: isLoading,
options: severityOptions,
},
}}
/>
<CommonUseField
path="riskScore"
componentProps={{
idAria: 'detectionEngineStepAboutRuleRiskScore',
'data-test-subj': 'detectionEngineStepAboutRuleRiskScore',
euiFieldProps: {
max: 100,
min: 0,
compressed: true,
fullWidth: false,
disabled: isLoading,
options: severityOptions,
},
}}
/>
<UseField
path="references"
component={AddItem}
componentProps={{
compressed: true,
addText: I18n.ADD_REFERENCE,
idAria: 'detectionEngineStepAboutRuleReferenceUrls',
isDisabled: isLoading,
dataTestSubj: 'detectionEngineStepAboutRuleReferenceUrls',
}}
/>
<UseField
path="falsePositives"
component={AddItem}
componentProps={{
compressed: true,
addText: I18n.ADD_FALSE_POSITIVE,
idAria: 'detectionEngineStepAboutRuleFalsePositives',
isDisabled: isLoading,
dataTestSubj: 'detectionEngineStepAboutRuleFalsePositives',
}}
/>
<UseField
path="threats"
component={AddMitreThreat}
componentProps={{
compressed: true,
idAria: 'detectionEngineStepAboutRuleMitreThreats',
isDisabled: isLoading,
dataTestSubj: 'detectionEngineStepAboutRuleMitreThreats',
}}
/>
<CommonUseField
path="tags"
componentProps={{
idAria: 'detectionEngineStepAboutRuleTags',
'data-test-subj': 'detectionEngineStepAboutRuleTags',
euiFieldProps: {
compressed: true,
fullWidth: true,
isDisabled: isLoading,
},
}}
/>
<FormDataProvider pathsToWatch="severity">
{({ severity }) => {
const newRiskScore = defaultRiskScoreBySeverity[severity as SeverityValue];
const riskScoreField = form.getFields().riskScore;
if (newRiskScore != null && riskScoreField.value !== newRiskScore) {
riskScoreField.setValue(newRiskScore);
}
return null;
}}
</FormDataProvider>
</Form>
<EuiHorizontalRule margin="m" />
<EuiFlexGroup alignItems="center" justifyContent="flexEnd" gutterSize="xs" responsive={false}>
<EuiFlexItem grow={false}>
<EuiButton fill onClick={onSubmit} isDisabled={isLoading}>
{myStepData.isNew ? CreateRuleI18n.CONTINUE : CreateRuleI18n.UPDATE}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
});

View file

@ -1,92 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
import React, { memo, useCallback, useState } from 'react';
import { RuleStep, RuleStepProps, ScheduleStepRule } from '../../types';
import { StepRuleDescription } from '../description_step';
import { ScheduleItem } from '../schedule_item_form';
import { Form, UseField, useForm } from '../shared_imports';
import { schema } from './schema';
import * as I18n from './translations';
export const StepScheduleRule = memo<RuleStepProps>(({ isEditView, isLoading, setStepData }) => {
const [myStepData, setMyStepData] = useState<ScheduleStepRule>({
enabled: true,
interval: '5m',
isNew: true,
from: '0m',
});
const { form } = useForm({
schema,
defaultValue: myStepData,
options: { stripEmptyFields: false },
});
const onSubmit = useCallback(
async (enabled: boolean) => {
const { isValid: newIsValid, data } = await form.submit();
if (newIsValid) {
setStepData(RuleStep.scheduleRule, { ...data, enabled }, newIsValid);
setMyStepData({ ...data, isNew: false } as ScheduleStepRule);
}
},
[form]
);
return isEditView && myStepData != null ? (
<StepRuleDescription schema={schema} data={myStepData} />
) : (
<>
<Form form={form} data-test-subj="stepScheduleRule">
<UseField
path="interval"
component={ScheduleItem}
componentProps={{
compressed: true,
idAria: 'detectionEngineStepScheduleRuleInterval',
isDisabled: isLoading,
dataTestSubj: 'detectionEngineStepScheduleRuleInterval',
}}
/>
<UseField
path="from"
component={ScheduleItem}
componentProps={{
compressed: true,
idAria: 'detectionEngineStepScheduleRuleFrom',
isDisabled: isLoading,
dataTestSubj: 'detectionEngineStepScheduleRuleFrom',
}}
/>
</Form>
<EuiHorizontalRule margin="s" />
<EuiFlexGroup alignItems="center" justifyContent="flexEnd" gutterSize="xs" responsive={false}>
<EuiFlexItem grow={false}>
<EuiButton
fill={false}
isDisabled={isLoading}
isLoading={isLoading}
onClick={onSubmit.bind(null, false)}
>
{I18n.COMPLETE_WITHOUT_ACTIVATING}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
fill
isDisabled={isLoading}
isLoading={isLoading}
onClick={onSubmit.bind(null, true)}
>
{I18n.COMPLETE_WITH_ACTIVATING}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
});

View file

@ -1,48 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.createRule.pageTitle', {
defaultMessage: 'Create new rule',
});
export const DEFINE_RULE = i18n.translate('xpack.siem.detectionEngine.createRule.defineRuleTitle', {
defaultMessage: 'Define Rule',
});
export const ABOUT_RULE = i18n.translate('xpack.siem.detectionEngine.createRule.aboutRuleTitle', {
defaultMessage: 'About Rule',
});
export const SCHEDULE_RULE = i18n.translate(
'xpack.siem.detectionEngine.createRule.scheduleRuleTitle',
{
defaultMessage: 'Schedule Rule',
}
);
export const OPTIONAL_FIELD = i18n.translate(
'xpack.siem.detectionEngine.createRule.optionalFieldDescription',
{
defaultMessage: 'Optional',
}
);
export const CONTINUE = i18n.translate(
'xpack.siem.detectionEngine.createRule.continueButtonTitle',
{
defaultMessage: 'Continue',
}
);
export const UPDATE = i18n.translate('xpack.siem.detectionEngine.createRule.updateButtonTitle', {
defaultMessage: 'Update',
});
export const DELETE = i18n.translate('xpack.siem.detectionEngine.createRule.deleteDescription', {
defaultMessage: 'Delete',
});

View file

@ -1,88 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { FieldValueQueryBar } from './components/query_bar';
import { esFilters } from '../../../../../../../../src/plugins/data/common';
export enum RuleStep {
defineRule = 'define-rule',
aboutRule = 'about-rule',
scheduleRule = 'schedule-rule',
}
export type RuleStatusType = 'passive' | 'active' | 'valid';
export interface RuleStepData {
data: unknown;
isValid: boolean;
}
export interface RuleStepProps {
setStepData: (step: RuleStep, data: unknown, isValid: boolean) => void;
isEditView: boolean;
isLoading: boolean;
resizeParentContainer?: (height: number) => void;
}
interface StepRuleData {
isNew: boolean;
}
export interface AboutStepRule extends StepRuleData {
name: string;
description: string;
severity: string;
riskScore: number;
references: string[];
falsePositives: string[];
tags: string[];
threats: IMitreEnterpriseAttack[];
}
export interface DefineStepRule extends StepRuleData {
useIndicesConfig: string;
index: string[];
queryBar: FieldValueQueryBar;
}
export interface ScheduleStepRule extends StepRuleData {
enabled: boolean;
interval: string;
from: string;
to?: string;
}
export interface DefineStepRuleJson {
index: string[];
filters: esFilters.Filter[];
saved_id?: string;
query: string;
language: string;
}
export interface AboutStepRuleJson {
name: string;
description: string;
severity: string;
risk_score: number;
references: string[];
false_positives: string[];
tags: string[];
threats: IMitreEnterpriseAttack[];
}
export type ScheduleStepRuleJson = ScheduleStepRule;
export type FormatRuleType = 'query' | 'saved_query';
export interface IMitreAttack {
id: string;
name: string;
reference: string;
}
export interface IMitreEnterpriseAttack {
framework: string;
tactic: IMitreAttack;
techniques: IMitreAttack[];
}

View file

@ -4,37 +4,26 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiButton, EuiPanel, EuiSelect, EuiSpacer } from '@elastic/eui';
import { EuiButton, EuiSpacer } from '@elastic/eui';
import React from 'react';
import { StickyContainer } from 'react-sticky';
import { FiltersGlobal } from '../../components/filters_global';
import { HeaderPage } from '../../components/header_page';
import { HeaderSection } from '../../components/header_section';
import { HistogramSignals } from '../../components/page/detection_engine/histogram_signals';
import { SiemSearchBar } from '../../components/search_bar';
import { WrapperPage } from '../../components/wrapper_page';
import { GlobalTime } from '../../containers/global_time';
import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source';
import { SpyRoute } from '../../utils/route/spy_routes';
import { SignalsTable } from './components/signals';
import { SignalsCharts } from './components/signals_chart';
import { useSignalInfo } from './components/signals_info';
import { DetectionEngineEmptyPage } from './detection_engine_empty_page';
import * as i18n from './translations';
import { SignalsTable } from './signals';
import { GlobalTime } from '../../containers/global_time';
export const DetectionEngineComponent = React.memo(() => {
const sampleChartOptions = [
{ text: 'Risk scores', value: 'risk_scores' },
{ text: 'Severities', value: 'severities' },
{ text: 'Top destination IPs', value: 'destination_ips' },
{ text: 'Top event actions', value: 'event_actions' },
{ text: 'Top event categories', value: 'event_categories' },
{ text: 'Top host names', value: 'host_names' },
{ text: 'Top rule types', value: 'rule_types' },
{ text: 'Top rules', value: 'rules' },
{ text: 'Top source IPs', value: 'source_ips' },
{ text: 'Top users', value: 'users' },
];
const [lastSignals] = useSignalInfo({});
return (
<>
<WithSource sourceId="default">
@ -46,24 +35,25 @@ export const DetectionEngineComponent = React.memo(() => {
</FiltersGlobal>
<WrapperPage>
<HeaderPage border subtitle={i18n.PAGE_SUBTITLE} title={i18n.PAGE_TITLE}>
<HeaderPage
border
subtitle={
lastSignals != null && (
<>
{i18n.LAST_SIGNAL}
{': '}
{lastSignals}
</>
)
}
title={i18n.PAGE_TITLE}
>
<EuiButton fill href="#/detection-engine/rules" iconType="gear">
{i18n.BUTTON_MANAGE_RULES}
</EuiButton>
</HeaderPage>
<EuiPanel>
<HeaderSection title="Signal detection frequency">
<EuiSelect
options={sampleChartOptions}
onChange={() => {}}
prepend="Stack by"
value={sampleChartOptions[0].value}
/>
</HeaderSection>
<HistogramSignals />
</EuiPanel>
<SignalsCharts />
<EuiSpacer />
<GlobalTime>{({ to, from }) => <SignalsTable from={from} to={to} />}</GlobalTime>
@ -72,7 +62,6 @@ export const DetectionEngineComponent = React.memo(() => {
) : (
<WrapperPage>
<HeaderPage border title={i18n.PAGE_TITLE} />
<DetectionEngineEmptyPage />
</WrapperPage>
);

View file

@ -1,128 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiSpacer,
EuiTabbedContent,
} from '@elastic/eui';
import React from 'react';
import { HeaderPage } from '../../../components/header_page';
import { HeaderSection } from '../../../components/header_section';
import { WrapperPage } from '../../../components/wrapper_page';
import { SpyRoute } from '../../../utils/route/spy_routes';
import * as i18n from './translations';
const Define = React.memo(() => (
<>
<EuiSpacer />
<EuiPanel>
<HeaderSection title="Define rule" />
</EuiPanel>
</>
));
Define.displayName = 'Define';
const About = React.memo(() => (
<>
<EuiSpacer />
<EuiPanel>
<HeaderSection title="About rule" />
</EuiPanel>
</>
));
About.displayName = 'About';
const Schedule = React.memo(() => (
<>
<EuiSpacer />
<EuiPanel>
<HeaderSection title="Schedule rule" />
</EuiPanel>
</>
));
Schedule.displayName = 'Schedule';
export const EditRuleComponent = React.memo(() => {
return (
<>
<WrapperPage restrictWidth>
<HeaderPage
backOptions={{
href: '#detection-engine/rules/rule-details',
text: 'Back to automated exfiltration',
}}
title={i18n.PAGE_TITLE}
>
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
<EuiFlexItem grow={false}>
<EuiButton href="#/detection-engine/rules/rule-details" iconType="cross">
{'Cancel'}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton fill href="#/detection-engine/rules/rule-details" iconType="save">
{'Save changes'}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</HeaderPage>
<EuiTabbedContent
tabs={[
{
id: 'tabDefine',
name: 'Define',
content: <Define />,
},
{
id: 'tabAbout',
name: 'About',
content: <About />,
},
{
id: 'tabSchedule',
name: 'Schedule',
content: <Schedule />,
},
]}
/>
<EuiSpacer />
<EuiFlexGroup
alignItems="center"
gutterSize="s"
justifyContent="flexEnd"
responsive={false}
>
<EuiFlexItem grow={false}>
<EuiButton href="#/detection-engine/rules/rule-details" iconType="cross">
{'Cancel'}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton fill href="#/detection-engine/rules/rule-details" iconType="save">
{'Save changes'}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</WrapperPage>
<SpyRoute />
</>
);
});
EditRuleComponent.displayName = 'EditRuleComponent';

View file

@ -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.
*/
export const sampleChartOptions = [
{ text: 'Risk scores', value: 'risk_scores' },
{ text: 'Severities', value: 'severities' },
{ text: 'Top destination IPs', value: 'destination_ips' },
{ text: 'Top event actions', value: 'event_actions' },
{ text: 'Top event categories', value: 'event_categories' },
{ text: 'Top host names', value: 'host_names' },
{ text: 'Top rule types', value: 'rule_types' },
{ text: 'Top rules', value: 'rules' },
{ text: 'Top source IPs', value: 'source_ips' },
{ text: 'Top users', value: 'users' },
];

View file

@ -7,10 +7,10 @@
import React from 'react';
import { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom';
import { CreateRuleComponent } from './create_rule';
import { CreateRuleComponent } from './rules/create';
import { DetectionEngineComponent } from './detection_engine';
import { EditRuleComponent } from './edit_rule';
import { RuleDetailsComponent } from './rule_details';
import { EditRuleComponent } from './rules/edit';
import { RuleDetailsComponent } from './rules/details';
import { RulesComponent } from './rules';
const detectionEnginePath = `/:pageName(detection-engine)`;
@ -19,21 +19,21 @@ type Props = Partial<RouteComponentProps<{}>> & { url: string };
export const DetectionEngineContainer = React.memo<Props>(() => (
<Switch>
<Route exact path={detectionEnginePath} render={() => <DetectionEngineComponent />} strict />
<Route exact path={`${detectionEnginePath}/rules`} render={() => <RulesComponent />} />
<Route
path={`${detectionEnginePath}/rules/create-rule`}
render={() => <CreateRuleComponent />}
/>
<Route
exact
path={`${detectionEnginePath}/rules/rule-details`}
render={() => <RuleDetailsComponent />}
/>
<Route
path={`${detectionEnginePath}/rules/rule-details/edit-rule`}
render={() => <EditRuleComponent />}
/>
<Route exact path={detectionEnginePath} strict>
<DetectionEngineComponent />
</Route>
<Route exact path={`${detectionEnginePath}/rules`}>
<RulesComponent />
</Route>
<Route path={`${detectionEnginePath}/rules/create`}>
<CreateRuleComponent />
</Route>
<Route exact path={`${detectionEnginePath}/rules/:ruleId`}>
<RuleDetailsComponent />
</Route>
<Route path={`${detectionEnginePath}/rules/:ruleId/edit`}>
<EditRuleComponent />
</Route>
<Route
path="/detection-engine/"
render={({ location: { search = '' } }) => (

View file

@ -1,660 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
EuiBasicTable,
EuiButton,
EuiButtonIcon,
EuiCallOut,
EuiFilterButton,
EuiFilterGroup,
EuiFlexGroup,
EuiFlexItem,
EuiIconTip,
EuiPanel,
EuiPopover,
EuiSelect,
EuiSpacer,
EuiSwitch,
EuiTabbedContent,
EuiTextColor,
} from '@elastic/eui';
import moment from 'moment';
import React, { useState } from 'react';
import { StickyContainer } from 'react-sticky';
import { getEmptyTagValue } from '../../../components/empty_value';
import { FiltersGlobal } from '../../../components/filters_global';
import { HeaderPage } from '../../../components/header_page';
import { HeaderSection } from '../../../components/header_section';
import { HistogramSignals } from '../../../components/page/detection_engine/histogram_signals';
import { ProgressInline } from '../../../components/progress_inline';
import { SiemSearchBar } from '../../../components/search_bar';
import {
UtilityBar,
UtilityBarAction,
UtilityBarGroup,
UtilityBarSection,
UtilityBarText,
} from '../../../components/detection_engine/utility_bar';
import { WrapperPage } from '../../../components/wrapper_page';
import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../../containers/source';
import { SpyRoute } from '../../../utils/route/spy_routes';
import { DetectionEngineEmptyPage } from '../detection_engine_empty_page';
import * as i18n from './translations';
// Michael: Will need to change this to get the current datetime format from Kibana settings.
const dateTimeFormat = (value: string) => {
return moment(value).format('M/D/YYYY, h:mm A');
};
const OpenSignals = React.memo(() => {
return (
<>
<UtilityBar>
<UtilityBarSection>
<UtilityBarGroup>
<UtilityBarText>{'Showing: 439 signals'}</UtilityBarText>
</UtilityBarGroup>
<UtilityBarGroup>
<UtilityBarText>{'Selected: 20 signals'}</UtilityBarText>
<UtilityBarAction
iconSide="right"
iconType="arrowDown"
popoverContent={() => <p>{'Batch actions context menu here.'}</p>}
>
{'Batch actions'}
</UtilityBarAction>
<UtilityBarAction iconType="listAdd">
{'Select all signals on all pages'}
</UtilityBarAction>
</UtilityBarGroup>
<UtilityBarGroup>
<UtilityBarAction iconType="cross">{'Clear 7 filters'}</UtilityBarAction>
<UtilityBarAction iconType="cross">{'Clear aggregation'}</UtilityBarAction>
</UtilityBarGroup>
</UtilityBarSection>
<UtilityBarSection>
<UtilityBarGroup>
<UtilityBarAction
iconSide="right"
iconType="arrowDown"
popoverContent={() => <p>{'Customize columns context menu here.'}</p>}
>
{'Customize columns'}
</UtilityBarAction>
<UtilityBarAction iconType="indexMapping">{'Aggregate data'}</UtilityBarAction>
</UtilityBarGroup>
</UtilityBarSection>
</UtilityBar>
{/* Michael: Open signals datagrid here. Talk to Chandler Prall about possibility of early access. If not possible, use basic table. */}
</>
);
});
const ClosedSignals = React.memo(() => {
return (
<>
<UtilityBar>
<UtilityBarSection>
<UtilityBarGroup>
<UtilityBarText>{'Showing: 439 signals'}</UtilityBarText>
</UtilityBarGroup>
</UtilityBarSection>
<UtilityBarSection>
<UtilityBarGroup>
<UtilityBarAction
iconSide="right"
iconType="arrowDown"
popoverContent={() => <p>{'Customize columns context menu here.'}</p>}
>
{'Customize columns'}
</UtilityBarAction>
<UtilityBarAction iconType="indexMapping">{'Aggregate data'}</UtilityBarAction>
</UtilityBarGroup>
</UtilityBarSection>
</UtilityBar>
{/* Michael: Closed signals datagrid here. Talk to Chandler Prall about possibility of early access. If not possible, use basic table. */}
</>
);
});
const Signals = React.memo(() => {
const sampleChartOptions = [
{ text: 'Risk scores', value: 'risk_scores' },
{ text: 'Severities', value: 'severities' },
{ text: 'Top destination IPs', value: 'destination_ips' },
{ text: 'Top event actions', value: 'event_actions' },
{ text: 'Top event categories', value: 'event_categories' },
{ text: 'Top host names', value: 'host_names' },
{ text: 'Top source IPs', value: 'source_ips' },
{ text: 'Top users', value: 'users' },
];
const filterGroupOptions = ['open', 'closed'];
const [filterGroupState, setFilterGroupState] = useState(filterGroupOptions[0]);
return (
<>
<EuiSpacer />
<EuiPanel>
<HeaderSection title="Signal detection frequency">
<EuiSelect
options={sampleChartOptions}
onChange={() => {}}
prepend="Stack by"
value={sampleChartOptions[0].value}
/>
</HeaderSection>
<HistogramSignals />
</EuiPanel>
<EuiSpacer />
<EuiPanel>
<HeaderSection title="Signals">
<EuiFilterGroup>
<EuiFilterButton
hasActiveFilters={filterGroupState === filterGroupOptions[0]}
onClick={() => setFilterGroupState(filterGroupOptions[0])}
withNext
>
{'Open signals'}
</EuiFilterButton>
<EuiFilterButton
hasActiveFilters={filterGroupState === filterGroupOptions[1]}
onClick={() => setFilterGroupState(filterGroupOptions[1])}
>
{'Closed signals'}
</EuiFilterButton>
</EuiFilterGroup>
</HeaderSection>
{filterGroupState === filterGroupOptions[0] ? <OpenSignals /> : <ClosedSignals />}
</EuiPanel>
</>
);
});
Signals.displayName = 'Signals';
const ActivityMonitor = React.memo(() => {
interface ColumnTypes {
id: number;
ran: string;
lookedBackTo: string;
status: string;
response: string | undefined;
}
interface PageTypes {
index: number;
size: number;
}
interface SortTypes {
field: string;
direction: string;
}
const actions = [
{
available: (item: ColumnTypes) => item.status === 'Running',
description: 'Stop',
icon: 'stop',
isPrimary: true,
name: 'Stop',
onClick: () => {},
type: 'icon',
},
{
available: (item: ColumnTypes) => item.status === 'Stopped',
description: 'Resume',
icon: 'play',
isPrimary: true,
name: 'Resume',
onClick: () => {},
type: 'icon',
},
];
// Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes?
const columns = [
{
field: 'ran',
name: 'Ran',
render: (value: ColumnTypes['ran']) => <time dateTime={value}>{dateTimeFormat(value)}</time>,
sortable: true,
truncateText: true,
},
{
field: 'lookedBackTo',
name: 'Looked back to',
render: (value: ColumnTypes['lookedBackTo']) => (
<time dateTime={value}>{dateTimeFormat(value)}</time>
),
sortable: true,
truncateText: true,
},
{
field: 'status',
name: 'Status',
sortable: true,
truncateText: true,
},
{
field: 'response',
name: 'Response',
render: (value: ColumnTypes['response']) => {
return value === undefined ? (
getEmptyTagValue()
) : (
<>
{value === 'Fail' ? (
<EuiTextColor color="danger">
{value} <EuiIconTip content="Full fail message here." type="iInCircle" />
</EuiTextColor>
) : (
<EuiTextColor color="secondary">{value}</EuiTextColor>
)}
</>
);
},
sortable: true,
truncateText: true,
},
{
actions,
width: '40px',
},
];
const sampleTableData = [
{
id: 1,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Running',
},
{
id: 2,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Stopped',
},
{
id: 3,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Fail',
},
{
id: 4,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 5,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 6,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 7,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 8,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 9,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 10,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 11,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 12,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 13,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 14,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 15,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 16,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 17,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 18,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 19,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 20,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 21,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
];
const [itemsTotalState] = useState<number>(sampleTableData.length);
const [pageState, setPageState] = useState<PageTypes>({ index: 0, size: 20 });
// const [selectedState, setSelectedState] = useState<ColumnTypes[]>([]);
const [sortState, setSortState] = useState<SortTypes>({ field: 'ran', direction: 'desc' });
return (
<>
<EuiSpacer />
<EuiPanel>
<HeaderSection title="Activity monitor" />
<UtilityBar border>
<UtilityBarSection>
<UtilityBarGroup>
<UtilityBarText>{'Showing: 39 activites'}</UtilityBarText>
</UtilityBarGroup>
<UtilityBarGroup>
<UtilityBarText>{'Selected: 2 activities'}</UtilityBarText>
<UtilityBarAction iconType="stop">{'Stop selected'}</UtilityBarAction>
</UtilityBarGroup>
<UtilityBarGroup>
<UtilityBarAction iconType="cross">{'Clear 7 filters'}</UtilityBarAction>
</UtilityBarGroup>
</UtilityBarSection>
</UtilityBar>
<EuiBasicTable
columns={columns}
isSelectable
itemId="id"
items={sampleTableData}
onChange={({ page, sort }: { page: PageTypes; sort: SortTypes }) => {
setPageState(page);
setSortState(sort);
}}
pagination={{
pageIndex: pageState.index,
pageSize: pageState.size,
totalItemCount: itemsTotalState,
pageSizeOptions: [5, 10, 20],
}}
selection={{
selectable: (item: ColumnTypes) => item.status !== 'Completed',
selectableMessage: (selectable: boolean) =>
selectable ? undefined : 'Completed runs cannot be acted upon',
onSelectionChange: (selectedItems: ColumnTypes[]) => {
// setSelectedState(selectedItems);
},
}}
sorting={{
sort: sortState,
}}
/>
</EuiPanel>
</>
);
});
ActivityMonitor.displayName = 'ActivityMonitor';
export const RuleDetailsComponent = React.memo(() => {
const [popoverState, setPopoverState] = useState(false);
return (
<>
<WithSource sourceId="default">
{({ indicesExist, indexPattern }) => {
return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? (
<StickyContainer>
<FiltersGlobal>
<SiemSearchBar id="global" indexPattern={indexPattern} />
</FiltersGlobal>
<WrapperPage>
<HeaderPage
backOptions={{ href: '#detection-engine/rules', text: 'Back to rules' }}
badgeOptions={{ text: 'Experimental' }}
border
subtitle={[
'Created by: mmarcialis on 12/28/2019, 12:00 PM',
'Updated by: agoldstein on 12/28/2019, 12:00 PM',
]}
subtitle2={[
'Last signal: 23 minutes ago',
<ProgressInline current={95000} max={105000} unit="events">
{'Status: Running'}
</ProgressInline>,
]}
title="Automated exfiltration"
>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiSwitch checked={true} label="Activate rule" onChange={() => {}} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
<EuiFlexItem grow={false}>
<EuiButton
href="#detection-engine/rules/rule-details/edit-rule"
iconType="visControls"
>
{'Edit rule settings'}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPopover
button={
<EuiButtonIcon
aria-label="Additional actions"
iconType="boxesHorizontal"
onClick={() => setPopoverState(!popoverState)}
/>
}
closePopover={() => setPopoverState(false)}
isOpen={popoverState}
>
<p>{'Overflow context menu here.'}</p>
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</HeaderPage>
<EuiCallOut
color="danger"
iconType="alert"
size="s"
title="Rule failed to run on 12/28/2019, 12:00 PM"
>
<p>{'Full fail message here.'}</p>
</EuiCallOut>
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem component="section" grow={1}>
<EuiPanel>
<HeaderSection title="Definition" />
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem component="section" grow={2}>
<EuiPanel>
<HeaderSection title="About" />
{/* <p>{'Description'}</p> */}
{/* <EuiFlexGrid columns={2}>
<EuiFlexItem style={{ flex: '0 0 calc(100% - 24px)' }}>
<p>{'Description'}</p>
</EuiFlexItem>
<EuiFlexItem>
<p>{'Severity'}</p>
</EuiFlexItem>
<EuiFlexItem>
<p>{'Risk score boost'}</p>
</EuiFlexItem>
<EuiFlexItem>
<p>{'References'}</p>
</EuiFlexItem>
<EuiFlexItem>
<p>{'False positives'}</p>
</EuiFlexItem>
<EuiFlexItem>
<p>{'Mitre ATT&CK types'}</p>
</EuiFlexItem>
<EuiFlexItem>
<p>{'Tags'}</p>
</EuiFlexItem>
</EuiFlexGrid> */}
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem component="section" grow={1}>
<EuiPanel>
<HeaderSection title="Schedule" />
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiTabbedContent
tabs={[
{
id: 'tabSignals',
name: 'Signals',
content: <Signals />,
},
{
id: 'tabActivityMonitor',
name: 'Activity monitor',
content: <ActivityMonitor />,
},
]}
/>
</WrapperPage>
</StickyContainer>
) : (
<WrapperPage>
<HeaderPage border title={i18n.PAGE_TITLE} />
<DetectionEngineEmptyPage />
</WrapperPage>
);
}}
</WithSource>
<SpyRoute />
</>
);
});
RuleDetailsComponent.displayName = 'RuleDetailsComponent';

View file

@ -4,16 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
import * as H from 'history';
import React from 'react';
import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine';
import {
deleteRules,
duplicateRules,
enableRules,
} from '../../../../containers/detection_engine/rules/api';
Rule,
} from '../../../../containers/detection_engine/rules';
import { Action } from './reducer';
import { Rule } from '../../../../containers/detection_engine/rules/types';
export const editRuleAction = () => {};
export const editRuleAction = (rule: Rule, history: H.History) => {
history.push(`/${DETECTION_ENGINE_PAGE_NAME}/rules/${rule.id}/edit`);
};
export const runRuleAction = () => {};
@ -25,7 +30,7 @@ export const duplicateRuleAction = async (
dispatch({ type: 'updateLoading', ids: [rule.id], isLoading: true });
const duplicatedRule = await duplicateRules({ rules: [rule], kbnVersion });
dispatch({ type: 'updateLoading', ids: [rule.id], isLoading: false });
dispatch({ type: 'updateRules', rules: duplicatedRule });
dispatch({ type: 'updateRules', rules: duplicatedRule, appendRuleId: rule.id });
};
export const exportRulesAction = async (rules: Rule[], dispatch: React.Dispatch<Action>) => {

View file

@ -5,6 +5,7 @@
*/
import { EuiBadge, EuiHealth, EuiIconTip, EuiLink, EuiTextColor } from '@elastic/eui';
import * as H from 'history';
import React from 'react';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { getEmptyTagValue } from '../../../../components/empty_value';
@ -12,7 +13,6 @@ import {
deleteRulesAction,
duplicateRuleAction,
editRuleAction,
enableRulesAction,
exportRulesAction,
runRuleAction,
} from './actions';
@ -21,15 +21,15 @@ import { Action } from './reducer';
import { TableData } from '../types';
import * as i18n from '../translations';
import { PreferenceFormattedDate } from '../../../../components/formatted_date';
import { RuleSwitch, RuleStateChangeCallback } from '../components/rule_switch';
import { RuleSwitch } from '../components/rule_switch';
const getActions = (dispatch: React.Dispatch<Action>, kbnVersion: string) => [
const getActions = (dispatch: React.Dispatch<Action>, kbnVersion: string, history: H.History) => [
{
description: i18n.EDIT_RULE_SETTINGS,
icon: 'visControls',
name: i18n.EDIT_RULE_SETTINGS,
onClick: editRuleAction,
enabled: () => false,
onClick: (rowItem: TableData) => editRuleAction(rowItem.sourceRule, history),
enabled: (rowItem: TableData) => !rowItem.sourceRule.immutable,
},
{
description: i18n.RUN_RULE_MANUALLY,
@ -59,7 +59,11 @@ const getActions = (dispatch: React.Dispatch<Action>, kbnVersion: string) => [
];
// Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes?
export const getColumns = (dispatch: React.Dispatch<Action>, kbnVersion: string) => [
export const getColumns = (
dispatch: React.Dispatch<Action>,
kbnVersion: string,
history: H.History
) => [
{
field: 'rule',
name: i18n.COLUMN_RULE,
@ -147,25 +151,19 @@ export const getColumns = (dispatch: React.Dispatch<Action>, kbnVersion: string)
align: 'center',
field: 'activate',
name: i18n.COLUMN_ACTIVATE,
render: (value: TableData['activate'], item: TableData) => {
const handleRuleStateChange: RuleStateChangeCallback = async (enabled, id) => {
await enableRulesAction([id], enabled, dispatch, kbnVersion);
};
return (
<RuleSwitch
id={item.id}
enabled={item.activate}
isLoading={item.isLoading}
onRuleStateChange={handleRuleStateChange}
/>
);
},
render: (value: TableData['activate'], item: TableData) => (
<RuleSwitch
dispatch={dispatch}
id={item.id}
enabled={item.activate}
isLoading={item.isLoading}
/>
),
sortable: true,
width: '85px',
},
{
actions: getActions(dispatch, kbnVersion),
actions: getActions(dispatch, kbnVersion, history),
width: '40px',
},
];

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Rule } from '../../../../containers/detection_engine/rules/types';
import { Rule } from '../../../../containers/detection_engine/rules';
import { TableData } from '../types';
import { getEmptyValue } from '../../../../components/empty_value';
@ -13,7 +13,7 @@ export const formatRules = (rules: Rule[], selectedIds?: string[]): TableData[]
id: rule.id,
rule_id: rule.rule_id,
rule: {
href: `#/detection-engine/rules/rule-details/${encodeURIComponent(rule.id)}`,
href: `#/detection-engine/rules/${encodeURIComponent(rule.id)}`,
name: rule.name,
status: 'Status Placeholder',
},

View file

@ -12,6 +12,7 @@ import {
EuiSpacer,
} from '@elastic/eui';
import React, { useCallback, useEffect, useReducer, useState } from 'react';
import { useHistory } from 'react-router-dom';
import uuid from 'uuid';
import { HeaderSection } from '../../../../components/header_section';
@ -23,7 +24,7 @@ import {
UtilityBarText,
} from '../../../../components/detection_engine/utility_bar';
import { getColumns } from './columns';
import { useRules } from '../../../../containers/detection_engine/rules/use_rules';
import { useRules } from '../../../../containers/detection_engine/rules';
import { Loader } from '../../../../components/loader';
import { Panel } from '../../../../components/panel';
import { getBatchItems } from './batch_actions';
@ -74,7 +75,7 @@ export const AllRules = React.memo<{ importCompleteToggle: boolean }>(importComp
},
dispatch,
] = useReducer(allRulesReducer, initialState);
const history = useHistory();
const [isInitialLoad, setIsInitialLoad] = useState(true);
const [isLoadingRules, rulesData] = useRules(pagination, filterOptions, refreshToggle);
const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION);
@ -184,7 +185,7 @@ export const AllRules = React.memo<{ importCompleteToggle: boolean }>(importComp
</UtilityBar>
<EuiBasicTable
columns={getColumns(dispatch, kbnVersion)}
columns={getColumns(dispatch, kbnVersion, history)}
isSelectable
itemId="rule_id"
items={tableData}

View file

@ -8,7 +8,7 @@ import {
FilterOptions,
PaginationOptions,
Rule,
} from '../../../../containers/detection_engine/rules/types';
} from '../../../../containers/detection_engine/rules';
import { TableData } from '../types';
import { formatRules } from './helpers';
@ -31,7 +31,7 @@ export type Action =
| { type: 'setExportPayload'; exportPayload?: object[] }
| { type: 'setSelected'; selectedItems: TableData[] }
| { type: 'updateLoading'; ids: string[]; isLoading: boolean }
| { type: 'updateRules'; rules: Rule[]; pagination?: PaginationOptions }
| { type: 'updateRules'; rules: Rule[]; appendRuleId?: string; pagination?: PaginationOptions }
| { type: 'updatePagination'; pagination: PaginationOptions }
| { type: 'updateFilterOptions'; filterOptions: FilterOptions }
| { type: 'failure' };
@ -56,10 +56,18 @@ export const allRulesReducer = (state: State, action: Action): State => {
}
const ruleIds = state.rules.map(r => r.rule_id);
const appendIdx =
action.appendRuleId != null ? state.rules.findIndex(r => r.id === action.appendRuleId) : -1;
const updatedRules = action.rules.reduce(
(rules, updatedRule) =>
ruleIds.includes(updatedRule.rule_id)
? rules.map(r => (updatedRule.rule_id === r.rule_id ? updatedRule : r))
: appendIdx !== -1
? [
...rules.slice(0, appendIdx + 1),
updatedRule,
...rules.slice(appendIdx + 1, rules.length - 1),
]
: [...rules, updatedRule],
[...state.rules]
);

View file

@ -8,8 +8,8 @@ import { EuiButtonEmpty, EuiButtonIcon, EuiFormRow, EuiFieldText, EuiSpacer } fr
import { isEmpty } from 'lodash/fp';
import React, { ChangeEvent, useCallback, useEffect, useState, useRef } from 'react';
import * as RuleI18n from '../../translations';
import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports';
import * as CreateRuleI18n from '../../translations';
interface AddItemProps {
addText: string;
@ -134,7 +134,7 @@ export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: Ad
iconType="trash"
isDisabled={isDisabled}
onClick={() => removeItem(index)}
aria-label={CreateRuleI18n.DELETE}
aria-label={RuleI18n.DELETE}
/>
}
onChange={e => updateItem(e, index)}

View file

@ -7,6 +7,7 @@
import {
EuiBadge,
EuiDescriptionList,
EuiLoadingSpinner,
EuiFlexGroup,
EuiFlexItem,
EuiTextArea,
@ -15,15 +16,16 @@ import {
EuiListGroup,
} from '@elastic/eui';
import { isEmpty, chunk, get, pick } from 'lodash/fp';
import React, { memo, ReactNode } from 'react';
import React, { memo, ReactNode, useState } from 'react';
import styled from 'styled-components';
import {
IIndexPattern,
esFilters,
FilterManager,
Query,
} from '../../../../../../../../../../src/plugins/data/public';
import { useKibanaCore } from '../../../../../lib/compose/kibana_core';
import { FilterLabel } from './filter_label';
import { FormSchema } from '../shared_imports';
import * as I18n from './translations';
@ -32,6 +34,7 @@ import { IMitreEnterpriseAttack } from '../../types';
import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques';
interface StepRuleDescriptionProps {
direction?: 'row' | 'column';
data: unknown;
indexPatterns?: IIndexPattern;
schema: FormSchema;
@ -43,8 +46,8 @@ const EuiBadgeWrap = styled(EuiBadge)`
}
`;
const EuiFlexItemWidth = styled(EuiFlexItem)`
width: 50%;
const EuiFlexItemWidth = styled(EuiFlexItem)<{ direction: string }>`
${props => (props.direction === 'row' ? 'width : 50%;' : 'width: 100%;')};
`;
const MyEuiListGroup = styled(EuiListGroup)`
@ -60,21 +63,33 @@ const ThreatsEuiFlexGroup = styled(EuiFlexGroup)`
}
`;
const MyEuiTextArea = styled(EuiTextArea)`
max-width: 100%;
height: 80px;
`;
export const StepRuleDescription = memo<StepRuleDescriptionProps>(
({ data, indexPatterns, schema }) => {
({ data, direction = 'row', indexPatterns, schema }) => {
const core = useKibanaCore();
const [filterManager] = useState<FilterManager>(new FilterManager(core.uiSettings));
const keys = Object.keys(schema);
const listItems = keys.reduce(
(acc: ListItems[], key: string) => [
...acc,
...buildListItems(data, pick(key, schema), indexPatterns),
...buildListItems(data, pick(key, schema), filterManager, indexPatterns),
],
[]
);
return (
<EuiFlexGroup gutterSize="none" direction="row" justifyContent="spaceAround">
<EuiFlexGroup gutterSize="none" direction={direction} justifyContent="spaceAround">
{chunk(Math.ceil(listItems.length / 2), listItems).map((chunckListItems, index) => (
<EuiFlexItemWidth key={`description-step-rule-${index}`} grow={false}>
<EuiDescriptionList listItems={chunckListItems} />
<EuiFlexItemWidth
direction={direction}
key={`description-step-rule-${index}`}
grow={false}
>
<EuiDescriptionList listItems={chunckListItems} compressed />
</EuiFlexItemWidth>
))}
</EuiFlexGroup>
@ -90,12 +105,19 @@ interface ListItems {
const buildListItems = (
data: unknown,
schema: FormSchema,
filterManager: FilterManager,
indexPatterns?: IIndexPattern
): ListItems[] =>
Object.keys(schema).reduce<ListItems[]>(
(acc, field) => [
...acc,
...getDescriptionItem(field, get([field, 'label'], schema), data, indexPatterns),
...getDescriptionItem(
field,
get([field, 'label'], schema),
data,
filterManager,
indexPatterns
),
],
[]
);
@ -104,29 +126,35 @@ const getDescriptionItem = (
field: string,
label: string,
value: unknown,
filterManager: FilterManager,
indexPatterns?: IIndexPattern
): ListItems[] => {
if (field === 'useIndicesConfig') {
return [];
} else if (field === 'queryBar' && indexPatterns != null) {
} else if (field === 'queryBar') {
const filters = get('queryBar.filters', value) as esFilters.Filter[];
const query = get('queryBar.query', value) as Query;
const savedId = get('queryBar.saved_id', value);
let items: ListItems[] = [];
if (!isEmpty(filters)) {
filterManager.setFilters(filters);
items = [
...items,
{
title: <>{I18n.FILTERS_LABEL}</>,
description: (
<EuiFlexGroup wrap responsive={false} gutterSize="xs">
{filters.map((filter, index) => (
{filterManager.getFilters().map((filter, index) => (
<EuiFlexItem grow={false} key={`${field}-filter-${index}`}>
<EuiBadgeWrap color="hollow">
<FilterLabel
filter={filter}
valueLabel={esFilters.getDisplayValueFromFilter(filter, [indexPatterns])}
/>
{indexPatterns != null ? (
<FilterLabel
filter={filter}
valueLabel={esFilters.getDisplayValueFromFilter(filter, [indexPatterns])}
/>
) : (
<EuiLoadingSpinner size="m" />
)}
</EuiBadgeWrap>
</EuiFlexItem>
))}
@ -202,7 +230,7 @@ const getDescriptionItem = (
return [
{
title: label,
description: <EuiTextArea value={get(field, value)} readOnly={true} />,
description: <MyEuiTextArea value={get(field, value)} readOnly={true} />,
},
];
} else if (Array.isArray(get(field, value))) {
@ -212,7 +240,7 @@ const getDescriptionItem = (
{
title: label,
description: (
<EuiFlexGroup responsive={false} gutterSize="xs">
<EuiFlexGroup responsive={false} gutterSize="xs" wrap>
{values.map((val: string) =>
isEmpty(val) ? null : (
<EuiFlexItem grow={false} key={`${field}-${val}`}>
@ -227,10 +255,14 @@ const getDescriptionItem = (
}
return [];
}
return [
{
title: label,
description: get(field, value),
},
];
const description: string = get(field, value);
if (!isEmpty(description)) {
return [
{
title: label,
description,
},
];
}
return [];
};

View file

@ -19,20 +19,20 @@ import {
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { noop } from 'lodash/fp';
import React, { useCallback, useState } from 'react';
import { failure } from 'io-ts/lib/PathReporter';
import { identity } from 'fp-ts/lib/function';
import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import uuid from 'uuid';
import * as i18n from './translations';
import { duplicateRules } from '../../../../../containers/detection_engine/rules/api';
import { duplicateRules, RulesSchema } from '../../../../../containers/detection_engine/rules';
import { useKibanaUiSetting } from '../../../../../lib/settings/use_kibana_ui_setting';
import { DEFAULT_KBN_VERSION } from '../../../../../../common/constants';
import { ndjsonToJSON } from '../json_downloader';
import { RulesSchema } from '../../../../../containers/detection_engine/rules/types';
import { useStateToaster } from '../../../../../components/toasters';
import { ndjsonToJSON } from '../json_downloader';
import * as i18n from './translations';
interface ImportRuleModalProps {
showModal: boolean;
@ -138,7 +138,7 @@ export const ImportRuleModalComponent = ({
id="rule-overwrite-saved-object"
label={i18n.OVERWRITE_WITH_SAME_NAME}
disabled={true}
onChange={() => {}}
onChange={() => noop}
/>
</EuiModalBody>

View file

@ -20,7 +20,7 @@ import React, { ChangeEvent, useCallback } from 'react';
import styled from 'styled-components';
import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques';
import * as CreateRuleI18n from '../../translations';
import * as RuleI18n from '../../translations';
import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports';
import * as I18n from './translations';
import { IMitreEnterpriseAttack } from '../../types';
@ -154,7 +154,7 @@ export const AddMitreThreat = ({ dataTestSubj, field, idAria, isDisabled }: AddI
iconType="trash"
isDisabled={isDisabled}
onClick={() => removeItem(index)}
aria-label={CreateRuleI18n.DELETE}
aria-label={RuleI18n.DELETE}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -217,6 +217,7 @@ export const QueryBarDefineRule = ({
onSubmitQuery={onSubmitQuery}
savedQuery={savedQuery}
onSavedQuery={onSavedQuery}
hideSavedQuery={false}
/>
</div>
)}

View file

@ -14,7 +14,7 @@ exports[`RuleSwitch renders correctly against snapshot 1`] = `
disabled={false}
label="rule-switch"
onChange={[Function]}
showLabel={false}
showLabel={true}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -7,17 +7,26 @@
import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import * as React from 'react';
import { useKibanaCore } from '../../../../../lib/compose/kibana_core';
import { RuleSwitchComponent } from './index';
const mockUseKibanaCore = useKibanaCore as jest.Mock;
jest.mock('../../../../../lib/compose/kibana_core');
mockUseKibanaCore.mockImplementation(() => ({
uiSettings: {
get$: () => 'world',
},
injectedMetadata: {
getKibanaVersion: () => '8.0.0',
},
}));
describe('RuleSwitch', () => {
test('renders correctly against snapshot', () => {
const wrapper = shallow(
<RuleSwitchComponent
enabled={true}
id={'7'}
isLoading={false}
onRuleStateChange={jest.fn()}
/>
<RuleSwitchComponent optionLabel="rule-switch" enabled={true} id={'7'} isLoading={false} />
);
expect(toJson(wrapper)).toMatchSnapshot();
});

View file

@ -4,9 +4,22 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
EuiFlexGroup,
EuiFlexItem,
EuiLoadingSpinner,
EuiSwitch,
EuiSwitchEvent,
} from '@elastic/eui';
import { isEmpty } from 'lodash/fp';
import styled from 'styled-components';
import React, { useCallback } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSwitch } from '@elastic/eui';
import React, { useCallback, useState, useEffect } from 'react';
import { DEFAULT_KBN_VERSION } from '../../../../../../common/constants';
import { enableRules } from '../../../../../containers/detection_engine/rules';
import { useKibanaUiSetting } from '../../../../../lib/settings/use_kibana_ui_setting';
import { enableRulesAction } from '../../all/actions';
import { Action } from '../../all/reducer';
const StaticSwitch = styled(EuiSwitch)`
.euiSwitch__thumb,
@ -17,43 +30,75 @@ const StaticSwitch = styled(EuiSwitch)`
StaticSwitch.displayName = 'StaticSwitch';
export type RuleStateChangeCallback = (isEnabled: boolean, id: string) => void;
export interface RuleSwitchProps {
dispatch?: React.Dispatch<Action>;
id: string;
enabled: boolean;
isLoading: boolean;
onRuleStateChange: RuleStateChangeCallback;
isLoading?: boolean;
optionLabel?: string;
}
/**
* Basic switch component for displaying loader when enabled/disabled
*/
export const RuleSwitchComponent = ({
dispatch,
id,
enabled,
isLoading,
onRuleStateChange,
enabled,
optionLabel,
}: RuleSwitchProps) => {
const handleChange = useCallback(
e => {
onRuleStateChange(e.target.checked!, id);
const [myIsLoading, setMyIsLoading] = useState(false);
const [myEnabled, setMyEnabled] = useState(enabled ?? false);
const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION);
const onRuleStateChange = useCallback(
async (event: EuiSwitchEvent) => {
setMyIsLoading(true);
if (dispatch != null) {
await enableRulesAction([id], event.target.checked!, dispatch, kbnVersion);
} else {
try {
const updatedRules = await enableRules({
ids: [id],
enabled: event.target.checked!,
kbnVersion,
});
setMyEnabled(updatedRules[0].enabled);
} catch {
setMyIsLoading(false);
}
}
setMyIsLoading(false);
},
[onRuleStateChange, id]
[dispatch, id, kbnVersion]
);
useEffect(() => {
if (myEnabled !== enabled) {
setMyEnabled(enabled);
}
}, [enabled]);
useEffect(() => {
if (myIsLoading !== isLoading) {
setMyIsLoading(isLoading ?? false);
}
}, [isLoading]);
return (
<EuiFlexGroup alignItems="center" justifyContent="spaceAround">
<EuiFlexItem grow={false}>
{isLoading ? (
{myIsLoading ? (
<EuiLoadingSpinner size="m" data-test-subj="rule-switch-loader" />
) : (
<StaticSwitch
data-test-subj="rule-switch"
label="rule-switch"
showLabel={false}
label={optionLabel ?? ''}
showLabel={!isEmpty(optionLabel)}
disabled={false}
checked={enabled ?? false}
onChange={handleChange}
checked={myEnabled}
onChange={onRuleStateChange}
/>
)}
</EuiFlexItem>

View file

@ -37,23 +37,25 @@ export const ScheduleItem = ({ dataTestSubj, field, idAria, isDisabled }: Schedu
const [timeVal, setTimeVal] = useState<number>(0);
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
const onChangeTimeType = useCallback(e => {
setTimeType(e.target.value);
}, []);
const onChangeTimeType = useCallback(
e => {
setTimeType(e.target.value);
field.setValue(`${timeVal}${e.target.value}`);
},
[timeVal]
);
const onChangeTimeVal = useCallback(e => {
const sanitizedValue: number = parseInt(e.target.value, 10);
setTimeVal(isNaN(sanitizedValue) ? 0 : sanitizedValue);
}, []);
const onChangeTimeVal = useCallback(
e => {
const sanitizedValue: number = parseInt(e.target.value, 10);
setTimeVal(sanitizedValue);
field.setValue(`${sanitizedValue}${timeType}`);
},
[timeType]
);
useEffect(() => {
if (!isEmpty(timeVal) && Number(timeVal) >= 0 && field.value !== `${timeVal}${timeType}`) {
field.setValue(`${timeVal}${timeType}`);
}
}, [field.value, timeType, timeVal]);
useEffect(() => {
if (!isEmpty(field.value)) {
if (field.value !== `${timeVal}${timeType}`) {
const filterTimeVal = (field.value as string).match(/\d+/g);
const filterTimeType = (field.value as string).match(/[a-zA-Z]+/g);
if (

View file

@ -10,7 +10,9 @@ export {
FieldHook,
FIELD_TYPES,
Form,
FormData,
FormDataProvider,
FormHook,
FormSchema,
UseField,
useForm,

View file

@ -6,7 +6,7 @@
import { AboutStepRule } from '../../types';
export const defaultValue: AboutStepRule = {
export const stepAboutDefaultValue: AboutStepRule = {
name: '',
description: '',
isNew: true,

View file

@ -0,0 +1,214 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiButton, EuiHorizontalRule, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { isEqual, get } from 'lodash/fp';
import React, { memo, useCallback, useEffect, useState } from 'react';
import { RuleStepProps, RuleStep, AboutStepRule } from '../../types';
import * as RuleI18n from '../../translations';
import { Field, Form, FormDataProvider, getUseField, UseField, useForm } from '../shared_imports';
import { AddItem } from '../add_item_form';
import { defaultRiskScoreBySeverity, severityOptions, SeverityValue } from './data';
import { stepAboutDefaultValue } from './default_value';
import { schema } from './schema';
import * as I18n from './translations';
import { StepRuleDescription } from '../description_step';
import { AddMitreThreat } from '../mitre';
const CommonUseField = getUseField({ component: Field });
interface StepAboutRuleProps extends RuleStepProps {
defaultValues?: AboutStepRule | null;
}
export const StepAboutRule = memo<StepAboutRuleProps>(
({
defaultValues,
descriptionDirection = 'row',
isReadOnlyView,
isUpdateView = false,
isLoading,
setForm,
setStepData,
}) => {
const [myStepData, setMyStepData] = useState<AboutStepRule>(stepAboutDefaultValue);
const { form } = useForm({
defaultValue: myStepData,
options: { stripEmptyFields: false },
schema,
});
const onSubmit = useCallback(async () => {
if (setStepData) {
setStepData(RuleStep.aboutRule, null, false);
const { isValid, data } = await form.submit();
if (isValid) {
setStepData(RuleStep.aboutRule, data, isValid);
setMyStepData({ ...data, isNew: false } as AboutStepRule);
}
}
}, [form]);
useEffect(() => {
const { isNew, ...initDefaultValue } = myStepData;
if (defaultValues != null && !isEqual(initDefaultValue, defaultValues)) {
const myDefaultValues = {
...defaultValues,
isNew: false,
};
setMyStepData(myDefaultValues);
if (!isReadOnlyView) {
Object.keys(schema).forEach(key => {
const val = get(key, myDefaultValues);
if (val != null) {
form.setFieldValue(key, val);
}
});
}
}
}, [defaultValues]);
useEffect(() => {
if (setForm != null) {
setForm(RuleStep.aboutRule, form);
}
}, [form]);
return isReadOnlyView && myStepData != null ? (
<StepRuleDescription direction={descriptionDirection} schema={schema} data={myStepData} />
) : (
<>
<Form form={form} data-test-subj="stepAboutRule">
<CommonUseField
path="name"
componentProps={{
idAria: 'detectionEngineStepAboutRuleName',
'data-test-subj': 'detectionEngineStepAboutRuleName',
euiFieldProps: {
compressed: true,
fullWidth: false,
disabled: isLoading,
},
}}
/>
<CommonUseField
path="description"
componentProps={{
compressed: true,
idAria: 'detectionEngineStepAboutRuleDescription',
'data-test-subj': 'detectionEngineStepAboutRuleDescription',
euiFieldProps: {
compressed: true,
disabled: isLoading,
},
}}
/>
<CommonUseField
path="severity"
componentProps={{
idAria: 'detectionEngineStepAboutRuleSeverity',
'data-test-subj': 'detectionEngineStepAboutRuleSeverity',
euiFieldProps: {
compressed: true,
fullWidth: false,
disabled: isLoading,
options: severityOptions,
},
}}
/>
<CommonUseField
path="riskScore"
componentProps={{
idAria: 'detectionEngineStepAboutRuleRiskScore',
'data-test-subj': 'detectionEngineStepAboutRuleRiskScore',
euiFieldProps: {
max: 100,
min: 0,
compressed: true,
fullWidth: false,
disabled: isLoading,
options: severityOptions,
},
}}
/>
<UseField
path="references"
component={AddItem}
componentProps={{
compressed: true,
addText: I18n.ADD_REFERENCE,
idAria: 'detectionEngineStepAboutRuleReferenceUrls',
isDisabled: isLoading,
dataTestSubj: 'detectionEngineStepAboutRuleReferenceUrls',
}}
/>
<UseField
path="falsePositives"
component={AddItem}
componentProps={{
compressed: true,
addText: I18n.ADD_FALSE_POSITIVE,
idAria: 'detectionEngineStepAboutRuleFalsePositives',
isDisabled: isLoading,
dataTestSubj: 'detectionEngineStepAboutRuleFalsePositives',
}}
/>
<UseField
path="threats"
component={AddMitreThreat}
componentProps={{
compressed: true,
idAria: 'detectionEngineStepAboutRuleMitreThreats',
isDisabled: isLoading,
dataTestSubj: 'detectionEngineStepAboutRuleMitreThreats',
}}
/>
<CommonUseField
path="tags"
componentProps={{
idAria: 'detectionEngineStepAboutRuleTags',
'data-test-subj': 'detectionEngineStepAboutRuleTags',
euiFieldProps: {
compressed: true,
fullWidth: true,
isDisabled: isLoading,
},
}}
/>
<FormDataProvider pathsToWatch="severity">
{({ severity }) => {
const newRiskScore = defaultRiskScoreBySeverity[severity as SeverityValue];
const riskScoreField = form.getFields().riskScore;
if (newRiskScore != null && riskScoreField.value !== newRiskScore) {
riskScoreField.setValue(newRiskScore);
}
return null;
}}
</FormDataProvider>
</Form>
{!isUpdateView && (
<>
<EuiHorizontalRule margin="m" />
<EuiFlexGroup
alignItems="center"
justifyContent="flexEnd"
gutterSize="xs"
responsive={false}
>
<EuiFlexItem grow={false}>
<EuiButton fill onClick={onSubmit} isDisabled={isLoading}>
{myStepData.isNew ? RuleI18n.CONTINUE : RuleI18n.UPDATE}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</>
)}
</>
);
}
);

View file

@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n';
import { isEmpty } from 'lodash/fp';
import React from 'react';
import * as CreateRuleI18n from '../../translations';
import * as RuleI18n from '../../translations';
import { IMitreEnterpriseAttack } from '../../types';
import {
FIELD_TYPES,
@ -99,7 +99,7 @@ export const schema: FormSchema = {
defaultMessage: 'Reference URLs',
}
),
labelAppend: <EuiText size="xs">{CreateRuleI18n.OPTIONAL_FIELD}</EuiText>,
labelAppend: <EuiText size="xs">{RuleI18n.OPTIONAL_FIELD}</EuiText>,
},
falsePositives: {
label: i18n.translate(
@ -108,7 +108,7 @@ export const schema: FormSchema = {
defaultMessage: 'False positives',
}
),
labelAppend: <EuiText size="xs">{CreateRuleI18n.OPTIONAL_FIELD}</EuiText>,
labelAppend: <EuiText size="xs">{RuleI18n.OPTIONAL_FIELD}</EuiText>,
},
threats: {
label: i18n.translate(
@ -117,7 +117,7 @@ export const schema: FormSchema = {
defaultMessage: 'MITRE ATT&CK',
}
),
labelAppend: <EuiText size="xs">{CreateRuleI18n.OPTIONAL_FIELD}</EuiText>,
labelAppend: <EuiText size="xs">{RuleI18n.OPTIONAL_FIELD}</EuiText>,
validations: [
{
validator: (
@ -146,6 +146,6 @@ export const schema: FormSchema = {
label: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTagsLabel', {
defaultMessage: 'Tags',
}),
labelAppend: <EuiText size="xs">{CreateRuleI18n.OPTIONAL_FIELD}</EuiText>,
labelAppend: <EuiText size="xs">{RuleI18n.OPTIONAL_FIELD}</EuiText>,
},
};

View file

@ -5,14 +5,14 @@
*/
import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
import { isEqual } from 'lodash/fp';
import React, { memo, useCallback, useState } from 'react';
import { isEqual, get } from 'lodash/fp';
import React, { memo, useCallback, useState, useEffect } from 'react';
import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/public';
import { useFetchIndexPatterns } from '../../../../../containers/detection_engine/rules/fetch_index_patterns';
import { useUiSetting$ } from '../../../../../../../../../../src/plugins/kibana_react/public';
import { useFetchIndexPatterns } from '../../../../../containers/detection_engine/rules';
import { DEFAULT_INDEX_KEY } from '../../../../../../common/constants';
import { useKibanaUiSetting } from '../../../../../lib/settings/use_kibana_ui_setting';
import * as CreateRuleI18n from '../../translations';
import * as RuleI18n from '../../translations';
import { DefineStepRule, RuleStep, RuleStepProps } from '../../types';
import { StepRuleDescription } from '../description_step';
import { QueryBarDefineRule } from '../query_bar';
@ -22,40 +22,102 @@ import * as I18n from './translations';
const CommonUseField = getUseField({ component: Field });
export const StepDefineRule = memo<RuleStepProps>(
({ isEditView, isLoading, resizeParentContainer, setStepData }) => {
interface StepDefineRuleProps extends RuleStepProps {
defaultValues?: DefineStepRule | null;
}
const stepDefineDefaultValue = {
index: [],
isNew: true,
queryBar: {
query: { query: '', language: 'kuery' },
filters: [],
saved_id: null,
},
useIndicesConfig: 'true',
};
const getStepDefaultValue = (
indicesConfig: string[],
defaultValues: DefineStepRule | null
): DefineStepRule => {
if (defaultValues != null) {
return {
...defaultValues,
isNew: false,
useIndicesConfig: `${isEqual(defaultValues.index, indicesConfig)}`,
};
} else {
return {
...stepDefineDefaultValue,
index: indicesConfig != null ? indicesConfig : [],
};
}
};
export const StepDefineRule = memo<StepDefineRuleProps>(
({
defaultValues,
descriptionDirection = 'row',
isReadOnlyView,
isLoading,
isUpdateView = false,
resizeParentContainer,
setForm,
setStepData,
}) => {
const [localUseIndicesConfig, setLocalUseIndicesConfig] = useState('');
const [indicesConfig] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY);
const [
{ indexPatterns: indexPatternQueryBar, isLoading: indexPatternLoadingQueryBar },
setIndices,
] = useFetchIndexPatterns();
const [indicesConfig] = useKibanaUiSetting(DEFAULT_INDEX_KEY);
const [myStepData, setMyStepData] = useState<DefineStepRule>({
index: indicesConfig || [],
isNew: true,
queryBar: {
query: { query: '', language: 'kuery' },
filters: [],
saved_id: null,
},
useIndicesConfig: 'true',
});
] = useFetchIndexPatterns(defaultValues != null ? defaultValues.index : indicesConfig ?? []);
const [myStepData, setMyStepData] = useState<DefineStepRule>(stepDefineDefaultValue);
const { form } = useForm({
schema,
defaultValue: myStepData,
options: { stripEmptyFields: false },
schema,
});
const onSubmit = useCallback(async () => {
const { isValid, data } = await form.submit();
if (isValid) {
setStepData(RuleStep.defineRule, data, isValid);
setMyStepData({ ...data, isNew: false } as DefineStepRule);
if (setStepData) {
setStepData(RuleStep.defineRule, null, false);
const { isValid, data } = await form.submit();
if (isValid && setStepData) {
setStepData(RuleStep.defineRule, data, isValid);
setMyStepData({ ...data, isNew: false } as DefineStepRule);
}
}
}, [form]);
return isEditView && myStepData != null ? (
useEffect(() => {
if (indicesConfig != null && defaultValues != null) {
const myDefaultValues = getStepDefaultValue(indicesConfig, defaultValues);
if (!isEqual(myDefaultValues, myStepData)) {
setMyStepData(myDefaultValues);
setLocalUseIndicesConfig(myDefaultValues.useIndicesConfig);
if (!isReadOnlyView) {
Object.keys(schema).forEach(key => {
const val = get(key, myDefaultValues);
if (val != null) {
form.setFieldValue(key, val);
}
});
}
}
}
}, [defaultValues, indicesConfig]);
useEffect(() => {
if (setForm != null) {
setForm(RuleStep.defineRule, form);
}
}, [form]);
return isReadOnlyView && myStepData != null ? (
<StepRuleDescription
direction={descriptionDirection}
indexPatterns={indexPatternQueryBar as IIndexPattern}
schema={schema}
data={myStepData}
@ -135,19 +197,23 @@ export const StepDefineRule = memo<RuleStepProps>(
}}
</FormDataProvider>
</Form>
<EuiHorizontalRule margin="m" />
<EuiFlexGroup
alignItems="center"
justifyContent="flexEnd"
gutterSize="xs"
responsive={false}
>
<EuiFlexItem grow={false}>
<EuiButton fill onClick={onSubmit} isDisabled={isLoading}>
{myStepData.isNew ? CreateRuleI18n.CONTINUE : CreateRuleI18n.UPDATE}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
{!isUpdateView && (
<>
<EuiHorizontalRule margin="m" />
<EuiFlexGroup
alignItems="center"
justifyContent="flexEnd"
gutterSize="xs"
responsive={false}
>
<EuiFlexItem grow={false}>
<EuiButton fill onClick={onSubmit} isDisabled={isLoading}>
{myStepData.isNew ? RuleI18n.CONTINUE : RuleI18n.UPDATE}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</>
)}
</>
);
}

View file

@ -10,9 +10,7 @@ import { isEmpty } from 'lodash/fp';
import React from 'react';
import { esKuery } from '../../../../../../../../../../src/plugins/data/public';
import * as CreateRuleI18n from '../../translations';
import * as RuleI18n from '../../translations';
import { FieldValueQueryBar } from '../query_bar';
import {
ERROR_CODE,
@ -40,7 +38,7 @@ export const schema: FormSchema = {
label: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.fiedIndicesLabel', {
defaultMessage: 'Indices',
}),
labelAppend: <EuiText size="xs">{CreateRuleI18n.OPTIONAL_FIELD}</EuiText>,
labelAppend: <EuiText size="xs">{RuleI18n.OPTIONAL_FIELD}</EuiText>,
validations: [
{
validator: emptyField(

View file

@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiPanel, EuiProgress } from '@elastic/eui';
import React, { memo } from 'react';
import styled from 'styled-components';
import { HeaderSection } from '../../../../../components/header_section';
interface StepPanelProps {
children: React.ReactNode;
loading: boolean;
title: string;
}
const MyPanel = styled(EuiPanel)`
poistion: relative;
`;
export const StepPanel = memo<StepPanelProps>(({ children, loading, title }) => {
return (
<MyPanel>
{loading && <EuiProgress size="xs" color="accent" position="absolute" />}
<HeaderSection title={title} />
{children}
</MyPanel>
);
});

View file

@ -0,0 +1,148 @@
/*
* 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 { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
import { isEqual, get } from 'lodash/fp';
import React, { memo, useCallback, useEffect, useState } from 'react';
import { RuleStep, RuleStepProps, ScheduleStepRule } from '../../types';
import { StepRuleDescription } from '../description_step';
import { ScheduleItem } from '../schedule_item_form';
import { Form, UseField, useForm } from '../shared_imports';
import { schema } from './schema';
import * as I18n from './translations';
interface StepScheduleRuleProps extends RuleStepProps {
defaultValues?: ScheduleStepRule | null;
}
const stepScheduleDefaultValue = {
enabled: true,
interval: '5m',
isNew: true,
from: '0m',
};
export const StepScheduleRule = memo<StepScheduleRuleProps>(
({
defaultValues,
descriptionDirection = 'row',
isReadOnlyView,
isLoading,
isUpdateView = false,
setStepData,
setForm,
}) => {
const [myStepData, setMyStepData] = useState<ScheduleStepRule>(stepScheduleDefaultValue);
const { form } = useForm({
defaultValue: myStepData,
options: { stripEmptyFields: false },
schema,
});
const onSubmit = useCallback(
async (enabled: boolean) => {
if (setStepData) {
setStepData(RuleStep.scheduleRule, null, false);
const { isValid: newIsValid, data } = await form.submit();
if (newIsValid) {
setStepData(RuleStep.scheduleRule, { ...data, enabled }, newIsValid);
setMyStepData({ ...data, isNew: false } as ScheduleStepRule);
}
}
},
[form]
);
useEffect(() => {
const { isNew, ...initDefaultValue } = myStepData;
if (defaultValues != null && !isEqual(initDefaultValue, defaultValues)) {
const myDefaultValues = {
...defaultValues,
isNew: false,
};
setMyStepData(myDefaultValues);
if (!isReadOnlyView) {
Object.keys(schema).forEach(key => {
const val = get(key, myDefaultValues);
if (val != null) {
form.setFieldValue(key, val);
}
});
}
}
}, [defaultValues]);
useEffect(() => {
if (setForm != null) {
setForm(RuleStep.scheduleRule, form);
}
}, [form]);
return isReadOnlyView && myStepData != null ? (
<StepRuleDescription direction={descriptionDirection} schema={schema} data={myStepData} />
) : (
<>
<Form form={form} data-test-subj="stepScheduleRule">
<UseField
path="interval"
component={ScheduleItem}
componentProps={{
compressed: true,
idAria: 'detectionEngineStepScheduleRuleInterval',
isDisabled: isLoading,
dataTestSubj: 'detectionEngineStepScheduleRuleInterval',
}}
/>
<UseField
path="from"
component={ScheduleItem}
componentProps={{
compressed: true,
idAria: 'detectionEngineStepScheduleRuleFrom',
isDisabled: isLoading,
dataTestSubj: 'detectionEngineStepScheduleRuleFrom',
}}
/>
</Form>
{!isUpdateView && (
<>
<EuiHorizontalRule margin="s" />
<EuiFlexGroup
alignItems="center"
justifyContent="flexEnd"
gutterSize="xs"
responsive={false}
>
<EuiFlexItem grow={false}>
<EuiButton
fill={false}
isDisabled={isLoading}
isLoading={isLoading}
onClick={onSubmit.bind(null, false)}
>
{I18n.COMPLETE_WITHOUT_ACTIVATING}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
fill
isDisabled={isLoading}
isLoading={isLoading}
onClick={onSubmit.bind(null, true)}
>
{I18n.COMPLETE_WITH_ACTIVATING}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</>
)}
</>
);
}
);

View file

@ -8,7 +8,7 @@ import { EuiText } from '@elastic/eui';
import React from 'react';
import { i18n } from '@kbn/i18n';
import * as CreateRuleI18n from '../../translations';
import * as RuleI18n from '../../translations';
import { FormSchema } from '../shared_imports';
export const schema: FormSchema = {
@ -33,7 +33,7 @@ export const schema: FormSchema = {
defaultMessage: 'Additional look-back',
}
),
labelAppend: <EuiText size="xs">{CreateRuleI18n.OPTIONAL_FIELD}</EuiText>,
labelAppend: <EuiText size="xs">{RuleI18n.OPTIONAL_FIELD}</EuiText>,
helpText: i18n.translate(
'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldAdditionalLookBackHelpText',
{

View file

@ -7,7 +7,7 @@
import { isEmpty } from 'lodash/fp';
import moment from 'moment';
import { NewRule } from '../../../containers/detection_engine/rules/types';
import { NewRule } from '../../../../containers/detection_engine/rules';
import {
AboutStepRule,
@ -17,7 +17,7 @@ import {
ScheduleStepRuleJson,
AboutStepRuleJson,
FormatRuleType,
} from './types';
} from '../types';
const getTimeTypeValue = (time: string): { unit: string; value: number } => {
const timeObj = {
@ -40,7 +40,7 @@ const getTimeTypeValue = (time: string): { unit: string; value: number } => {
};
const formatDefineStepData = (defineStepData: DefineStepRule): DefineStepRuleJson => {
const { queryBar, useIndicesConfig, ...rest } = defineStepData;
const { queryBar, useIndicesConfig, isNew, ...rest } = defineStepData;
const { filters, query, saved_id: savedId } = queryBar;
return {
...rest,
@ -52,8 +52,7 @@ const formatDefineStepData = (defineStepData: DefineStepRule): DefineStepRuleJso
};
const formatScheduleStepData = (scheduleData: ScheduleStepRule): ScheduleStepRuleJson => {
const formatScheduleData = scheduleData;
const { isNew, ...formatScheduleData } = scheduleData;
if (!isEmpty(formatScheduleData.interval) && !isEmpty(formatScheduleData.from)) {
const { unit: intervalUnit, value: intervalValue } = getTimeTypeValue(
formatScheduleData.interval
@ -64,12 +63,16 @@ const formatScheduleStepData = (scheduleData: ScheduleStepRule): ScheduleStepRul
formatScheduleData.from = `now-${duration.asSeconds()}s`;
formatScheduleData.to = 'now';
}
return formatScheduleData;
return {
...formatScheduleData,
meta: {
from: scheduleData.from,
},
};
};
const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRuleJson => {
const { falsePositives, references, riskScore, threats, ...rest } = aboutStepData;
const { falsePositives, references, riskScore, threats, isNew, ...rest } = aboutStepData;
return {
false_positives: falsePositives.filter(item => !isEmpty(item)),
references: references.filter(item => !isEmpty(item)),
@ -91,7 +94,8 @@ const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRuleJson =>
export const formatRule = (
defineStepData: DefineStepRule,
aboutStepData: AboutStepRule,
scheduleData: ScheduleStepRule
scheduleData: ScheduleStepRule,
ruleId?: string
): NewRule => {
const type: FormatRuleType = defineStepData.queryBar.saved_id != null ? 'saved_query' : 'query';
const persistData = {
@ -99,10 +103,6 @@ export const formatRule = (
...formatDefineStepData(defineStepData),
...formatAboutStepData(aboutStepData),
...formatScheduleStepData(scheduleData),
meta: {
from: scheduleData.from,
},
};
return persistData;
return ruleId != null ? { id: ruleId, ...persistData } : persistData;
};

View file

@ -9,19 +9,19 @@ import React, { useCallback, useRef, useState } from 'react';
import { Redirect } from 'react-router-dom';
import styled from 'styled-components';
import { HeaderPage } from '../../../components/header_page';
import { WrapperPage } from '../../../components/wrapper_page';
import { AccordionTitle } from './components/accordion_title';
import { StepAboutRule } from './components/step_about_rule';
import { StepDefineRule } from './components/step_define_rule';
import { StepScheduleRule } from './components/step_schedule_rule';
import { usePersistRule } from '../../../containers/detection_engine/rules/persist_rule';
import { SpyRoute } from '../../../utils/route/spy_routes';
import { HeaderPage } from '../../../../components/header_page';
import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine';
import { WrapperPage } from '../../../../components/wrapper_page';
import { AccordionTitle } from '../components/accordion_title';
import { StepAboutRule } from '../components/step_about_rule';
import { StepDefineRule } from '../components/step_define_rule';
import { StepScheduleRule } from '../components/step_schedule_rule';
import { usePersistRule } from '../../../../containers/detection_engine/rules';
import { SpyRoute } from '../../../../utils/route/spy_routes';
import * as RuleI18n from '../translations';
import { AboutStepRule, DefineStepRule, RuleStep, RuleStepData, ScheduleStepRule } from '../types';
import { formatRule } from './helpers';
import * as i18n from './translations';
import { AboutStepRule, DefineStepRule, RuleStep, RuleStepData, ScheduleStepRule } from './types';
import { DETECTION_ENGINE_PAGE_NAME } from '../../../components/link_to/redirect_to_detection_engine';
const stepsRuleOrder = [RuleStep.defineRule, RuleStep.aboutRule, RuleStep.scheduleRule];
@ -44,41 +44,44 @@ export const CreateRuleComponent = React.memo(() => {
[RuleStep.aboutRule]: { isValid: false, data: {} },
[RuleStep.scheduleRule]: { isValid: false, data: {} },
});
const [isStepRuleInEditView, setIsStepRuleInEditView] = useState<Record<RuleStep, boolean>>({
const [isStepRuleInReadOnlyView, setIsStepRuleInEditView] = useState<Record<RuleStep, boolean>>({
[RuleStep.defineRule]: false,
[RuleStep.aboutRule]: false,
[RuleStep.scheduleRule]: false,
});
const [{ isLoading, isSaved }, setRule] = usePersistRule();
const setStepData = (step: RuleStep, data: unknown, isValid: boolean) => {
stepsData.current[step] = { ...stepsData.current[step], data, isValid };
if (isValid) {
const stepRuleIdx = stepsRuleOrder.findIndex(item => step === item);
if ([0, 1].includes(stepRuleIdx)) {
setIsStepRuleInEditView({
...isStepRuleInEditView,
[step]: true,
});
if (openAccordionId !== stepsRuleOrder[stepRuleIdx + 1]) {
openCloseAccordion(stepsRuleOrder[stepRuleIdx + 1]);
setOpenAccordionId(stepsRuleOrder[stepRuleIdx + 1]);
const setStepData = useCallback(
(step: RuleStep, data: unknown, isValid: boolean) => {
stepsData.current[step] = { ...stepsData.current[step], data, isValid };
if (isValid) {
const stepRuleIdx = stepsRuleOrder.findIndex(item => step === item);
if ([0, 1].includes(stepRuleIdx)) {
setIsStepRuleInEditView({
...isStepRuleInReadOnlyView,
[step]: true,
});
if (openAccordionId !== stepsRuleOrder[stepRuleIdx + 1]) {
openCloseAccordion(stepsRuleOrder[stepRuleIdx + 1]);
setOpenAccordionId(stepsRuleOrder[stepRuleIdx + 1]);
}
} else if (
stepRuleIdx === 2 &&
stepsData.current[RuleStep.defineRule].isValid &&
stepsData.current[RuleStep.aboutRule].isValid
) {
setRule(
formatRule(
stepsData.current[RuleStep.defineRule].data as DefineStepRule,
stepsData.current[RuleStep.aboutRule].data as AboutStepRule,
stepsData.current[RuleStep.scheduleRule].data as ScheduleStepRule
)
);
}
} else if (
stepRuleIdx === 2 &&
stepsData.current[RuleStep.defineRule].isValid &&
stepsData.current[RuleStep.aboutRule].isValid
) {
setRule(
formatRule(
stepsData.current[RuleStep.defineRule].data as DefineStepRule,
stepsData.current[RuleStep.aboutRule].data as AboutStepRule,
stepsData.current[RuleStep.scheduleRule].data as ScheduleStepRule
)
);
}
}
};
},
[openAccordionId, stepsData.current, setRule]
);
const getAccordionType = useCallback(
(accordionId: RuleStep) => {
@ -95,19 +98,23 @@ export const CreateRuleComponent = React.memo(() => {
const defineRuleButton = (
<AccordionTitle
name="1"
title={i18n.DEFINE_RULE}
title={RuleI18n.DEFINE_RULE}
type={getAccordionType(RuleStep.defineRule)}
/>
);
const aboutRuleButton = (
<AccordionTitle name="2" title={i18n.ABOUT_RULE} type={getAccordionType(RuleStep.aboutRule)} />
<AccordionTitle
name="2"
title={RuleI18n.ABOUT_RULE}
type={getAccordionType(RuleStep.aboutRule)}
/>
);
const scheduleRuleButton = (
<AccordionTitle
name="3"
title={i18n.SCHEDULE_RULE}
title={RuleI18n.SCHEDULE_RULE}
type={getAccordionType(RuleStep.scheduleRule)}
/>
);
@ -142,7 +149,7 @@ export const CreateRuleComponent = React.memo(() => {
openAccordionId != null &&
openAccordionId !== id &&
!stepsData.current[openAccordionId].isValid &&
!isStepRuleInEditView[id] &&
!isStepRuleInReadOnlyView[id] &&
isOpen
) {
openCloseAccordion(id);
@ -153,20 +160,20 @@ export const CreateRuleComponent = React.memo(() => {
}
}
},
[isStepRuleInEditView, openAccordionId]
[isStepRuleInReadOnlyView, openAccordionId]
);
const manageIsEditable = useCallback(
(id: RuleStep) => {
setIsStepRuleInEditView({
...isStepRuleInEditView,
...isStepRuleInReadOnlyView,
[id]: false,
});
},
[isStepRuleInEditView]
[isStepRuleInReadOnlyView]
);
if (isSaved && stepsData.current[RuleStep.scheduleRule].isValid) {
if (isSaved) {
return <Redirect to={`/${DETECTION_ENGINE_PAGE_NAME}/rules`} />;
}
@ -201,7 +208,7 @@ export const CreateRuleComponent = React.memo(() => {
>
<EuiHorizontalRule margin="xs" />
<StepDefineRule
isEditView={isStepRuleInEditView[RuleStep.defineRule]}
isReadOnlyView={isStepRuleInReadOnlyView[RuleStep.defineRule]}
isLoading={isLoading}
setStepData={setStepData}
resizeParentContainer={height => setHeightAccordion(height)}
@ -231,7 +238,7 @@ export const CreateRuleComponent = React.memo(() => {
>
<EuiHorizontalRule margin="xs" />
<StepAboutRule
isEditView={isStepRuleInEditView[RuleStep.aboutRule]}
isReadOnlyView={isStepRuleInReadOnlyView[RuleStep.aboutRule]}
isLoading={isLoading}
setStepData={setStepData}
/>
@ -260,7 +267,7 @@ export const CreateRuleComponent = React.memo(() => {
>
<EuiHorizontalRule margin="xs" />
<StepScheduleRule
isEditView={isStepRuleInEditView[RuleStep.scheduleRule]}
isReadOnlyView={isStepRuleInReadOnlyView[RuleStep.scheduleRule]}
isLoading={isLoading}
setStepData={setStepData}
/>

View file

@ -6,6 +6,6 @@
import { i18n } from '@kbn/i18n';
export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.ruleDetails.pageTitle', {
defaultMessage: 'Rule details',
export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.createRule.pageTitle', {
defaultMessage: 'Create new rule',
});

View file

@ -0,0 +1,223 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiButton, EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { memo, useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { StickyContainer } from 'react-sticky';
import { FiltersGlobal } from '../../../../components/filters_global';
import { FormattedDate } from '../../../../components/formatted_date';
import { HeaderPage } from '../../../../components/header_page';
import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine';
import { SiemSearchBar } from '../../../../components/search_bar';
import { WrapperPage } from '../../../../components/wrapper_page';
import { useRule } from '../../../../containers/detection_engine/rules';
import {
indicesExistOrDataTemporarilyUnavailable,
WithSource,
} from '../../../../containers/source';
import { SpyRoute } from '../../../../utils/route/spy_routes';
import { SignalsCharts } from '../../components/signals_chart';
import { SignalsTable } from '../../components/signals';
import { DetectionEngineEmptyPage } from '../../detection_engine_empty_page';
import { useSignalInfo } from '../../components/signals_info';
import { StepAboutRule } from '../components/step_about_rule';
import { StepDefineRule } from '../components/step_define_rule';
import { StepScheduleRule } from '../components/step_schedule_rule';
import { buildSignalsRuleIdFilter } from '../../components/signals/default_config';
import * as detectionI18n from '../../translations';
import { RuleSwitch } from '../components/rule_switch';
import { StepPanel } from '../components/step_panel';
import { getStepsData } from '../helpers';
import * as ruleI18n from '../translations';
import * as i18n from './translations';
import { GlobalTime } from '../../../../containers/global_time';
export const RuleDetailsComponent = memo(() => {
const { ruleId } = useParams();
const [loading, rule] = useRule(ruleId);
const { aboutRuleData, defineRuleData, scheduleRuleData } = getStepsData({
rule,
detailsView: true,
});
const [lastSignals] = useSignalInfo({ ruleId });
const title = loading === true || rule === null ? <EuiLoadingSpinner size="m" /> : rule.name;
const subTitle = useMemo(
() =>
loading === true || rule === null ? (
<EuiLoadingSpinner size="m" />
) : (
[
<FormattedMessage
id="xpack.siem.detectionEngine.ruleDetails.ruleCreationDescription"
defaultMessage="Created by: {by} on {date}"
values={{
by: rule?.created_by ?? i18n.UNKNOWN,
date: (
<FormattedDate
value={rule?.created_at ?? new Date().toISOString()}
fieldName="createdAt"
/>
),
}}
/>,
rule?.updated_by != null ? (
<FormattedMessage
id="xpack.siem.detectionEngine.ruleDetails.ruleUpdateDescription"
defaultMessage="Updated by: {by} on {date}"
values={{
by: rule?.updated_by ?? i18n.UNKNOWN,
date: (
<FormattedDate
value={rule?.updated_at ?? new Date().toISOString()}
fieldName="updatedAt"
/>
),
}}
/>
) : (
''
),
]
),
[loading, rule]
);
const signalDefaultFilters = useMemo(
() => (ruleId != null ? buildSignalsRuleIdFilter(ruleId) : []),
[ruleId]
);
return (
<>
<WithSource sourceId="default">
{({ indicesExist, indexPattern }) => {
return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? (
<GlobalTime>
{({ to, from }) => (
<StickyContainer>
<FiltersGlobal>
<SiemSearchBar id="global" indexPattern={indexPattern} />
</FiltersGlobal>
<WrapperPage>
<HeaderPage
backOptions={{ href: '#detection-engine/rules', text: i18n.BACK_TO_RULES }}
badgeOptions={{ text: i18n.EXPERIMENTAL }}
border
subtitle={subTitle}
subtitle2={[
lastSignals != null ? (
<>
{detectionI18n.LAST_SIGNAL}
{': '}
{lastSignals}
</>
) : null,
'Status: Comming Soon',
]}
title={title}
>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<RuleSwitch
id={rule?.id ?? '-1'}
enabled={rule?.enabled ?? false}
optionLabel={i18n.ACTIVATE_RULE}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
<EuiFlexItem grow={false}>
<EuiButton
href={`#${DETECTION_ENGINE_PAGE_NAME}/rules/${ruleId}/edit`}
iconType="visControls"
isDisabled={rule?.immutable ?? true}
>
{ruleI18n.EDIT_RULE_SETTINGS}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</HeaderPage>
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem component="section" grow={1}>
<StepPanel loading={loading} title={ruleI18n.DEFINITION}>
{defineRuleData != null && (
<StepDefineRule
descriptionDirection="column"
isReadOnlyView={true}
isLoading={false}
defaultValues={defineRuleData}
/>
)}
</StepPanel>
</EuiFlexItem>
<EuiFlexItem component="section" grow={2}>
<StepPanel loading={loading} title={ruleI18n.ABOUT}>
{aboutRuleData != null && (
<StepAboutRule
descriptionDirection="column"
isReadOnlyView={true}
isLoading={false}
defaultValues={aboutRuleData}
/>
)}
</StepPanel>
</EuiFlexItem>
<EuiFlexItem component="section" grow={1}>
<StepPanel loading={loading} title={ruleI18n.SCHEDULE}>
{scheduleRuleData != null && (
<StepScheduleRule
descriptionDirection="column"
isReadOnlyView={true}
isLoading={false}
defaultValues={scheduleRuleData}
/>
)}
</StepPanel>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<SignalsCharts />
<EuiSpacer />
{ruleId != null && (
<SignalsTable from={from} to={to} defaultFilters={signalDefaultFilters} />
)}
</WrapperPage>
</StickyContainer>
)}
</GlobalTime>
) : (
<WrapperPage>
<HeaderPage border title={i18n.PAGE_TITLE} />
<DetectionEngineEmptyPage />
</WrapperPage>
);
}}
</WithSource>
<SpyRoute />
</>
);
});
RuleDetailsComponent.displayName = 'RuleDetailsComponent';

View file

@ -0,0 +1,36 @@
/*
* 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 PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.ruleDetails.pageTitle', {
defaultMessage: 'Rule details',
});
export const BACK_TO_RULES = i18n.translate(
'xpack.siem.detectionEngine.ruleDetails.backToRulesDescription',
{
defaultMessage: 'Back to rules',
}
);
export const EXPERIMENTAL = i18n.translate(
'xpack.siem.detectionEngine.ruleDetails.experimentalDescription',
{
defaultMessage: 'Experimental',
}
);
export const ACTIVATE_RULE = i18n.translate(
'xpack.siem.detectionEngine.ruleDetails.activateRuleLabel',
{
defaultMessage: 'Activate',
}
);
export const UNKNOWN = i18n.translate('xpack.siem.detectionEngine.ruleDetails.unknownDescription', {
defaultMessage: 'Unknown',
});

View file

@ -0,0 +1,323 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
EuiButton,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiTabbedContent,
EuiTabbedContentTab,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Redirect, useParams } from 'react-router-dom';
import { HeaderPage } from '../../../../components/header_page';
import { WrapperPage } from '../../../../components/wrapper_page';
import { SpyRoute } from '../../../../utils/route/spy_routes';
import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine';
import { useRule, usePersistRule } from '../../../../containers/detection_engine/rules';
import { FormHook, FormData } from '../components/shared_imports';
import { StepPanel } from '../components/step_panel';
import { StepAboutRule } from '../components/step_about_rule';
import { StepDefineRule } from '../components/step_define_rule';
import { StepScheduleRule } from '../components/step_schedule_rule';
import { formatRule } from '../create/helpers';
import { getStepsData } from '../helpers';
import * as ruleI18n from '../translations';
import { RuleStep, DefineStepRule, AboutStepRule, ScheduleStepRule } from '../types';
import * as i18n from './translations';
interface StepRuleForm {
isValid: boolean;
}
interface AboutStepRuleForm extends StepRuleForm {
data: AboutStepRule | null;
}
interface DefineStepRuleForm extends StepRuleForm {
data: DefineStepRule | null;
}
interface ScheduleStepRuleForm extends StepRuleForm {
data: ScheduleStepRule | null;
}
export const EditRuleComponent = memo(() => {
const { ruleId } = useParams();
const [loading, rule] = useRule(ruleId);
const [initForm, setInitForm] = useState(false);
const [myAboutRuleForm, setMyAboutRuleForm] = useState<AboutStepRuleForm>({
data: null,
isValid: false,
});
const [myDefineRuleForm, setMyDefineRuleForm] = useState<DefineStepRuleForm>({
data: null,
isValid: false,
});
const [myScheduleRuleForm, setMyScheduleRuleForm] = useState<ScheduleStepRuleForm>({
data: null,
isValid: false,
});
const [selectedTab, setSelectedTab] = useState<EuiTabbedContentTab>();
const stepsForm = useRef<Record<RuleStep, FormHook<FormData> | null>>({
[RuleStep.defineRule]: null,
[RuleStep.aboutRule]: null,
[RuleStep.scheduleRule]: null,
});
const [{ isLoading, isSaved }, setRule] = usePersistRule();
const [tabHasError, setTabHasError] = useState<RuleStep[]>([]);
const setStepsForm = useCallback(
(step: RuleStep, form: FormHook<FormData>) => {
stepsForm.current[step] = form;
if (initForm && step === (selectedTab?.id as RuleStep) && form.isSubmitted === false) {
setInitForm(false);
form.submit();
}
},
[initForm, selectedTab]
);
const tabs = useMemo(
() => [
{
id: RuleStep.defineRule,
name: ruleI18n.DEFINITION,
content: (
<>
<EuiSpacer />
<StepPanel loading={loading} title={ruleI18n.DEFINITION}>
{myDefineRuleForm.data != null && (
<StepDefineRule
isReadOnlyView={false}
isLoading={isLoading}
isUpdateView
defaultValues={myDefineRuleForm.data}
setForm={setStepsForm}
/>
)}
<EuiSpacer />
</StepPanel>
</>
),
},
{
id: RuleStep.aboutRule,
name: ruleI18n.ABOUT,
content: (
<>
<EuiSpacer />
<StepPanel loading={loading} title={ruleI18n.ABOUT}>
{myAboutRuleForm.data != null && (
<StepAboutRule
isReadOnlyView={false}
isLoading={isLoading}
isUpdateView
defaultValues={myAboutRuleForm.data}
setForm={setStepsForm}
/>
)}
<EuiSpacer />
</StepPanel>
</>
),
},
{
id: RuleStep.scheduleRule,
name: ruleI18n.SCHEDULE,
content: (
<>
<EuiSpacer />
<StepPanel loading={loading} title={ruleI18n.SCHEDULE}>
{myScheduleRuleForm.data != null && (
<StepScheduleRule
isReadOnlyView={false}
isLoading={isLoading}
isUpdateView
defaultValues={myScheduleRuleForm.data}
setForm={setStepsForm}
/>
)}
<EuiSpacer />
</StepPanel>
</>
),
},
],
[
loading,
isLoading,
myAboutRuleForm,
myDefineRuleForm,
myScheduleRuleForm,
setStepsForm,
stepsForm,
]
);
const onSubmit = useCallback(async () => {
const activeFormId = selectedTab?.id as RuleStep;
const activeForm = await stepsForm.current[activeFormId]?.submit();
const invalidForms = [RuleStep.aboutRule, RuleStep.defineRule, RuleStep.scheduleRule].reduce<
RuleStep[]
>((acc, step) => {
if (
(step === activeFormId && activeForm != null && !activeForm?.isValid) ||
(step === RuleStep.aboutRule && !myAboutRuleForm.isValid) ||
(step === RuleStep.defineRule && !myDefineRuleForm.isValid) ||
(step === RuleStep.scheduleRule && !myScheduleRuleForm.isValid)
) {
return [...acc, step];
}
return acc;
}, []);
if (invalidForms.length === 0 && activeForm != null) {
setTabHasError([]);
setRule(
formatRule(
(activeFormId === RuleStep.defineRule
? activeForm.data
: myDefineRuleForm.data) as DefineStepRule,
(activeFormId === RuleStep.aboutRule
? activeForm.data
: myAboutRuleForm.data) as AboutStepRule,
(activeFormId === RuleStep.scheduleRule
? activeForm.data
: myScheduleRuleForm.data) as ScheduleStepRule,
ruleId
)
);
} else {
setTabHasError(invalidForms);
}
}, [stepsForm, myAboutRuleForm, myDefineRuleForm, myScheduleRuleForm, selectedTab, ruleId]);
useEffect(() => {
if (rule != null) {
const { aboutRuleData, defineRuleData, scheduleRuleData } = getStepsData({ rule });
setMyAboutRuleForm({ data: aboutRuleData, isValid: true });
setMyDefineRuleForm({ data: defineRuleData, isValid: true });
setMyScheduleRuleForm({ data: scheduleRuleData, isValid: true });
}
}, [rule]);
const onTabClick = useCallback(
async (tab: EuiTabbedContentTab) => {
if (selectedTab != null) {
const ruleStep = selectedTab.id as RuleStep;
const respForm = await stepsForm.current[ruleStep]?.submit();
if (respForm != null) {
if (ruleStep === RuleStep.aboutRule) {
setMyAboutRuleForm({
data: respForm.data as AboutStepRule,
isValid: respForm.isValid,
});
} else if (ruleStep === RuleStep.defineRule) {
setMyDefineRuleForm({
data: respForm.data as DefineStepRule,
isValid: respForm.isValid,
});
} else if (ruleStep === RuleStep.scheduleRule) {
setMyScheduleRuleForm({
data: respForm.data as ScheduleStepRule,
isValid: respForm.isValid,
});
}
}
}
setInitForm(true);
setSelectedTab(tab);
},
[selectedTab, stepsForm.current]
);
useEffect(() => {
if (rule != null) {
const { aboutRuleData, defineRuleData, scheduleRuleData } = getStepsData({ rule });
setMyAboutRuleForm({ data: aboutRuleData, isValid: true });
setMyDefineRuleForm({ data: defineRuleData, isValid: true });
setMyScheduleRuleForm({ data: scheduleRuleData, isValid: true });
}
}, [rule]);
useEffect(() => {
setSelectedTab(tabs[0]);
}, []);
if (isSaved || (rule != null && rule.immutable)) {
return <Redirect to={`/${DETECTION_ENGINE_PAGE_NAME}/rules/${ruleId}`} />;
}
return (
<>
<WrapperPage restrictWidth>
<HeaderPage
backOptions={{
href: `#/${DETECTION_ENGINE_PAGE_NAME}/rules/${ruleId}`,
text: `${i18n.BACK_TO} ${rule?.name ?? ''}`,
}}
isLoading={isLoading}
title={i18n.PAGE_TITLE}
/>
{tabHasError.length > 0 && (
<EuiCallOut title={i18n.SORRY_ERRORS} color="danger" iconType="alert">
<FormattedMessage
id="xpack.siem.detectionEngine.rule.editRule.errorMsgDescription"
defaultMessage="You have an invalid input in {countError, plural, one {this tab} other {these tabs}}: {tabHasError}"
values={{
countError: tabHasError.length,
tabHasError: tabHasError
.map(t => {
if (t === RuleStep.aboutRule) {
return ruleI18n.ABOUT;
} else if (t === RuleStep.defineRule) {
return ruleI18n.DEFINITION;
} else if (t === RuleStep.scheduleRule) {
return ruleI18n.SCHEDULE;
}
return t;
})
.join(', '),
}}
/>
</EuiCallOut>
)}
<EuiTabbedContent
initialSelectedTab={tabs[0]}
selectedTab={tabs.find(t => t.id === selectedTab?.id)}
onTabClick={onTabClick}
tabs={tabs}
/>
<EuiSpacer />
<EuiFlexGroup
alignItems="center"
gutterSize="s"
justifyContent="flexEnd"
responsive={false}
>
<EuiFlexItem grow={false}>
<EuiButton iconType="cross" href={`#/${DETECTION_ENGINE_PAGE_NAME}/rules/${ruleId}`}>
{i18n.CANCEL}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton fill onClick={onSubmit} iconType="save" isLoading={isLoading}>
{i18n.SAVE_CHANGES}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</WrapperPage>
<SpyRoute />
</>
);
});
EditRuleComponent.displayName = 'EditRuleComponent';

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.editRule.pageTitle', {
defaultMessage: 'Edit rule settings',
});
export const CANCEL = i18n.translate('xpack.siem.detectionEngine.editRule.cancelTitle', {
defaultMessage: 'Cancel',
});
export const SAVE_CHANGES = i18n.translate('xpack.siem.detectionEngine.editRule.saveChangeTitle', {
defaultMessage: 'Save changes',
});
export const SORRY_ERRORS = i18n.translate(
'xpack.siem.detectionEngine.editRule.errorMsgDescription',
{
defaultMessage: 'Sorry',
}
);
export const BACK_TO = i18n.translate('xpack.siem.detectionEngine.editRule.backToDescription', {
defaultMessage: 'Back to',
});

View file

@ -0,0 +1,63 @@
/*
* 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 { pick } from 'lodash/fp';
import { esFilters } from '../../../../../../../../src/plugins/data/public';
import { Rule } from '../../../containers/detection_engine/rules';
import { AboutStepRule, DefineStepRule, IMitreEnterpriseAttack, ScheduleStepRule } from './types';
interface GetStepsData {
aboutRuleData: AboutStepRule | null;
defineRuleData: DefineStepRule | null;
scheduleRuleData: ScheduleStepRule | null;
}
export const getStepsData = ({
rule,
detailsView = false,
}: {
rule: Rule | null;
detailsView?: boolean;
}): GetStepsData => {
const defineRuleData: DefineStepRule | null =
rule != null
? {
isNew: false,
index: rule.index,
queryBar: {
query: { query: rule.query as string, language: rule.language },
filters: rule.filters as esFilters.Filter[],
saved_id: rule.saved_id ?? null,
},
useIndicesConfig: 'true',
}
: null;
const aboutRuleData: AboutStepRule | null =
rule != null
? {
isNew: false,
...pick(['description', 'name', 'references', 'severity', 'tags', 'threats'], rule),
...(detailsView ? { name: '' } : {}),
threats: rule.threats as IMitreEnterpriseAttack[],
falsePositives: rule.false_positives,
riskScore: rule.risk_score,
}
: null;
const scheduleRuleData: ScheduleStepRule | null =
rule != null
? {
isNew: false,
...pick(['enabled', 'interval'], rule),
from:
rule?.meta?.from != null
? rule.meta.from.replace('now-', '')
: rule.from.replace('now-', ''),
}
: null;
return { aboutRuleData, defineRuleData, scheduleRuleData };
};

View file

@ -4,20 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiTabbedContent } from '@elastic/eui';
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { HeaderPage } from '../../../components/header_page';
import { WrapperPage } from '../../../components/wrapper_page';
import { SpyRoute } from '../../../utils/route/spy_routes';
import * as i18n from './translations';
import { AllRules } from './all_rules';
import { ActivityMonitor } from './activity_monitor';
import { FormattedRelativePreferenceDate } from '../../../components/formatted_date';
import { getEmptyTagValue } from '../../../components/empty_value';
import { HeaderPage } from '../../../components/header_page';
import { WrapperPage } from '../../../components/wrapper_page';
import { SpyRoute } from '../../../utils/route/spy_routes';
import { AllRules } from './all';
import { ImportRuleModal } from './components/import_rule_modal';
import * as i18n from './translations';
export const RulesComponent = React.memo(() => {
const [showImportModal, setShowImportModal] = useState(false);
@ -62,27 +61,14 @@ export const RulesComponent = React.memo(() => {
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton fill href="#/detection-engine/rules/create-rule" iconType="plusInCircle">
<EuiButton fill href="#/detection-engine/rules/create" iconType="plusInCircle">
{i18n.ADD_NEW_RULE}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</HeaderPage>
<EuiTabbedContent
tabs={[
{
id: 'tabAllRules',
name: i18n.ALL_RULES,
content: <AllRules importCompleteToggle={importCompleteToggle} />,
},
{
id: 'tabActivityMonitor',
name: i18n.ACTIVITY_MONITOR,
content: <ActivityMonitor />,
},
]}
/>
<AllRules importCompleteToggle={importCompleteToggle} />
</WrapperPage>
<SpyRoute />

View file

@ -205,3 +205,46 @@ export const COLUMN_ACTIVATE = i18n.translate(
defaultMessage: 'Activate',
}
);
export const DEFINE_RULE = i18n.translate('xpack.siem.detectionEngine.rules.defineRuleTitle', {
defaultMessage: 'Define Rule',
});
export const ABOUT_RULE = i18n.translate('xpack.siem.detectionEngine.rules.aboutRuleTitle', {
defaultMessage: 'About Rule',
});
export const SCHEDULE_RULE = i18n.translate('xpack.siem.detectionEngine.rules.scheduleRuleTitle', {
defaultMessage: 'Schedule Rule',
});
export const DEFINITION = i18n.translate('xpack.siem.detectionEngine.rules.stepDefinitionTitle', {
defaultMessage: 'Definition',
});
export const ABOUT = i18n.translate('xpack.siem.detectionEngine.rules.stepAboutTitle', {
defaultMessage: 'About',
});
export const SCHEDULE = i18n.translate('xpack.siem.detectionEngine.rules.stepScheduleTitle', {
defaultMessage: 'Schedule',
});
export const OPTIONAL_FIELD = i18n.translate(
'xpack.siem.detectionEngine.rules.optionalFieldDescription',
{
defaultMessage: 'Optional',
}
);
export const CONTINUE = i18n.translate('xpack.siem.detectionEngine.rules.continueButtonTitle', {
defaultMessage: 'Continue',
});
export const UPDATE = i18n.translate('xpack.siem.detectionEngine.rules.updateButtonTitle', {
defaultMessage: 'Update',
});
export const DELETE = i18n.translate('xpack.siem.detectionEngine.rules.deleteDescription', {
defaultMessage: 'Delete',
});

View file

@ -4,7 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Rule } from '../../../containers/detection_engine/rules/types';
import { esFilters } from '../../../../../../../../src/plugins/data/common';
import { Rule } from '../../../containers/detection_engine/rules';
import { FieldValueQueryBar } from './components/query_bar';
import { FormData, FormHook } from './components/shared_imports';
export interface EuiBasicTableSortTypes {
field: string;
@ -39,3 +42,96 @@ export interface TableData {
isLoading: boolean;
sourceRule: Rule;
}
export enum RuleStep {
defineRule = 'define-rule',
aboutRule = 'about-rule',
scheduleRule = 'schedule-rule',
}
export type RuleStatusType = 'passive' | 'active' | 'valid';
export interface RuleStepData {
data: unknown;
isValid: boolean;
}
export interface RuleStepProps {
descriptionDirection?: 'row' | 'column';
setStepData?: (step: RuleStep, data: unknown, isValid: boolean) => void;
isReadOnlyView: boolean;
isUpdateView?: boolean;
isLoading: boolean;
resizeParentContainer?: (height: number) => void;
setForm?: (step: RuleStep, form: FormHook<FormData>) => void;
}
interface StepRuleData {
isNew: boolean;
}
export interface AboutStepRule extends StepRuleData {
name: string;
description: string;
severity: string;
riskScore: number;
references: string[];
falsePositives: string[];
tags: string[];
threats: IMitreEnterpriseAttack[];
}
export interface DefineStepRule extends StepRuleData {
useIndicesConfig: string;
index: string[];
queryBar: FieldValueQueryBar;
}
export interface ScheduleStepRule extends StepRuleData {
enabled: boolean;
interval: string;
from: string;
to?: string;
}
export interface DefineStepRuleJson {
index: string[];
filters: esFilters.Filter[];
saved_id?: string;
query: string;
language: string;
}
export interface AboutStepRuleJson {
name: string;
description: string;
severity: string;
risk_score: number;
references: string[];
false_positives: string[];
tags: string[];
threats: IMitreEnterpriseAttack[];
}
export interface ScheduleStepRuleJson {
enabled: boolean;
interval: string;
from: string;
to?: string;
meta?: unknown;
}
export type MyRule = Omit<DefineStepRule & ScheduleStepRule & AboutStepRule, 'isNew'> & {
immutable: boolean;
};
export type FormatRuleType = 'query' | 'saved_query';
export interface IMitreAttack {
id: string;
name: string;
reference: string;
}
export interface IMitreEnterpriseAttack {
framework: string;
tactic: IMitreAttack;
techniques: IMitreAttack[];
}

View file

@ -10,8 +10,16 @@ export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.pageTitle',
defaultMessage: 'Detection engine',
});
export const PAGE_SUBTITLE = i18n.translate('xpack.siem.detectionEngine.pageSubtitle', {
defaultMessage: 'Last signal: X minutes ago',
export const LAST_SIGNAL = i18n.translate('xpack.siem.detectionEngine.lastSignalTitle', {
defaultMessage: 'Last signal:',
});
export const TOTAL_SIGNAL = i18n.translate('xpack.siem.detectionEngine.totalSignalTitle', {
defaultMessage: 'Total',
});
export const SIGNAL = i18n.translate('xpack.siem.detectionEngine.signalTitle', {
defaultMessage: 'Signals',
});
export const BUTTON_MANAGE_RULES = i18n.translate('xpack.siem.detectionEngine.buttonManageRules', {

View file

@ -7,12 +7,15 @@
import { querySignalsSchema } from './query_signals_index_schema';
import { SignalsQueryRestParams } from '../../signals/types';
describe('query and aggs on signals index', () => {
test('query and aggs simultaneously', () => {
describe('query, aggs, size, _source and track_total_hits on signals index', () => {
test('query, aggs, size, _source and track_total_hits simultaneously', () => {
expect(
querySignalsSchema.validate<Partial<SignalsQueryRestParams>>({
query: {},
aggs: {},
size: 1,
track_total_hits: true,
_source: ['field'],
}).error
).toBeFalsy();
});
@ -33,7 +36,31 @@ describe('query and aggs on signals index', () => {
).toBeFalsy();
});
test('missing query and aggs is invalid', () => {
test('size only', () => {
expect(
querySignalsSchema.validate<Partial<SignalsQueryRestParams>>({
size: 1,
}).error
).toBeFalsy();
});
test('track_total_hits only', () => {
expect(
querySignalsSchema.validate<Partial<SignalsQueryRestParams>>({
track_total_hits: true,
}).error
).toBeFalsy();
});
test('_source only', () => {
expect(
querySignalsSchema.validate<Partial<SignalsQueryRestParams>>({
_source: ['field'],
}).error
).toBeFalsy();
});
test('missing query, aggs, size, _source and track_total_hits is invalid', () => {
expect(querySignalsSchema.validate<Partial<SignalsQueryRestParams>>({}).error).toBeTruthy();
});
});

View file

@ -9,4 +9,7 @@ import Joi from 'joi';
export const querySignalsSchema = Joi.object({
query: Joi.object(),
aggs: Joi.object(),
size: Joi.number(),
track_total_hits: Joi.boolean(),
_source: Joi.array().items(Joi.string()),
}).min(1);

View file

@ -25,14 +25,14 @@ export const querySignalsRouteDef = (server: ServerFacade): Hapi.ServerRoute =>
},
},
async handler(request: SignalsQueryRequest) {
const { query, aggs } = request.payload;
const body = { query, aggs };
const { query, aggs, _source, track_total_hits, size } = request.payload;
const index = getIndex(request, server);
const { callWithRequest } = server.plugins.elasticsearch.getCluster('data');
try {
return callWithRequest(request, 'search', {
index,
body,
body: { query, aggs, _source, track_total_hits, size },
});
} catch (exc) {
// error while getting or updating signal with id: id in signal index .siem-signals

View file

@ -24,6 +24,9 @@ export interface SignalsStatusParams {
export interface SignalQueryParams {
query: object | undefined | null;
aggs: object | undefined | null;
_source: string[] | undefined | null;
size: number | undefined | null;
track_total_hits: boolean | undefined | null;
}
export type SignalsStatusRestParams = Omit<SignalsStatusParams, 'signalIds'> & {