mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
* 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:
parent
6ac17261f6
commit
0a574034b5
85 changed files with 2189 additions and 1529 deletions
|
@ -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,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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';
|
|
@ -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;
|
||||
|
|
|
@ -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];
|
||||
};
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -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';
|
||||
|
|
|
@ -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];
|
||||
};
|
|
@ -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 {
|
|
@ -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,
|
|
@ -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';
|
||||
|
|
@ -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';
|
|
@ -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
|
|
@ -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;
|
|
@ -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[];
|
|
@ -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>
|
||||
));
|
|
@ -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];
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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;
|
||||
};
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
|
@ -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',
|
||||
});
|
|
@ -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[];
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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';
|
|
@ -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' },
|
||||
];
|
|
@ -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 = '' } }) => (
|
||||
|
|
|
@ -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';
|
|
@ -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>) => {
|
|
@ -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',
|
||||
},
|
||||
];
|
|
@ -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',
|
||||
},
|
|
@ -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}
|
|
@ -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]
|
||||
);
|
|
@ -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)}
|
|
@ -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 [];
|
||||
};
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -217,6 +217,7 @@ export const QueryBarDefineRule = ({
|
|||
onSubmitQuery={onSubmitQuery}
|
||||
savedQuery={savedQuery}
|
||||
onSavedQuery={onSavedQuery}
|
||||
hideSavedQuery={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
|
@ -14,7 +14,7 @@ exports[`RuleSwitch renders correctly against snapshot 1`] = `
|
|||
disabled={false}
|
||||
label="rule-switch"
|
||||
onChange={[Function]}
|
||||
showLabel={false}
|
||||
showLabel={true}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 (
|
|
@ -10,7 +10,9 @@ export {
|
|||
FieldHook,
|
||||
FIELD_TYPES,
|
||||
Form,
|
||||
FormData,
|
||||
FormDataProvider,
|
||||
FormHook,
|
||||
FormSchema,
|
||||
UseField,
|
||||
useForm,
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { AboutStepRule } from '../../types';
|
||||
|
||||
export const defaultValue: AboutStepRule = {
|
||||
export const stepAboutDefaultValue: AboutStepRule = {
|
||||
name: '',
|
||||
description: '',
|
||||
isNew: true,
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -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>,
|
||||
},
|
||||
};
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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(
|
|
@ -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>
|
||||
);
|
||||
});
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -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',
|
||||
{
|
|
@ -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;
|
||||
};
|
|
@ -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}
|
||||
/>
|
|
@ -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',
|
||||
});
|
|
@ -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';
|
|
@ -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',
|
||||
});
|
|
@ -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';
|
|
@ -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',
|
||||
});
|
|
@ -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 };
|
||||
};
|
|
@ -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 />
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'> & {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue