[SIEM] Detection engine cleanup for rule details/creation/edit page (#55069) (#55269)

* update extra action on rule detail to match design

* remove experimental label

* allow pre-package to be deleted + do not allow wrong user to create pre-packages rules

* Additional look back minimum value to 1

* fix flow with edit rule

* add success toaster when rule is created or updated

* Fix Timeline selector loading

* review ben doc + change detectin engine to detection even in url

* Succeeded text size consistency in rule details page

* fix description of threats

* fix test

* fix type

* fix internatinalization

* Update x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/translations.ts

Co-Authored-By: Garrett Spong <spong@users.noreply.github.com>

* Update x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/translations.ts

Co-Authored-By: Garrett Spong <spong@users.noreply.github.com>

* Update x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx

Co-Authored-By: Garrett Spong <spong@users.noreply.github.com>

* review I

* fix type

Co-authored-by: Garrett Spong <spong@users.noreply.github.com>

Co-authored-by: Garrett Spong <spong@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Xavier Mouligneau 2020-01-18 11:17:51 -05:00 committed by GitHub
parent 666eda060b
commit be1c35f4d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 301 additions and 195 deletions

View file

@ -55,7 +55,7 @@ export const HeaderGlobal = React.memo<HeaderGlobalProps>(({ hideDetectionEngine
display="condensed"
navTabs={
hideDetectionEngine
? pickBy((_, key) => key !== SiemPageName.detectionEngine, navTabs)
? pickBy((_, key) => key !== SiemPageName.detections, navTabs)
: navTabs
}
/>

View file

@ -61,32 +61,32 @@ export const LinkToPage = React.memo<LinkToPageProps>(({ match }) => (
<Route
component={RedirectToDetectionEnginePage}
exact
path={`${match.url}/:pageName(${SiemPageName.detectionEngine})`}
path={`${match.url}/:pageName(${SiemPageName.detections})`}
strict
/>
<Route
component={RedirectToDetectionEnginePage}
exact
path={`${match.url}/:pageName(${SiemPageName.detectionEngine})/:tabName(${DetectionEngineTab.alerts}|${DetectionEngineTab.signals})`}
path={`${match.url}/:pageName(${SiemPageName.detections})/:tabName(${DetectionEngineTab.alerts}|${DetectionEngineTab.signals})`}
strict
/>
<Route
component={RedirectToRulesPage}
exact
path={`${match.url}/:pageName(${SiemPageName.detectionEngine})/rules`}
path={`${match.url}/:pageName(${SiemPageName.detections})/rules`}
/>
<Route
component={RedirectToCreateRulePage}
path={`${match.url}/:pageName(${SiemPageName.detectionEngine})/rules/create-rule`}
path={`${match.url}/:pageName(${SiemPageName.detections})/rules/create-rule`}
/>
<Route
component={RedirectToRuleDetailsPage}
exact
path={`${match.url}/:pageName(${SiemPageName.detectionEngine})/rules/rule-details`}
path={`${match.url}/:pageName(${SiemPageName.detections})/rules/rule-details`}
/>
<Route
component={RedirectToEditRulePage}
path={`${match.url}/:pageName(${SiemPageName.detectionEngine})/rules/rule-details/edit-rule`}
path={`${match.url}/:pageName(${SiemPageName.detections})/rules/rule-details/edit-rule`}
/>
<Route
component={RedirectToTimelinesPage}

View file

@ -15,7 +15,7 @@ export type DetectionEngineComponentProps = RouteComponentProps<{
search: string;
}>;
export const DETECTION_ENGINE_PAGE_NAME = 'detection-engine';
export const DETECTION_ENGINE_PAGE_NAME = 'detections';
export const RedirectToDetectionEnginePage = ({
match: {

View file

@ -64,12 +64,12 @@ describe('SIEM Navigation', () => {
expect(setBreadcrumbs).toHaveBeenNthCalledWith(1, {
detailName: undefined,
navTabs: {
'detection-engine': {
detections: {
disabled: false,
href: '#/link-to/detection-engine',
id: 'detection-engine',
name: 'Detection engine',
urlKey: 'detection-engine',
href: '#/link-to/detections',
id: 'detections',
name: 'Detections',
urlKey: 'detections',
},
hosts: {
disabled: false,
@ -146,12 +146,12 @@ describe('SIEM Navigation', () => {
detailName: undefined,
filters: [],
navTabs: {
'detection-engine': {
detections: {
disabled: false,
href: '#/link-to/detection-engine',
id: 'detection-engine',
name: 'Detection engine',
urlKey: 'detection-engine',
href: '#/link-to/detections',
id: 'detections',
name: 'Detections',
urlKey: 'detections',
},
hosts: {
disabled: false,

View file

@ -45,11 +45,25 @@ const MyEuiFlexItem = styled(EuiFlexItem)`
white-space: nowrap;
`;
const EuiSelectableContainer = styled.div`
const EuiSelectableContainer = styled.div<{ loading: boolean }>`
.euiSelectable {
.euiFormControlLayout__childrenWrapper {
display: flex;
}
${({ loading }) => `${
loading
? `
.euiFormControlLayoutIcons {
display: none;
}
.euiFormControlLayoutIcons.euiFormControlLayoutIcons--right {
display: block;
left: 12px;
top: 12px;
}`
: ''
}
`}
}
`;
@ -265,7 +279,7 @@ const SearchTimelineSuperSelectComponent: React.FC<SearchTimelineSuperSelectProp
onlyUserFavorite={onlyFavorites}
>
{({ timelines, loading, totalCount }) => (
<EuiSelectableContainer>
<EuiSelectableContainer loading={loading}>
<EuiSelectable
height={POPOVER_HEIGHT}
isLoading={loading && timelines.length === 0}

View file

@ -6,7 +6,7 @@
export enum CONSTANTS {
appQuery = 'query',
detectionEnginePage = 'detectionEngine.page',
detectionsPage = 'detections.page',
filters = 'filters',
hostsDetails = 'hosts.details',
hostsPage = 'hosts.page',
@ -20,4 +20,4 @@ export enum CONSTANTS {
unknown = 'unknown',
}
export type UrlStateType = 'detection-engine' | 'host' | 'network' | 'overview' | 'timeline';
export type UrlStateType = 'detections' | 'host' | 'network' | 'overview' | 'timeline';

View file

@ -4,46 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { navTabs } from '../../pages/home/home_navigations';
import { SiemPageName } from '../../pages/home/types';
import { isKqlForRoute, getTitle } from './helpers';
import { CONSTANTS } from './constants';
import { getTitle } from './helpers';
import { HostsType } from '../../store/hosts/model';
describe('Helpers Url_State', () => {
describe('isKqlForRoute', () => {
test('host page and host page kuery', () => {
const result = isKqlForRoute(SiemPageName.hosts, undefined, CONSTANTS.hostsPage);
expect(result).toBeTruthy();
});
test('host page and host details kuery', () => {
const result = isKqlForRoute(SiemPageName.hosts, undefined, CONSTANTS.hostsDetails);
expect(result).toBeFalsy();
});
test('host details and host details kuery', () => {
const result = isKqlForRoute(SiemPageName.hosts, 'siem-kibana', CONSTANTS.hostsDetails);
expect(result).toBeTruthy();
});
test('host details and host page kuery', () => {
const result = isKqlForRoute(SiemPageName.hosts, 'siem-kibana', CONSTANTS.hostsPage);
expect(result).toBeFalsy();
});
test('network page and network page kuery', () => {
const result = isKqlForRoute(SiemPageName.network, undefined, CONSTANTS.networkPage);
expect(result).toBeTruthy();
});
test('network page and network details kuery', () => {
const result = isKqlForRoute(SiemPageName.network, undefined, CONSTANTS.networkDetails);
expect(result).toBeFalsy();
});
test('network details and network details kuery', () => {
const result = isKqlForRoute(SiemPageName.network, '10.100.7.198', CONSTANTS.networkDetails);
expect(result).toBeTruthy();
});
test('network details and network page kuery', () => {
const result = isKqlForRoute(SiemPageName.network, '123.234.34', CONSTANTS.networkPage);
expect(result).toBeFalsy();
});
});
describe('getTitle', () => {
test('host page name', () => {
const result = getTitle('hosts', undefined, navTabs);

View file

@ -78,8 +78,8 @@ export const getUrlType = (pageName: string): UrlStateType => {
return 'host';
} else if (pageName === SiemPageName.network) {
return 'network';
} else if (pageName === SiemPageName.detectionEngine) {
return 'detection-engine';
} else if (pageName === SiemPageName.detections) {
return 'detections';
} else if (pageName === SiemPageName.timelines) {
return 'timeline';
}
@ -111,31 +111,14 @@ export const getCurrentLocation = (
return CONSTANTS.networkDetails;
}
return CONSTANTS.networkPage;
} else if (pageName === SiemPageName.detectionEngine) {
return CONSTANTS.detectionEnginePage;
} else if (pageName === SiemPageName.detections) {
return CONSTANTS.detectionsPage;
} else if (pageName === SiemPageName.timelines) {
return CONSTANTS.timelinePage;
}
return CONSTANTS.unknown;
};
export const isKqlForRoute = (
pageName: string,
detailName: string | undefined,
queryLocation: LocationTypes | null = null
): boolean => {
const currentLocation = getCurrentLocation(pageName, detailName);
if (
(currentLocation === CONSTANTS.hostsPage && queryLocation === CONSTANTS.hostsPage) ||
(currentLocation === CONSTANTS.networkPage && queryLocation === CONSTANTS.networkPage) ||
(currentLocation === CONSTANTS.hostsDetails && queryLocation === CONSTANTS.hostsDetails) ||
(currentLocation === CONSTANTS.networkDetails && queryLocation === CONSTANTS.networkDetails)
) {
return true;
}
return false;
};
export const makeMapStateToProps = () => {
const getInputsSelector = inputsSelectors.inputsSelector();
const getGlobalQuerySelector = inputsSelectors.globalQuerySelector();

View file

@ -24,7 +24,7 @@ export const ALL_URL_STATE_KEYS: KeyUrlState[] = [
];
export const URL_STATE_KEYS: Record<UrlStateType, KeyUrlState[]> = {
'detection-engine': [
detections: [
CONSTANTS.appQuery,
CONSTANTS.filters,
CONSTANTS.savedQuery,
@ -56,7 +56,7 @@ export const URL_STATE_KEYS: Record<UrlStateType, KeyUrlState[]> = {
};
export type LocationTypes =
| CONSTANTS.detectionEnginePage
| CONSTANTS.detectionsPage
| CONSTANTS.hostsDetails
| CONSTANTS.hostsPage
| CONSTANTS.networkDetails

View file

@ -0,0 +1,81 @@
/*
* 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 { createPrepackagedRules } from './api';
type Return = [boolean, boolean | null];
interface UseCreatePackagedRules {
canUserCRUD: boolean | null;
hasIndexManage: boolean | null;
hasManageApiKey: boolean | null;
isAuthenticated: boolean | null;
isSignalIndexExists: boolean | null;
}
/**
* Hook for creating the packages rules
*
* @param canUserCRUD boolean
* @param hasIndexManage boolean
* @param hasManageApiKey boolean
* @param isAuthenticated boolean
* @param isSignalIndexExists boolean
*
* @returns [loading, hasCreatedPackageRules]
*/
export const useCreatePackagedRules = ({
canUserCRUD,
hasIndexManage,
hasManageApiKey,
isAuthenticated,
isSignalIndexExists,
}: UseCreatePackagedRules): Return => {
const [hasCreatedPackageRules, setHasCreatedPackageRules] = useState<boolean | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let isSubscribed = true;
const abortCtrl = new AbortController();
setLoading(true);
async function createRules() {
try {
await createPrepackagedRules({
signal: abortCtrl.signal,
});
if (isSubscribed) {
setHasCreatedPackageRules(true);
}
} catch (error) {
if (isSubscribed) {
setHasCreatedPackageRules(false);
}
}
if (isSubscribed) {
setLoading(false);
}
}
if (
canUserCRUD &&
hasIndexManage &&
hasManageApiKey &&
isAuthenticated &&
isSignalIndexExists
) {
createRules();
}
return () => {
isSubscribed = false;
abortCtrl.abort();
};
}, [canUserCRUD, hasIndexManage, hasManageApiKey, isAuthenticated, isSignalIndexExists]);
return [loading, hasCreatedPackageRules];
};

View file

@ -8,7 +8,6 @@ import { useEffect, useState, useRef } from 'react';
import { errorToToaster } from '../../../components/ml/api/error_to_toaster';
import { useStateToaster } from '../../../components/toasters';
import { createPrepackagedRules } from '../rules';
import { createSignalIndex, getSignalIndex } from './api';
import * as i18n from './translations';
import { PostSignalError, SignalIndexError } from './types';
@ -41,7 +40,6 @@ export const useSignalIndex = (): Return => {
if (isSubscribed && signal != null) {
setSignalIndexName(signal.name);
setSignalIndexExists(true);
createPrepackagedRules({ signal: abortCtrl.signal });
}
} catch (error) {
if (isSubscribed) {

View file

@ -45,7 +45,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 1,
rule: {
href: '#/detection-engine/rules/rule-details',
href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@ -55,7 +55,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 2,
rule: {
href: '#/detection-engine/rules/rule-details',
href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@ -65,7 +65,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 3,
rule: {
href: '#/detection-engine/rules/rule-details',
href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@ -76,7 +76,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 4,
rule: {
href: '#/detection-engine/rules/rule-details',
href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@ -87,7 +87,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 5,
rule: {
href: '#/detection-engine/rules/rule-details',
href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@ -98,7 +98,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 6,
rule: {
href: '#/detection-engine/rules/rule-details',
href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@ -109,7 +109,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 7,
rule: {
href: '#/detection-engine/rules/rule-details',
href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@ -120,7 +120,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 8,
rule: {
href: '#/detection-engine/rules/rule-details',
href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@ -131,7 +131,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 9,
rule: {
href: '#/detection-engine/rules/rule-details',
href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@ -142,7 +142,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 10,
rule: {
href: '#/detection-engine/rules/rule-details',
href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@ -153,7 +153,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 11,
rule: {
href: '#/detection-engine/rules/rule-details',
href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@ -164,7 +164,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 12,
rule: {
href: '#/detection-engine/rules/rule-details',
href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@ -175,7 +175,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 13,
rule: {
href: '#/detection-engine/rules/rule-details',
href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@ -186,7 +186,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 14,
rule: {
href: '#/detection-engine/rules/rule-details',
href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@ -197,7 +197,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 15,
rule: {
href: '#/detection-engine/rules/rule-details',
href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@ -208,7 +208,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 16,
rule: {
href: '#/detection-engine/rules/rule-details',
href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@ -219,7 +219,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 17,
rule: {
href: '#/detection-engine/rules/rule-details',
href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@ -230,7 +230,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 18,
rule: {
href: '#/detection-engine/rules/rule-details',
href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@ -241,7 +241,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 19,
rule: {
href: '#/detection-engine/rules/rule-details',
href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@ -252,7 +252,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 20,
rule: {
href: '#/detection-engine/rules/rule-details',
href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@ -263,7 +263,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 21,
rule: {
href: '#/detection-engine/rules/rule-details',
href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',

View file

@ -86,7 +86,7 @@ export const STACK_BY_USERS = i18n.translate(
export const HISTOGRAM_HEADER = i18n.translate(
'xpack.siem.detectionEngine.signals.histogram.headerTitle',
{
defaultMessage: 'Signal detection frequency',
defaultMessage: 'Signal count',
}
);

View file

@ -10,6 +10,7 @@ import React, { useEffect, useReducer, Dispatch, createContext, useContext } fro
import { usePrivilegeUser } from '../../../../containers/detection_engine/signals/use_privilege_user';
import { useSignalIndex } from '../../../../containers/detection_engine/signals/use_signal_index';
import { useKibana } from '../../../../lib/kibana';
import { useCreatePackagedRules } from '../../../../containers/detection_engine/rules/use_create_packaged_rules';
export interface State {
canUserCRUD: boolean | null;
@ -161,6 +162,14 @@ export const useUserInfo = (): State => {
createSignalIndex,
] = useSignalIndex();
useCreatePackagedRules({
canUserCRUD,
hasIndexManage,
hasManageApiKey,
isAuthenticated,
isSignalIndexExists,
});
const uiCapabilities = useKibana().services.application.capabilities;
const capabilitiesCanUserCRUD: boolean =
typeof uiCapabilities.siem.crud === 'boolean' ? uiCapabilities.siem.crud : false;

View file

@ -148,7 +148,7 @@ const DetectionEngineComponent = React.memo<DetectionEngineComponentProps>(
}
title={i18n.PAGE_TITLE}
>
<EuiButton fill href="#/detection-engine/rules" iconType="gear">
<EuiButton fill href="#/detections/rules" iconType="gear">
{i18n.BUTTON_MANAGE_RULES}
</EuiButton>
</HeaderPage>

View file

@ -15,7 +15,7 @@ import { RuleDetails } from './rules/details';
import { RulesComponent } from './rules';
import { DetectionEngineTab } from './types';
const detectionEnginePath = `/:pageName(detection-engine)`;
const detectionEnginePath = `/:pageName(detections)`;
type Props = Partial<RouteComponentProps<{}>> & { url: string };
@ -42,12 +42,9 @@ export const DetectionEngineContainer = React.memo<Props>(() => (
<EditRuleComponent />
</Route>
<Route
path="/detection-engine/"
path="/detections/"
render={({ location: { search = '' } }) => (
<Redirect
from="/detection-engine/"
to={`/detection-engine/${DetectionEngineTab.signals}${search}`}
/>
<Redirect from="/detections/" to={`/detections/${DetectionEngineTab.signals}${search}`} />
)}
/>
</Switch>

View file

@ -60,7 +60,7 @@ export const mockTableData: TableData[] = [
lastResponse: { type: '—' },
method: 'saved_query',
rule: {
href: '#/detection-engine/rules/id/abe6c564-050d-45a5-aaf0-386c37dd1f61',
href: '#/detections/rules/id/abe6c564-050d-45a5-aaf0-386c37dd1f61',
name: 'Home Grown!',
status: 'Status Placeholder',
},
@ -112,7 +112,7 @@ export const mockTableData: TableData[] = [
lastResponse: { type: '—' },
method: 'saved_query',
rule: {
href: '#/detection-engine/rules/id/63f06f34-c181-4b2d-af35-f2ace572a1ee',
href: '#/detections/rules/id/63f06f34-c181-4b2d-af35-f2ace572a1ee',
name: 'Home Grown!',
status: 'Status Placeholder',
},

View file

@ -90,7 +90,7 @@ export const getColumns = (
field: 'method',
name: i18n.COLUMN_METHOD,
truncateText: true,
width: '16%',
width: '14%',
},
{
field: 'severity',
@ -165,7 +165,7 @@ export const getColumns = (
/>
),
sortable: true,
width: '85px',
width: '95px',
},
];
const actions: RulesColumns[] = [

View file

@ -24,7 +24,7 @@ export const formatRules = (rules: Rule[], selectedIds?: string[]): TableData[]
immutable: rule.immutable,
rule_id: rule.rule_id,
rule: {
href: `#/detection-engine/rules/id/${encodeURIComponent(rule.id)}`,
href: `#/detections/rules/id/${encodeURIComponent(rule.id)}`,
name: rule.name,
status: 'Status Placeholder',
},

View file

@ -125,7 +125,7 @@ export const buildThreatsDescription = ({
description: (
<ThreatsEuiFlexGroup direction="column">
{threats.map((threat, index) => {
const tactic = tacticsOptions.find(t => t.name === threat.tactic.name);
const tactic = tacticsOptions.find(t => t.id === threat.tactic.id);
return (
<EuiFlexItem key={`${threat.tactic.name}-${index}`}>
<EuiLink href={threat.tactic.reference} target="_blank">
@ -133,7 +133,7 @@ export const buildThreatsDescription = ({
</EuiLink>
<EuiFlexGroup gutterSize="none" alignItems="flexStart" direction="column">
{threat.techniques.map(technique => {
const myTechnique = techniquesOptions.find(t => t.name === technique.name);
const myTechnique = techniquesOptions.find(t => t.id === technique.id);
return (
<EuiFlexItem>
<TechniqueLinkItem

View file

@ -10,7 +10,7 @@ exports[`RuleActionsOverflow renders correctly against snapshot 1`] = `
delay="regular"
position="top"
>
<EuiButtonIcon
<ForwardRef(Styled(EuiButtonIcon))
aria-label="All actions"
iconType="boxesHorizontal"
isDisabled={false}

View file

@ -12,17 +12,29 @@ import {
EuiToolTip,
} from '@elastic/eui';
import React, { useCallback, useMemo, useState } from 'react';
import styled from 'styled-components';
import { noop } from 'lodash/fp';
import { useHistory } from 'react-router-dom';
import { Rule } from '../../../../../containers/detection_engine/rules';
import * as i18n from './translations';
import * as i18nActions from '../../../rules/translations';
import { deleteRulesAction, duplicateRulesAction } from '../../all/actions';
import { displaySuccessToast, useStateToaster } from '../../../../../components/toasters';
import { deleteRulesAction, duplicateRulesAction } from '../../all/actions';
import { RuleDownloader } from '../rule_downloader';
import { DETECTION_ENGINE_PAGE_NAME } from '../../../../../components/link_to/redirect_to_detection_engine';
const MyEuiButtonIcon = styled(EuiButtonIcon)`
&.euiButtonIcon {
svg {
transform: rotate(90deg);
}
border: 1px solid  ${({ theme }) => theme.euiColorPrimary};
width: 40px;
height: 40px;
}
`;
interface RuleActionsOverflowComponentProps {
rule: Rule | null;
userHasNoPermissions: boolean;
@ -86,20 +98,29 @@ const RuleActionsOverflowComponent = ({
[rule, userHasNoPermissions]
);
const handlePopoverOpen = useCallback(() => {
setIsPopoverOpen(!isPopoverOpen);
}, [setIsPopoverOpen, isPopoverOpen]);
const button = useMemo(
() => (
<EuiToolTip position="top" content={i18n.ALL_ACTIONS}>
<MyEuiButtonIcon
iconType="boxesHorizontal"
aria-label={i18n.ALL_ACTIONS}
isDisabled={userHasNoPermissions}
onClick={handlePopoverOpen}
/>
</EuiToolTip>
),
[handlePopoverOpen, userHasNoPermissions]
);
return (
<>
<EuiPopover
anchorPosition="leftCenter"
button={
<EuiToolTip position="top" content={i18n.ALL_ACTIONS}>
<EuiButtonIcon
iconType="boxesHorizontal"
aria-label={i18n.ALL_ACTIONS}
isDisabled={userHasNoPermissions}
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
/>
</EuiToolTip>
}
button={button}
closePopover={() => setIsPopoverOpen(false)}
id="ruleActionsOverflow"
isOpen={isPopoverOpen}

View file

@ -150,7 +150,13 @@ export const ScheduleItem = ({
/>
}
>
<EuiFieldNumber fullWidth min={0} onChange={onChangeTimeVal} value={timeVal} {...rest} />
<EuiFieldNumber
fullWidth
min={minimumValue}
onChange={onChangeTimeVal}
value={timeVal}
{...rest}
/>
</EuiFormControlLayout>
</StyledEuiFormRow>
);

View file

@ -5,6 +5,7 @@
*/
import { AboutStepRule } from '../../types';
import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/search_super_select/translations';
export const threatsDefault = [
{
@ -25,7 +26,7 @@ export const stepAboutDefaultValue: AboutStepRule = {
tags: [],
timeline: {
id: null,
title: null,
title: DEFAULT_TIMELINE_TITLE,
},
threats: threatsDefault,
};

View file

@ -5,10 +5,11 @@
*/
import { EuiButton, EuiHorizontalRule, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { isEqual, get } from 'lodash/fp';
import { isEqual } from 'lodash/fp';
import React, { FC, memo, useCallback, useEffect, useState } from 'react';
import styled from 'styled-components';
import { setFieldValue } from '../../helpers';
import { RuleStepProps, RuleStep, AboutStepRule } from '../../types';
import * as RuleI18n from '../../translations';
import { AddItem } from '../add_item_form';
@ -71,14 +72,7 @@ const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({
isNew: false,
};
setMyStepData(myDefaultValues);
if (!isReadOnlyView) {
Object.keys(schema).forEach(key => {
const val = get(key, myDefaultValues);
if (val != null) {
form.setFieldValue(key, val);
}
});
}
setFieldValue(form, schema, myDefaultValues);
}
}, [defaultValues]);
@ -88,7 +82,7 @@ const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({
}
}, [form]);
return isReadOnlyView && myStepData != null ? (
return isReadOnlyView && myStepData.name != null ? (
<StepContentWrapper addPadding={addPadding}>
<StepRuleDescription direction={descriptionDirection} schema={schema} data={myStepData} />
</StepContentWrapper>

View file

@ -11,7 +11,7 @@ import {
EuiFlexItem,
EuiButton,
} from '@elastic/eui';
import { isEmpty, isEqual, get } from 'lodash/fp';
import { isEmpty, isEqual } from 'lodash/fp';
import React, { FC, memo, useCallback, useState, useEffect } from 'react';
import styled from 'styled-components';
@ -19,6 +19,7 @@ import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/pu
import { useFetchIndexPatterns } from '../../../../../containers/detection_engine/rules';
import { DEFAULT_INDEX_KEY } from '../../../../../../common/constants';
import { useUiSetting$ } from '../../../../../lib/kibana';
import { setFieldValue } from '../../helpers';
import * as RuleI18n from '../../translations';
import { DefineStepRule, RuleStep, RuleStepProps } from '../../types';
import { StepRuleDescription } from '../description_step';
@ -121,14 +122,7 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
if (!isEqual(myDefaultValues, myStepData)) {
setMyStepData(myDefaultValues);
setLocalUseIndicesConfig(isEqual(myDefaultValues.index, indicesConfig));
if (!isReadOnlyView) {
Object.keys(schema).forEach(key => {
const val = get(key, myDefaultValues);
if (val != null) {
form.setFieldValue(key, val);
}
});
}
setFieldValue(form, schema, myDefaultValues);
}
}
}, [defaultValues, indicesConfig]);
@ -152,7 +146,7 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
setOpenTimelineSearch(false);
}, []);
return isReadOnlyView && myStepData != null ? (
return isReadOnlyView && myStepData?.queryBar != null ? (
<StepContentWrapper addPadding={addPadding}>
<StepRuleDescription
direction={descriptionDirection}

View file

@ -5,9 +5,10 @@
*/
import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
import { isEqual, get } from 'lodash/fp';
import { isEqual } from 'lodash/fp';
import React, { FC, memo, useCallback, useEffect, useState } from 'react';
import { setFieldValue } from '../../helpers';
import { RuleStep, RuleStepProps, ScheduleStepRule } from '../../types';
import { StepRuleDescription } from '../description_step';
import { ScheduleItem } from '../schedule_item_form';
@ -24,7 +25,7 @@ const stepScheduleDefaultValue = {
enabled: true,
interval: '5m',
isNew: true,
from: '0m',
from: '1m',
};
const StepScheduleRuleComponent: FC<StepScheduleRuleProps> = ({
@ -67,14 +68,7 @@ const StepScheduleRuleComponent: FC<StepScheduleRuleProps> = ({
isNew: false,
};
setMyStepData(myDefaultValues);
if (!isReadOnlyView) {
Object.keys(schema).forEach(key => {
const val = get(key, myDefaultValues);
if (val != null) {
form.setFieldValue(key, val);
}
});
}
setFieldValue(form, schema, myDefaultValues);
}
}, [defaultValues]);

View file

@ -14,13 +14,14 @@ export const schema: FormSchema = {
label: i18n.translate(
'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldIntervalLabel',
{
defaultMessage: 'Rule run interval & look-back',
defaultMessage: 'Runs every',
}
),
helpText: i18n.translate(
'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldIntervalHelpText',
{
defaultMessage: 'How often and how far back this rule will search specified indices.',
defaultMessage:
'Rules run periodically and detect signals within the specified time frame.',
}
),
},
@ -28,15 +29,14 @@ export const schema: FormSchema = {
label: i18n.translate(
'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldAdditionalLookBackLabel',
{
defaultMessage: 'Additional look-back',
defaultMessage: 'Additional look-back time',
}
),
labelAppend: OptionalFieldLabel,
helpText: i18n.translate(
'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldAdditionalLookBackHelpText',
{
defaultMessage:
'Add more time to the look-back range in order to prevent potential gaps in signal reporting.',
defaultMessage: 'Adds time to the look-back period to prevent missed signals.',
}
),
},

View file

@ -9,13 +9,13 @@ import { i18n } from '@kbn/i18n';
export const COMPLETE_WITHOUT_ACTIVATING = i18n.translate(
'xpack.siem.detectionEngine.createRule. stepScheduleRule.completeWithoutActivatingTitle',
{
defaultMessage: 'Complete rule without activating',
defaultMessage: 'Create rule without activating it',
}
);
export const COMPLETE_WITH_ACTIVATING = i18n.translate(
'xpack.siem.detectionEngine.createRule. stepScheduleRule.completeWithActivatingTitle',
{
defaultMessage: 'Complete rule & activate',
defaultMessage: 'Create & activate rule',
}
);

View file

@ -9,10 +9,11 @@ import React, { useCallback, useRef, useState } from 'react';
import { Redirect } from 'react-router-dom';
import styled from 'styled-components';
import { usePersistRule } from '../../../../containers/detection_engine/rules';
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 { usePersistRule } from '../../../../containers/detection_engine/rules';
import { displaySuccessToast, useStateToaster } from '../../../../components/toasters';
import { SpyRoute } from '../../../../utils/route/spy_routes';
import { useUserInfo } from '../../components/user_info';
import { AccordionTitle } from '../components/accordion_title';
@ -55,6 +56,7 @@ export const CreateRuleComponent = React.memo(() => {
canUserCRUD,
hasManageApiKey,
} = useUserInfo();
const [, dispatchToaster] = useStateToaster();
const [openAccordionId, setOpenAccordionId] = useState<RuleStep>(RuleStep.defineRule);
const defineRuleRef = useRef<EuiAccordion | null>(null);
const aboutRuleRef = useRef<EuiAccordion | null>(null);
@ -95,6 +97,7 @@ export const CreateRuleComponent = React.memo(() => {
const stepRuleIdx = stepsRuleOrder.findIndex(item => step === item);
if ([0, 1].includes(stepRuleIdx)) {
if (isStepRuleInReadOnlyView[stepsRuleOrder[stepRuleIdx + 1]]) {
setOpenAccordionId(stepsRuleOrder[stepRuleIdx + 1]);
setIsStepRuleInEditView({
...isStepRuleInReadOnlyView,
[step]: true,
@ -203,12 +206,15 @@ export const CreateRuleComponent = React.memo(() => {
async (id: RuleStep) => {
const activeForm = await stepsForm.current[openAccordionId]?.submit();
if (activeForm != null && activeForm?.isValid) {
stepsData.current[openAccordionId] = {
...stepsData.current[openAccordionId],
data: activeForm.data,
isValid: activeForm.isValid,
};
setOpenAccordionId(id);
openCloseAccordion(openAccordionId);
setIsStepRuleInEditView({
...isStepRuleInReadOnlyView,
[openAccordionId]: openAccordionId === RuleStep.scheduleRule ? false : true,
[openAccordionId]: true,
[id]: false,
});
}
@ -217,6 +223,8 @@ export const CreateRuleComponent = React.memo(() => {
);
if (isSaved) {
const ruleName = (stepsData.current[RuleStep.aboutRule].data as AboutStepRule).name;
displaySuccessToast(i18n.SUCCESSFULLY_CREATED_RULES(ruleName), dispatchToaster);
return <Redirect to={`/${DETECTION_ENGINE_PAGE_NAME}/rules`} />;
}
@ -224,7 +232,7 @@ export const CreateRuleComponent = React.memo(() => {
<>
<WrapperPage restrictWidth>
<HeaderPage
backOptions={{ href: '#detection-engine/rules', text: 'Back to rules' }}
backOptions={{ href: '#detections/rules', text: i18n.BACK_TO_RULES }}
border
isLoading={isLoading || loading}
title={i18n.PAGE_TITLE}
@ -252,6 +260,9 @@ export const CreateRuleComponent = React.memo(() => {
<EuiHorizontalRule margin="m" />
<StepDefineRule
addPadding={true}
defaultValues={
(stepsData.current[RuleStep.defineRule].data as DefineStepRule) ?? null
}
isReadOnlyView={isStepRuleInReadOnlyView[RuleStep.defineRule]}
isLoading={isLoading || loading}
setForm={setStepsForm}
@ -284,6 +295,7 @@ export const CreateRuleComponent = React.memo(() => {
<EuiHorizontalRule margin="m" />
<StepAboutRule
addPadding={true}
defaultValues={(stepsData.current[RuleStep.aboutRule].data as AboutStepRule) ?? null}
descriptionDirection="row"
isReadOnlyView={isStepRuleInReadOnlyView[RuleStep.aboutRule]}
isLoading={isLoading || loading}
@ -316,6 +328,9 @@ export const CreateRuleComponent = React.memo(() => {
<EuiHorizontalRule margin="m" />
<StepScheduleRule
addPadding={true}
defaultValues={
(stepsData.current[RuleStep.scheduleRule].data as ScheduleStepRule) ?? null
}
descriptionDirection="row"
isReadOnlyView={isStepRuleInReadOnlyView[RuleStep.scheduleRule]}
isLoading={isLoading || loading}

View file

@ -10,6 +10,19 @@ export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.createRule.
defaultMessage: 'Create new rule',
});
export const BACK_TO_RULES = i18n.translate(
'xpack.siem.detectionEngine.createRule.backToRulesDescription',
{
defaultMessage: 'Back to signal detection rules',
}
);
export const EDIT_RULE = i18n.translate('xpack.siem.detectionEngine.createRule.editRuleButton', {
defaultMessage: 'Edit',
});
export const SUCCESSFULLY_CREATED_RULES = (ruleName: string) =>
i18n.translate('xpack.siem.detectionEngine.rules.create.successfullyCreatedRuleTitle', {
values: { ruleName },
defaultMessage: '{ruleName} was created',
});

View file

@ -12,6 +12,7 @@ import {
EuiSpacer,
EuiHealth,
EuiTab,
EuiText,
EuiTabs,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
@ -249,7 +250,6 @@ const RuleDetailsComponent = memo<RuleDetailsComponentProps>(
href: `#${DETECTION_ENGINE_PAGE_NAME}/rules`,
text: i18n.BACK_TO_RULES,
}}
badgeOptions={{ text: i18n.EXPERIMENTAL }}
border
subtitle={subTitle}
subtitle2={[
@ -273,7 +273,7 @@ const RuleDetailsComponent = memo<RuleDetailsComponentProps>(
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiHealth color={statusColor}>
{rule?.status ?? getEmptyTagValue()}
<EuiText size="xs">{rule?.status ?? getEmptyTagValue()}</EuiText>
</EuiHealth>
</EuiFlexItem>
{rule?.status_date && (

View file

@ -13,7 +13,7 @@ export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.ruleDetails
export const BACK_TO_RULES = i18n.translate(
'xpack.siem.detectionEngine.ruleDetails.backToRulesDescription',
{
defaultMessage: 'Back to rules',
defaultMessage: 'Back to signal detection rules',
}
);

View file

@ -17,11 +17,12 @@ import { FormattedMessage } from '@kbn/i18n/react';
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Redirect, useParams } from 'react-router-dom';
import { useRule, usePersistRule } from '../../../../containers/detection_engine/rules';
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 { displaySuccessToast, useStateToaster } from '../../../../components/toasters';
import { SpyRoute } from '../../../../utils/route/spy_routes';
import { useUserInfo } from '../../components/user_info';
import { FormHook, FormData } from '../components/shared_imports';
import { StepPanel } from '../components/step_panel';
@ -48,6 +49,7 @@ interface ScheduleStepRuleForm extends StepRuleForm {
}
export const EditRuleComponent = memo(() => {
const [, dispatchToaster] = useStateToaster();
const {
loading: initLoading,
isSignalIndexExists,
@ -271,6 +273,7 @@ export const EditRuleComponent = memo(() => {
}, []);
if (isSaved || (rule != null && rule.immutable)) {
displaySuccessToast(i18n.SUCCESSFULLY_SAVED_RULE(rule?.name ?? ''), dispatchToaster);
return <Redirect to={`/${DETECTION_ENGINE_PAGE_NAME}/rules/id/${ruleId}`} />;
}

View file

@ -28,3 +28,9 @@ export const SORRY_ERRORS = i18n.translate(
export const BACK_TO = i18n.translate('xpack.siem.detectionEngine.editRule.backToDescription', {
defaultMessage: 'Back to',
});
export const SUCCESSFULLY_SAVED_RULE = (ruleName: string) =>
i18n.translate('xpack.siem.detectionEngine.rules.update.successfullySavedRuleTitle', {
values: { ruleName },
defaultMessage: '{ruleName} was saved',
});

View file

@ -4,11 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { pick } from 'lodash/fp';
import { get, pick } from 'lodash/fp';
import { useLocation } from 'react-router-dom';
import { esFilters } from '../../../../../../../../src/plugins/data/public';
import { Rule } from '../../../containers/detection_engine/rules';
import { FormData, FormHook, FormSchema } from './components/shared_imports';
import { AboutStepRule, DefineStepRule, IMitreEnterpriseAttack, ScheduleStepRule } from './types';
interface GetStepsData {
@ -67,3 +68,15 @@ export const getStepsData = ({
};
export const useQuery = () => new URLSearchParams(useLocation().search);
export const setFieldValue = (
form: FormHook<FormData>,
schema: FormSchema<FormData>,
defaultValues: unknown
) =>
Object.keys(schema).forEach(key => {
const val = get(key, defaultValues);
if (val != null) {
form.setFieldValue(key, val);
}
});

View file

@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n';
export const BACK_TO_DETECTION_ENGINE = i18n.translate(
'xpack.siem.detectionEngine.rules.backOptionsHeader',
{
defaultMessage: 'Back to detection engine',
defaultMessage: 'Back to detections',
}
);
@ -18,7 +18,7 @@ export const IMPORT_RULE = i18n.translate('xpack.siem.detectionEngine.rules.impo
});
export const ADD_NEW_RULE = i18n.translate('xpack.siem.detectionEngine.rules.addNewRuleTitle', {
defaultMessage: 'Add new rule',
defaultMessage: 'Create new rule',
});
export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.rules.pageTitle', {
@ -32,7 +32,7 @@ export const REFRESH = i18n.translate('xpack.siem.detectionEngine.rules.allRules
export const BATCH_ACTIONS = i18n.translate(
'xpack.siem.detectionEngine.rules.allRules.batchActionsTitle',
{
defaultMessage: 'Batch actions',
defaultMessage: 'Bulk actions',
}
);
@ -243,7 +243,7 @@ export const COLUMN_TAGS = i18n.translate(
export const COLUMN_ACTIVATE = i18n.translate(
'xpack.siem.detectionEngine.rules.allRules.columns.activateTitle',
{
defaultMessage: 'Activate',
defaultMessage: 'Activated',
}
);

View file

@ -6,8 +6,8 @@
import { i18n } from '@kbn/i18n';
export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.pageTitle', {
defaultMessage: 'Detection engine',
export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.detectionsPageTitle', {
defaultMessage: 'Detections',
});
export const LAST_SIGNAL = i18n.translate('xpack.siem.detectionEngine.lastSignalTitle', {

View file

@ -36,12 +36,12 @@ export const navTabs: SiemNavTab = {
disabled: false,
urlKey: 'network',
},
[SiemPageName.detectionEngine]: {
id: SiemPageName.detectionEngine,
[SiemPageName.detections]: {
id: SiemPageName.detections,
name: i18n.DETECTION_ENGINE,
href: getDetectionEngineUrl(),
disabled: false,
urlKey: 'detection-engine',
urlKey: 'detections',
},
[SiemPageName.timelines]: {
id: SiemPageName.timelines,

View file

@ -105,7 +105,7 @@ export const HomePage: React.FC = () => (
)}
/>
<Route
path={`/:pageName(${SiemPageName.detectionEngine})`}
path={`/:pageName(${SiemPageName.detections})`}
render={({ location, match }) => (
<DetectionEngineContainer location={location} url={match.url} />
)}

View file

@ -19,7 +19,7 @@ export const NETWORK = i18n.translate('xpack.siem.navigation.network', {
});
export const DETECTION_ENGINE = i18n.translate('xpack.siem.navigation.detectionEngine', {
defaultMessage: 'Detection engine',
defaultMessage: 'Detections',
});
export const TIMELINES = i18n.translate('xpack.siem.navigation.timelines', {

View file

@ -10,7 +10,7 @@ export enum SiemPageName {
overview = 'overview',
hosts = 'hosts',
network = 'network',
detectionEngine = 'detection-engine',
detections = 'detections',
timelines = 'timelines',
}
@ -18,7 +18,7 @@ export type SiemNavTabKey =
| SiemPageName.overview
| SiemPageName.hosts
| SiemPageName.network
| SiemPageName.detectionEngine
| SiemPageName.detections
| SiemPageName.timelines;
export type SiemNavTab = Record<SiemNavTabKey, NavTab>;