[SIEM] Adds Signals Histogram (#53742)
## Summary Detection Engine Meta Issue: #50405 This PR adds the `Signals Histogram` component for use on the main `Detection Engine` page, `Rule Details` page, and the newly designed `Overview` page. Out of the box configuration includes an `EuiSelect` for stacking by the following: * Risk Scores * Severities * Event Actions * Event Categories * Host Names * Rule Types * Rules * Users * Destination IPs * Source IPs Additional configuration properties are available to configure the component as needed depending on where it will be displayed (e.g. no `Stack By` option on `Overview`, filter to specific `rule_id` on `Rule Details`, etc): ``` ts interface SignalsHistogramPanelProps { defaultStackByOption?: SignalsHistogramOption; filters?: esFilters.Filter[]; from: number; query?: Query; legendPosition?: 'left' | 'right' | 'bottom' | 'top'; loadingInitial?: boolean; showLinkToSignals?: boolean; showTotalSignalsCount?: boolean; stackByOptions?: SignalsHistogramOption[]; title?: string; to: number; updateDateRange: (min: number, max: number) => void; } ``` ##### Light Theme: ![de_hist_light](https://user-images.githubusercontent.com/2946766/71299977-41685800-234e-11ea-93bd-05a0c4cb6ee1.gif) ##### Dark Theme: ![de_histogram_dark](https://user-images.githubusercontent.com/2946766/71299980-45947580-234e-11ea-9d26-380bae5c4aa6.gif) ##### Overview: Example props for overview impl: ``` jsx <SignalsHistogramPanel filters={filters} from={from} loadingInitial={loading} query={query} showTotalSignalsCount={true} showLinkToSignals={true} defaultStackByOption={{ text: 'Signals count by MITRE ATT&CK category', value: 'signal.rule.threats', }} legendPosition={'right'} to={to} title="Signals count by MITRE ATT&CK category" updateDateRange={updateDateRangeCallback} /> ``` ![image](https://user-images.githubusercontent.com/2946766/72030438-2fd7e900-3246-11ea-8404-40905ca5f85c.png) Note @andrew-goldstein @angorayc @MichaelMarcialis -- looks like the MITRE ATT&CK Tactics are stored as a nested object in `signal.rule.threat`, so we may have to do some finangling to get it to show on the histogram. e.g. format: ``` json { "framework": "MITRE ATT&CK", "tactic": { "id": "TA0010", "reference": "https://attack.mitre.org/tactics/TA0010", "name": "Exfiltration" }, "techniques": [ { "id": "T1002", "name": "Data Compressed", "reference": "https://attack.mitre.org/techniques/T1002" } ] } ``` ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. - [ ] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) - [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials * Will work with @benskelker on any specific documentation - [ ] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios - [ ] ~This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~ ### For maintainers - [ ] ~This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~ - [ ] ~This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~
This commit is contained in:
parent
68883c6333
commit
482faae799
|
@ -1,3 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`HistogramSignals it renders 1`] = `<HistogramSignals />`;
|
|
@ -1,23 +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 { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import { TestProviders } from '../../../../mock';
|
||||
import { HistogramSignals } from './index';
|
||||
|
||||
describe('HistogramSignals', () => {
|
||||
test('it renders', () => {
|
||||
const wrapper = shallow(
|
||||
<TestProviders>
|
||||
<HistogramSignals />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('HistogramSignals')).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -1,79 +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 {
|
||||
Axis,
|
||||
Chart,
|
||||
HistogramBarSeries,
|
||||
Settings,
|
||||
niceTimeFormatByDay,
|
||||
timeFormatter,
|
||||
} from '@elastic/charts';
|
||||
import React from 'react';
|
||||
import { npStart } from 'ui/new_platform';
|
||||
|
||||
export const HistogramSignals = React.memo(() => {
|
||||
const sampleChartData = [
|
||||
{ x: 1571090784000, y: 2, a: 'a' },
|
||||
{ x: 1571090784000, y: 2, b: 'b' },
|
||||
{ x: 1571093484000, y: 7, a: 'a' },
|
||||
{ x: 1571096184000, y: 3, a: 'a' },
|
||||
{ x: 1571098884000, y: 2, a: 'a' },
|
||||
{ x: 1571101584000, y: 7, a: 'a' },
|
||||
{ x: 1571104284000, y: 3, a: 'a' },
|
||||
{ x: 1571106984000, y: 2, a: 'a' },
|
||||
{ x: 1571109684000, y: 7, a: 'a' },
|
||||
{ x: 1571112384000, y: 3, a: 'a' },
|
||||
{ x: 1571115084000, y: 2, a: 'a' },
|
||||
{ x: 1571117784000, y: 7, a: 'a' },
|
||||
{ x: 1571120484000, y: 3, a: 'a' },
|
||||
{ x: 1571123184000, y: 2, a: 'a' },
|
||||
{ x: 1571125884000, y: 7, a: 'a' },
|
||||
{ x: 1571128584000, y: 3, a: 'a' },
|
||||
{ x: 1571131284000, y: 2, a: 'a' },
|
||||
{ x: 1571133984000, y: 7, a: 'a' },
|
||||
{ x: 1571136684000, y: 3, a: 'a' },
|
||||
{ x: 1571139384000, y: 2, a: 'a' },
|
||||
{ x: 1571142084000, y: 7, a: 'a' },
|
||||
{ x: 1571144784000, y: 3, a: 'a' },
|
||||
{ x: 1571147484000, y: 2, a: 'a' },
|
||||
{ x: 1571150184000, y: 7, a: 'a' },
|
||||
{ x: 1571152884000, y: 3, a: 'a' },
|
||||
{ x: 1571155584000, y: 2, a: 'a' },
|
||||
{ x: 1571158284000, y: 7, a: 'a' },
|
||||
{ x: 1571160984000, y: 3, a: 'a' },
|
||||
{ x: 1571163684000, y: 2, a: 'a' },
|
||||
{ x: 1571166384000, y: 7, a: 'a' },
|
||||
{ x: 1571169084000, y: 3, a: 'a' },
|
||||
{ x: 1571171784000, y: 2, a: 'a' },
|
||||
{ x: 1571174484000, y: 7, a: 'a' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Chart size={['100%', 259]}>
|
||||
<Settings
|
||||
legendPosition="bottom"
|
||||
showLegend
|
||||
theme={npStart.plugins.eui_utils.useChartsTheme()}
|
||||
/>
|
||||
|
||||
<Axis id="signalAxisX" position="bottom" tickFormat={timeFormatter(niceTimeFormatByDay(1))} />
|
||||
|
||||
<Axis id="signalAxisY" position="left" />
|
||||
|
||||
<HistogramBarSeries
|
||||
id="signalBar"
|
||||
xScaleType="time"
|
||||
yScaleType="linear"
|
||||
xAccessor="x"
|
||||
yAccessors={['y']}
|
||||
splitSeriesAccessors={['a', 'b']}
|
||||
data={sampleChartData}
|
||||
/>
|
||||
</Chart>
|
||||
);
|
||||
});
|
||||
HistogramSignals.displayName = 'HistogramSignals';
|
|
@ -42,7 +42,7 @@ export const fetchQuerySignals = async <Hit, Aggregations>({
|
|||
'content-type': 'application/json',
|
||||
'kbn-xsrf': 'true',
|
||||
},
|
||||
body: query,
|
||||
body: JSON.stringify(query),
|
||||
signal,
|
||||
});
|
||||
await throwIfNotOk(response);
|
||||
|
|
|
@ -10,7 +10,7 @@ export interface BasicSignals {
|
|||
signal: AbortSignal;
|
||||
}
|
||||
export interface QuerySignals extends BasicSignals {
|
||||
query: string;
|
||||
query: object;
|
||||
}
|
||||
|
||||
export interface SignalsResponse {
|
||||
|
@ -18,7 +18,8 @@ export interface SignalsResponse {
|
|||
timeout: boolean;
|
||||
}
|
||||
|
||||
export interface SignalSearchResponse<Hit = {}, Aggregations = undefined> extends SignalsResponse {
|
||||
export interface SignalSearchResponse<Hit = {}, Aggregations = {} | undefined>
|
||||
extends SignalsResponse {
|
||||
_shards: {
|
||||
total: number;
|
||||
successful: number;
|
||||
|
|
|
@ -4,20 +4,25 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import React, { SetStateAction, useEffect, useState } from 'react';
|
||||
|
||||
import { fetchQuerySignals } from './api';
|
||||
import { SignalSearchResponse } from './types';
|
||||
|
||||
type Return<Hit, Aggs> = [boolean, SignalSearchResponse<Hit, Aggs> | null];
|
||||
type Return<Hit, Aggs> = [
|
||||
boolean,
|
||||
SignalSearchResponse<Hit, Aggs> | null,
|
||||
React.Dispatch<SetStateAction<object>>
|
||||
];
|
||||
|
||||
/**
|
||||
* Hook for using to get a Signals from the Detection Engine API
|
||||
*
|
||||
* @param query convert a dsl into string
|
||||
* @param initialQuery query dsl object
|
||||
*
|
||||
*/
|
||||
export const useQuerySignals = <Hit, Aggs>(query: string): Return<Hit, Aggs> => {
|
||||
export const useQuerySignals = <Hit, Aggs>(initialQuery: object): Return<Hit, Aggs> => {
|
||||
const [query, setQuery] = useState(initialQuery);
|
||||
const [signals, setSignals] = useState<SignalSearchResponse<Hit, Aggs> | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
|
@ -53,5 +58,5 @@ export const useQuerySignals = <Hit, Aggs>(query: string): Return<Hit, Aggs> =>
|
|||
};
|
||||
}, [query]);
|
||||
|
||||
return [loading, signals];
|
||||
return [loading, signals, setQuery];
|
||||
};
|
||||
|
|
|
@ -1,41 +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 { 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' },
|
||||
];
|
||||
|
||||
const SignalsChartsComponent = () => (
|
||||
<EuiPanel>
|
||||
<HeaderSection title="Signal detection frequency">
|
||||
<EuiSelect
|
||||
options={sampleChartOptions}
|
||||
onChange={() => noop}
|
||||
prepend="Stack by"
|
||||
value={sampleChartOptions[0].value}
|
||||
/>
|
||||
</HeaderSection>
|
||||
|
||||
<HistogramSignals />
|
||||
</EuiPanel>
|
||||
);
|
||||
|
||||
export const SignalsCharts = memo(SignalsChartsComponent);
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 * as i18n from './translations';
|
||||
import { SignalsHistogramOption } from './types';
|
||||
|
||||
export const signalsHistogramOptions: SignalsHistogramOption[] = [
|
||||
{ text: i18n.STACK_BY_RISK_SCORES, value: 'signal.rule.risk_score' },
|
||||
{ text: i18n.STACK_BY_SEVERITIES, value: 'signal.rule.severity' },
|
||||
{ text: i18n.STACK_BY_DESTINATION_IPS, value: 'destination.ip' },
|
||||
{ text: i18n.STACK_BY_ACTIONS, value: 'event.action' },
|
||||
{ text: i18n.STACK_BY_CATEGORIES, value: 'event.category' },
|
||||
{ text: i18n.STACK_BY_HOST_NAMES, value: 'host.name' },
|
||||
{ text: i18n.STACK_BY_RULE_TYPES, value: 'signal.rule.type' },
|
||||
{ text: i18n.STACK_BY_RULE_NAMES, value: 'signal.rule.name' },
|
||||
{ text: i18n.STACK_BY_SOURCE_IPS, value: 'source.ip' },
|
||||
{ text: i18n.STACK_BY_USERS, value: 'user.name' },
|
||||
];
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* 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 { Position } from '@elastic/charts';
|
||||
import { EuiButton, EuiPanel, EuiSelect } from '@elastic/eui';
|
||||
import numeral from '@elastic/numeral';
|
||||
import React, { memo, useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { HeaderSection } from '../../../../components/header_section';
|
||||
import { SignalsHistogram } from './signals_histogram';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { Query } from '../../../../../../../../../src/plugins/data/common/query';
|
||||
import { esFilters } from '../../../../../../../../../src/plugins/data/common/es_query';
|
||||
import { SignalsHistogramOption, SignalsTotal } from './types';
|
||||
import { signalsHistogramOptions } from './config';
|
||||
import { getDetectionEngineUrl } from '../../../../components/link_to';
|
||||
import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants';
|
||||
import { useUiSetting$ } from '../../../../lib/kibana';
|
||||
|
||||
const defaultTotalSignalsObj: SignalsTotal = {
|
||||
value: 0,
|
||||
relation: 'eq',
|
||||
};
|
||||
|
||||
interface SignalsHistogramPanelProps {
|
||||
defaultStackByOption?: SignalsHistogramOption;
|
||||
filters?: esFilters.Filter[];
|
||||
from: number;
|
||||
query?: Query;
|
||||
legendPosition?: Position;
|
||||
loadingInitial?: boolean;
|
||||
showLinkToSignals?: boolean;
|
||||
showTotalSignalsCount?: boolean;
|
||||
stackByOptions?: SignalsHistogramOption[];
|
||||
title?: string;
|
||||
to: number;
|
||||
updateDateRange: (min: number, max: number) => void;
|
||||
}
|
||||
|
||||
export const SignalsHistogramPanel = memo<SignalsHistogramPanelProps>(
|
||||
({
|
||||
defaultStackByOption = signalsHistogramOptions[0],
|
||||
filters,
|
||||
query,
|
||||
from,
|
||||
legendPosition = 'bottom',
|
||||
loadingInitial = false,
|
||||
showLinkToSignals = false,
|
||||
showTotalSignalsCount = false,
|
||||
stackByOptions,
|
||||
to,
|
||||
title = i18n.HISTOGRAM_HEADER,
|
||||
updateDateRange,
|
||||
}) => {
|
||||
const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT);
|
||||
const [totalSignalsObj, setTotalSignalsObj] = useState<SignalsTotal>(defaultTotalSignalsObj);
|
||||
const [selectedStackByOption, setSelectedStackByOption] = useState<SignalsHistogramOption>(
|
||||
defaultStackByOption
|
||||
);
|
||||
|
||||
const totalSignals = useMemo(
|
||||
() =>
|
||||
i18n.SHOWING_SIGNALS(
|
||||
numeral(totalSignalsObj.value).format(defaultNumberFormat),
|
||||
totalSignalsObj.value,
|
||||
totalSignalsObj.relation === 'gte' ? '>' : totalSignalsObj.relation === 'lte' ? '<' : ''
|
||||
),
|
||||
[totalSignalsObj]
|
||||
);
|
||||
|
||||
const setSelectedOptionCallback = useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setSelectedStackByOption(
|
||||
stackByOptions?.find(co => co.value === event.target.value) ?? defaultStackByOption
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EuiPanel>
|
||||
<HeaderSection title={title} subtitle={showTotalSignalsCount && totalSignals}>
|
||||
{stackByOptions && (
|
||||
<EuiSelect
|
||||
onChange={setSelectedOptionCallback}
|
||||
options={stackByOptions}
|
||||
prepend={i18n.STACK_BY_LABEL}
|
||||
value={selectedStackByOption.value}
|
||||
/>
|
||||
)}
|
||||
{showLinkToSignals && (
|
||||
<EuiButton href={getDetectionEngineUrl()}>{i18n.VIEW_SIGNALS}</EuiButton>
|
||||
)}
|
||||
</HeaderSection>
|
||||
|
||||
<SignalsHistogram
|
||||
filters={filters}
|
||||
from={from}
|
||||
legendPosition={legendPosition}
|
||||
loadingInitial={loadingInitial}
|
||||
query={query}
|
||||
to={to}
|
||||
setTotalSignalsCount={setTotalSignalsObj}
|
||||
stackByField={selectedStackByOption.value}
|
||||
updateDateRange={updateDateRange}
|
||||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
SignalsHistogramPanel.displayName = 'SignalsHistogramPanel';
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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 { HistogramData, SignalsAggregation, SignalsBucket, SignalsGroupBucket } from '../types';
|
||||
import { SignalSearchResponse } from '../../../../../containers/detection_engine/signals/types';
|
||||
import * as i18n from '../translations';
|
||||
|
||||
export const formatSignalsData = (
|
||||
signalsData: SignalSearchResponse<{}, SignalsAggregation> | null
|
||||
) => {
|
||||
const groupBuckets: SignalsGroupBucket[] =
|
||||
signalsData?.aggregations?.signalsByGrouping?.buckets ?? [];
|
||||
return groupBuckets.reduce<HistogramData[]>((acc, { key: group, signals }) => {
|
||||
const signalsBucket: SignalsBucket[] = signals.buckets ?? [];
|
||||
|
||||
return [
|
||||
...acc,
|
||||
...signalsBucket.map(({ key, doc_count }: SignalsBucket) => ({
|
||||
x: key,
|
||||
y: doc_count,
|
||||
g: group,
|
||||
})),
|
||||
];
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const getSignalsHistogramQuery = (
|
||||
stackByField: string,
|
||||
from: number,
|
||||
to: number,
|
||||
additionalFilters: Array<{
|
||||
bool: { filter: unknown[]; should: unknown[]; must_not: unknown[]; must: unknown[] };
|
||||
}>
|
||||
) => ({
|
||||
aggs: {
|
||||
signalsByGrouping: {
|
||||
terms: {
|
||||
field: stackByField,
|
||||
missing: stackByField.endsWith('.ip') ? '0.0.0.0' : i18n.ALL_OTHERS,
|
||||
order: {
|
||||
_count: 'desc',
|
||||
},
|
||||
size: 10,
|
||||
},
|
||||
aggs: {
|
||||
signals: {
|
||||
auto_date_histogram: {
|
||||
field: '@timestamp',
|
||||
buckets: 36,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...additionalFilters,
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: from,
|
||||
lte: to,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* 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 {
|
||||
Axis,
|
||||
Chart,
|
||||
getAxisId,
|
||||
getSpecId,
|
||||
HistogramBarSeries,
|
||||
niceTimeFormatByDay,
|
||||
Position,
|
||||
Settings,
|
||||
timeFormatter,
|
||||
} from '@elastic/charts';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { EuiLoadingContent } from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import { useQuerySignals } from '../../../../../containers/detection_engine/signals/use_query';
|
||||
import { Query } from '../../../../../../../../../../src/plugins/data/common/query';
|
||||
import { esFilters, esQuery } from '../../../../../../../../../../src/plugins/data/common/es_query';
|
||||
import { SignalsAggregation, SignalsTotal } from '../types';
|
||||
import { formatSignalsData, getSignalsHistogramQuery } from './helpers';
|
||||
import { useTheme } from '../../../../../components/charts/common';
|
||||
import { useKibana } from '../../../../../lib/kibana';
|
||||
|
||||
interface HistogramSignalsProps {
|
||||
filters?: esFilters.Filter[];
|
||||
from: number;
|
||||
legendPosition?: Position;
|
||||
loadingInitial: boolean;
|
||||
query?: Query;
|
||||
setTotalSignalsCount: React.Dispatch<SignalsTotal>;
|
||||
stackByField: string;
|
||||
to: number;
|
||||
updateDateRange: (min: number, max: number) => void;
|
||||
}
|
||||
|
||||
export const SignalsHistogram = React.memo<HistogramSignalsProps>(
|
||||
({
|
||||
to,
|
||||
from,
|
||||
query,
|
||||
filters,
|
||||
legendPosition = 'bottom',
|
||||
loadingInitial,
|
||||
setTotalSignalsCount,
|
||||
stackByField,
|
||||
updateDateRange,
|
||||
}) => {
|
||||
const [isLoadingSignals, signalsData, setQuery] = useQuerySignals<{}, SignalsAggregation>(
|
||||
getSignalsHistogramQuery(stackByField, from, to, [])
|
||||
);
|
||||
const theme = useTheme();
|
||||
const kibana = useKibana();
|
||||
|
||||
const formattedSignalsData = useMemo(() => formatSignalsData(signalsData), [signalsData]);
|
||||
|
||||
useEffect(() => {
|
||||
setTotalSignalsCount(
|
||||
signalsData?.hits.total ?? {
|
||||
value: 0,
|
||||
relation: 'eq',
|
||||
}
|
||||
);
|
||||
}, [signalsData]);
|
||||
|
||||
useEffect(() => {
|
||||
const converted = esQuery.buildEsQuery(
|
||||
undefined,
|
||||
query != null ? [query] : [],
|
||||
filters?.filter(f => f.meta.disabled === false) ?? [],
|
||||
{
|
||||
...esQuery.getEsQueryConfig(kibana.services.uiSettings),
|
||||
dateFormatTZ: undefined,
|
||||
}
|
||||
);
|
||||
|
||||
setQuery(
|
||||
getSignalsHistogramQuery(stackByField, from, to, !isEmpty(converted) ? [converted] : [])
|
||||
);
|
||||
}, [stackByField, from, to, query, filters]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{loadingInitial || isLoadingSignals ? (
|
||||
<EuiLoadingContent data-test-subj="loadingPanelSignalsHistogram" lines={10} />
|
||||
) : (
|
||||
<Chart size={['100%', 259]}>
|
||||
<Settings
|
||||
legendPosition={legendPosition}
|
||||
onBrushEnd={updateDateRange}
|
||||
showLegend
|
||||
theme={theme}
|
||||
/>
|
||||
|
||||
<Axis
|
||||
id={getAxisId('signalsHistogramAxisX')}
|
||||
position="bottom"
|
||||
tickFormat={timeFormatter(niceTimeFormatByDay(1))}
|
||||
/>
|
||||
|
||||
<Axis id={getAxisId('signalsHistogramAxisY')} position="left" />
|
||||
|
||||
<HistogramBarSeries
|
||||
id={getSpecId('signalsHistogram')}
|
||||
xScaleType="time"
|
||||
yScaleType="linear"
|
||||
xAccessor="x"
|
||||
yAccessors={['y']}
|
||||
splitSeriesAccessors={['g']}
|
||||
data={formattedSignalsData}
|
||||
/>
|
||||
</Chart>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
SignalsHistogram.displayName = 'SignalsHistogram';
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* 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 STACK_BY_LABEL = i18n.translate(
|
||||
'xpack.siem.detectionEngine.signals.histogram.stackByOptions.stackByLabel',
|
||||
{
|
||||
defaultMessage: 'Stack by',
|
||||
}
|
||||
);
|
||||
|
||||
export const STACK_BY_RISK_SCORES = i18n.translate(
|
||||
'xpack.siem.detectionEngine.signals.histogram.stackByOptions.riskScoresDropDown',
|
||||
{
|
||||
defaultMessage: 'Risk scores',
|
||||
}
|
||||
);
|
||||
|
||||
export const STACK_BY_SEVERITIES = i18n.translate(
|
||||
'xpack.siem.detectionEngine.signals.histogram.stackByOptions.severitiesDropDown',
|
||||
{
|
||||
defaultMessage: 'Severities',
|
||||
}
|
||||
);
|
||||
|
||||
export const STACK_BY_DESTINATION_IPS = i18n.translate(
|
||||
'xpack.siem.detectionEngine.signals.histogram.stackByOptions.destinationIpsDropDown',
|
||||
{
|
||||
defaultMessage: 'Top destination IPs',
|
||||
}
|
||||
);
|
||||
|
||||
export const STACK_BY_SOURCE_IPS = i18n.translate(
|
||||
'xpack.siem.detectionEngine.signals.histogram.stackByOptions.sourceIpsDropDown',
|
||||
{
|
||||
defaultMessage: 'Top source IPs',
|
||||
}
|
||||
);
|
||||
|
||||
export const STACK_BY_ACTIONS = i18n.translate(
|
||||
'xpack.siem.detectionEngine.signals.histogram.stackByOptions.eventActionsDropDown',
|
||||
{
|
||||
defaultMessage: 'Top event actions',
|
||||
}
|
||||
);
|
||||
|
||||
export const STACK_BY_CATEGORIES = i18n.translate(
|
||||
'xpack.siem.detectionEngine.signals.histogram.stackByOptions.eventCategoriesDropDown',
|
||||
{
|
||||
defaultMessage: 'Top event categories',
|
||||
}
|
||||
);
|
||||
|
||||
export const STACK_BY_HOST_NAMES = i18n.translate(
|
||||
'xpack.siem.detectionEngine.signals.histogram.stackByOptions.hostNamesDropDown',
|
||||
{
|
||||
defaultMessage: 'Top host names',
|
||||
}
|
||||
);
|
||||
|
||||
export const STACK_BY_RULE_TYPES = i18n.translate(
|
||||
'xpack.siem.detectionEngine.signals.histogram.stackByOptions.ruleTypesDropDown',
|
||||
{
|
||||
defaultMessage: 'Top rule types',
|
||||
}
|
||||
);
|
||||
|
||||
export const STACK_BY_RULE_NAMES = i18n.translate(
|
||||
'xpack.siem.detectionEngine.signals.histogram.stackByOptions.rulesDropDown',
|
||||
{
|
||||
defaultMessage: 'Top rules',
|
||||
}
|
||||
);
|
||||
|
||||
export const STACK_BY_USERS = i18n.translate(
|
||||
'xpack.siem.detectionEngine.signals.histogram.stackByOptions.usersDropDown',
|
||||
{
|
||||
defaultMessage: 'Top users',
|
||||
}
|
||||
);
|
||||
|
||||
export const HISTOGRAM_HEADER = i18n.translate(
|
||||
'xpack.siem.detectionEngine.signals.histogram.headerTitle',
|
||||
{
|
||||
defaultMessage: 'Signal detection frequency',
|
||||
}
|
||||
);
|
||||
|
||||
export const ALL_OTHERS = i18n.translate(
|
||||
'xpack.siem.detectionEngine.signals.histogram.allOthersGroupingLabel',
|
||||
{
|
||||
defaultMessage: 'All others',
|
||||
}
|
||||
);
|
||||
|
||||
export const VIEW_SIGNALS = i18n.translate(
|
||||
'xpack.siem.detectionEngine.signals.histogram.viewSignalsButtonLabel',
|
||||
{
|
||||
defaultMessage: 'View signals',
|
||||
}
|
||||
);
|
||||
|
||||
export const SHOWING_SIGNALS = (
|
||||
totalSignalsFormatted: string,
|
||||
totalSignals: number,
|
||||
modifier: string
|
||||
) =>
|
||||
i18n.translate('xpack.siem.detectionEngine.signals.histogram.showingSignalsTitle', {
|
||||
values: { totalSignalsFormatted, totalSignals, modifier },
|
||||
defaultMessage:
|
||||
'Showing: {modifier}{totalSignalsFormatted} {totalSignals, plural, =1 {signal} other {signals}}',
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 SignalsHistogramOption {
|
||||
text: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface HistogramData {
|
||||
x: number;
|
||||
y: number;
|
||||
g: string;
|
||||
}
|
||||
|
||||
export interface SignalsAggregation {
|
||||
signalsByGrouping: {
|
||||
buckets: SignalsGroupBucket[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface SignalsBucket {
|
||||
key_as_string: string;
|
||||
key: number;
|
||||
doc_count: number;
|
||||
}
|
||||
|
||||
export interface SignalsGroupBucket {
|
||||
key: string;
|
||||
signals: {
|
||||
buckets: SignalsBucket[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface SignalsTotal {
|
||||
value: number;
|
||||
relation: string;
|
||||
}
|
|
@ -9,7 +9,7 @@ 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 { buildLastSignalsQuery } from './query.dsl';
|
||||
import { Aggs } from './types';
|
||||
|
||||
interface SignalInfo {
|
||||
|
@ -26,14 +26,7 @@ export const useSignalInfo = ({ ruleId = null }: SignalInfo): Return => {
|
|||
<EuiLoadingSpinner size="m" />
|
||||
);
|
||||
|
||||
let query = '';
|
||||
try {
|
||||
query = JSON.stringify(buildlastSignalsQuery(ruleId));
|
||||
} catch {
|
||||
query = '';
|
||||
}
|
||||
|
||||
const [loading, signals] = useQuerySignals<unknown, Aggs>(query);
|
||||
const [loading, signals] = useQuerySignals<unknown, Aggs>(buildLastSignalsQuery(ruleId));
|
||||
|
||||
useEffect(() => {
|
||||
if (signals != null) {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const buildlastSignalsQuery = (ruleId: string | undefined | null) => {
|
||||
export const buildLastSignalsQuery = (ruleId: string | undefined | null) => {
|
||||
const queryFilter = [
|
||||
{
|
||||
bool: { should: [{ match: { 'signal.status': 'open' } }], minimum_should_match: 1 },
|
||||
|
|
|
@ -4,10 +4,12 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiButton, EuiSpacer, EuiPanel, EuiLoadingContent } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { EuiButton, EuiLoadingContent, EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
import { StickyContainer } from 'react-sticky';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { ActionCreator } from 'typescript-fsa';
|
||||
import { FiltersGlobal } from '../../components/filters_global';
|
||||
import { HeaderPage } from '../../components/header_page';
|
||||
import { SiemSearchBar } from '../../components/search_bar';
|
||||
|
@ -18,24 +20,63 @@ import { SpyRoute } from '../../utils/route/spy_routes';
|
|||
|
||||
import { SignalsTable } from './components/signals';
|
||||
import * as signalsI18n from './components/signals/translations';
|
||||
import { SignalsCharts } from './components/signals_chart';
|
||||
import { SignalsHistogramPanel } from './components/signals_histogram_panel';
|
||||
import { Query } from '../../../../../../../src/plugins/data/common/query';
|
||||
import { esFilters } from '../../../../../../../src/plugins/data/common/es_query';
|
||||
import { inputsSelectors } from '../../store/inputs';
|
||||
import { State } from '../../store';
|
||||
import { InputsRange } from '../../store/inputs/model';
|
||||
import { signalsHistogramOptions } from './components/signals_histogram_panel/config';
|
||||
import { useSignalInfo } from './components/signals_info';
|
||||
import { DetectionEngineEmptyPage } from './detection_engine_empty_page';
|
||||
import { DetectionEngineNoIndex } from './detection_engine_no_signal_index';
|
||||
import { DetectionEngineUserUnauthenticated } from './detection_engine_user_unauthenticated';
|
||||
import * as i18n from './translations';
|
||||
import { HeaderSection } from '../../components/header_section';
|
||||
import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions';
|
||||
import { InputsModelId } from '../../store/inputs/constants';
|
||||
|
||||
interface DetectionEngineComponentProps {
|
||||
interface OwnProps {
|
||||
loading: boolean;
|
||||
isSignalIndexExists: boolean | null;
|
||||
isUserAuthenticated: boolean | null;
|
||||
signalsIndex: string | null;
|
||||
}
|
||||
|
||||
interface ReduxProps {
|
||||
filters: esFilters.Filter[];
|
||||
query: Query;
|
||||
}
|
||||
|
||||
export interface DispatchProps {
|
||||
setAbsoluteRangeDatePicker: ActionCreator<{
|
||||
id: InputsModelId;
|
||||
from: number;
|
||||
to: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
type DetectionEngineComponentProps = OwnProps & ReduxProps & DispatchProps;
|
||||
|
||||
export const DetectionEngineComponent = React.memo<DetectionEngineComponentProps>(
|
||||
({ loading, isSignalIndexExists, isUserAuthenticated, signalsIndex }) => {
|
||||
({
|
||||
filters,
|
||||
loading,
|
||||
isSignalIndexExists,
|
||||
isUserAuthenticated,
|
||||
query,
|
||||
setAbsoluteRangeDatePicker,
|
||||
signalsIndex,
|
||||
}) => {
|
||||
const [lastSignals] = useSignalInfo({});
|
||||
|
||||
const updateDateRangeCallback = useCallback(
|
||||
(min: number, max: number) => {
|
||||
setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max });
|
||||
},
|
||||
[setAbsoluteRangeDatePicker]
|
||||
);
|
||||
|
||||
if (isUserAuthenticated != null && !isUserAuthenticated && !loading) {
|
||||
return (
|
||||
<WrapperPage>
|
||||
|
@ -81,22 +122,33 @@ export const DetectionEngineComponent = React.memo<DetectionEngineComponentProps
|
|||
</EuiButton>
|
||||
</HeaderPage>
|
||||
|
||||
<SignalsCharts />
|
||||
|
||||
<EuiSpacer />
|
||||
<GlobalTime>
|
||||
{({ to, from }) =>
|
||||
!loading ? (
|
||||
isSignalIndexExists && (
|
||||
<SignalsTable from={from} signalsIndex={signalsIndex ?? ''} to={to} />
|
||||
)
|
||||
) : (
|
||||
<EuiPanel>
|
||||
<HeaderSection title={signalsI18n.SIGNALS_TABLE_TITLE} />
|
||||
<EuiLoadingContent />
|
||||
</EuiPanel>
|
||||
)
|
||||
}
|
||||
{({ to, from }) => (
|
||||
<>
|
||||
<SignalsHistogramPanel
|
||||
filters={filters}
|
||||
from={from}
|
||||
loadingInitial={loading}
|
||||
query={query}
|
||||
stackByOptions={signalsHistogramOptions}
|
||||
to={to}
|
||||
updateDateRange={updateDateRangeCallback}
|
||||
/>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
{!loading ? (
|
||||
isSignalIndexExists && (
|
||||
<SignalsTable from={from} signalsIndex={signalsIndex ?? ''} to={to} />
|
||||
)
|
||||
) : (
|
||||
<EuiPanel>
|
||||
<HeaderSection title={signalsI18n.SIGNALS_TABLE_TITLE} />
|
||||
<EuiLoadingContent />
|
||||
</EuiPanel>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</GlobalTime>
|
||||
</WrapperPage>
|
||||
</StickyContainer>
|
||||
|
@ -115,3 +167,22 @@ export const DetectionEngineComponent = React.memo<DetectionEngineComponentProps
|
|||
}
|
||||
);
|
||||
DetectionEngineComponent.displayName = 'DetectionEngineComponent';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getGlobalInputs = inputsSelectors.globalSelector();
|
||||
return (state: State) => {
|
||||
const globalInputs: InputsRange = getGlobalInputs(state);
|
||||
const { query, filters } = globalInputs;
|
||||
|
||||
return {
|
||||
query,
|
||||
filters,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export const DetectionEngine = connect(makeMapStateToProps, {
|
||||
setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker,
|
||||
})(DetectionEngineComponent);
|
||||
|
||||
DetectionEngine.displayName = 'DetectionEngine';
|
||||
|
|
|
@ -11,9 +11,9 @@ import { useSignalIndex } from '../../containers/detection_engine/signals/use_si
|
|||
import { usePrivilegeUser } from '../../containers/detection_engine/signals/use_privilege_user';
|
||||
|
||||
import { CreateRuleComponent } from './rules/create';
|
||||
import { DetectionEngineComponent } from './detection_engine';
|
||||
import { DetectionEngine } from './detection_engine';
|
||||
import { EditRuleComponent } from './rules/edit';
|
||||
import { RuleDetailsComponent } from './rules/details';
|
||||
import { RuleDetails } from './rules/details';
|
||||
import { RulesComponent } from './rules';
|
||||
|
||||
const detectionEnginePath = `/:pageName(detection-engine)`;
|
||||
|
@ -44,7 +44,7 @@ export const DetectionEngineContainer = React.memo<Props>(() => {
|
|||
return (
|
||||
<Switch>
|
||||
<Route exact path={detectionEnginePath} strict>
|
||||
<DetectionEngineComponent
|
||||
<DetectionEngine
|
||||
loading={indexNameLoading || privilegeLoading}
|
||||
isSignalIndexExists={isSignalIndexExists}
|
||||
isUserAuthenticated={isAuthenticated}
|
||||
|
@ -60,7 +60,7 @@ export const DetectionEngineContainer = React.memo<Props>(() => {
|
|||
<CreateRuleComponent />
|
||||
</Route>
|
||||
<Route exact path={`${detectionEnginePath}/rules/id/:ruleId`}>
|
||||
<RuleDetailsComponent signalsIndex={signalIndexName} />
|
||||
<RuleDetails signalsIndex={signalIndexName} />
|
||||
</Route>
|
||||
<Route exact path={`${detectionEnginePath}/rules/id/:ruleId/edit`}>
|
||||
<EditRuleComponent />
|
||||
|
|
|
@ -6,10 +6,12 @@
|
|||
|
||||
import { EuiButton, EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import React, { memo, useCallback, useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { StickyContainer } from 'react-sticky';
|
||||
|
||||
import { ActionCreator } from 'typescript-fsa';
|
||||
import { connect } from 'react-redux';
|
||||
import { FiltersGlobal } from '../../../../components/filters_global';
|
||||
import { FormattedDate } from '../../../../components/formatted_date';
|
||||
import { HeaderPage } from '../../../../components/header_page';
|
||||
|
@ -24,7 +26,7 @@ import {
|
|||
} from '../../../../containers/source';
|
||||
import { SpyRoute } from '../../../../utils/route/spy_routes';
|
||||
|
||||
import { SignalsCharts } from '../../components/signals_chart';
|
||||
import { SignalsHistogramPanel } from '../../components/signals_histogram_panel';
|
||||
import { SignalsTable } from '../../components/signals';
|
||||
import { DetectionEngineEmptyPage } from '../../detection_engine_empty_page';
|
||||
import { useSignalInfo } from '../../components/signals_info';
|
||||
|
@ -39,198 +41,261 @@ import { getStepsData } from '../helpers';
|
|||
import * as ruleI18n from '../translations';
|
||||
import * as i18n from './translations';
|
||||
import { GlobalTime } from '../../../../containers/global_time';
|
||||
import { signalsHistogramOptions } from '../../components/signals_histogram_panel/config';
|
||||
import { InputsModelId } from '../../../../store/inputs/constants';
|
||||
import { esFilters } from '../../../../../../../../../src/plugins/data/common/es_query';
|
||||
import { Query } from '../../../../../../../../../src/plugins/data/common/query';
|
||||
import { inputsSelectors } from '../../../../store/inputs';
|
||||
import { State } from '../../../../store';
|
||||
import { InputsRange } from '../../../../store/inputs/model';
|
||||
import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../../../store/inputs/actions';
|
||||
|
||||
interface RuleDetailsComponentProps {
|
||||
interface OwnProps {
|
||||
signalsIndex: string | null;
|
||||
}
|
||||
|
||||
export const RuleDetailsComponent = memo<RuleDetailsComponentProps>(({ signalsIndex }) => {
|
||||
const { ruleId } = useParams();
|
||||
const [loading, rule] = useRule(ruleId);
|
||||
const { aboutRuleData, defineRuleData, scheduleRuleData } = getStepsData({
|
||||
rule,
|
||||
detailsView: true,
|
||||
});
|
||||
const [lastSignals] = useSignalInfo({ ruleId });
|
||||
interface ReduxProps {
|
||||
filters: esFilters.Filter[];
|
||||
query: Query;
|
||||
}
|
||||
|
||||
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 ? (
|
||||
export interface DispatchProps {
|
||||
setAbsoluteRangeDatePicker: ActionCreator<{
|
||||
id: InputsModelId;
|
||||
from: number;
|
||||
to: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
type RuleDetailsComponentProps = OwnProps & ReduxProps & DispatchProps;
|
||||
|
||||
const RuleDetailsComponent = memo<RuleDetailsComponentProps>(
|
||||
({ filters, query, setAbsoluteRangeDatePicker, signalsIndex }) => {
|
||||
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.ruleUpdateDescription"
|
||||
defaultMessage="Updated by: {by} on {date}"
|
||||
id="xpack.siem.detectionEngine.ruleDetails.ruleCreationDescription"
|
||||
defaultMessage="Created by: {by} on {date}"
|
||||
values={{
|
||||
by: rule?.updated_by ?? i18n.UNKNOWN,
|
||||
by: rule?.created_by ?? i18n.UNKNOWN,
|
||||
date: (
|
||||
<FormattedDate
|
||||
value={rule?.updated_at ?? new Date().toISOString()}
|
||||
fieldName="updatedAt"
|
||||
value={rule?.created_at ?? new Date().toISOString()}
|
||||
fieldName="createdAt"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
),
|
||||
]
|
||||
),
|
||||
[loading, rule]
|
||||
);
|
||||
/>,
|
||||
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>
|
||||
const signalDefaultFilters = useMemo(
|
||||
() => (ruleId != null ? buildSignalsRuleIdFilter(ruleId) : []),
|
||||
[ruleId]
|
||||
);
|
||||
|
||||
<WrapperPage>
|
||||
<HeaderPage
|
||||
backOptions={{
|
||||
href: `#${DETECTION_ENGINE_PAGE_NAME}/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}
|
||||
/>
|
||||
const signalMergedFilters = useMemo(() => [...signalDefaultFilters, ...filters], [
|
||||
signalDefaultFilters,
|
||||
filters,
|
||||
]);
|
||||
|
||||
const updateDateRangeCallback = useCallback(
|
||||
(min: number, max: number) => {
|
||||
setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max });
|
||||
},
|
||||
[setAbsoluteRangeDatePicker]
|
||||
);
|
||||
|
||||
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_PAGE_NAME}/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/id/${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 grow={false}>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
href={`#${DETECTION_ENGINE_PAGE_NAME}/rules/id/${ruleId}/edit`}
|
||||
iconType="visControls"
|
||||
isDisabled={rule?.immutable ?? true}
|
||||
>
|
||||
{ruleI18n.EDIT_RULE_SETTINGS}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<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>
|
||||
</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
|
||||
defaultFilters={signalDefaultFilters}
|
||||
<EuiSpacer />
|
||||
<SignalsHistogramPanel
|
||||
filters={signalMergedFilters}
|
||||
query={query}
|
||||
from={from}
|
||||
signalsIndex={signalsIndex ?? ''}
|
||||
stackByOptions={signalsHistogramOptions}
|
||||
to={to}
|
||||
updateDateRange={updateDateRangeCallback}
|
||||
/>
|
||||
)}
|
||||
</WrapperPage>
|
||||
</StickyContainer>
|
||||
)}
|
||||
</GlobalTime>
|
||||
) : (
|
||||
<WrapperPage>
|
||||
<HeaderPage border title={i18n.PAGE_TITLE} />
|
||||
|
||||
<DetectionEngineEmptyPage />
|
||||
</WrapperPage>
|
||||
);
|
||||
}}
|
||||
</WithSource>
|
||||
<EuiSpacer />
|
||||
|
||||
<SpyRoute />
|
||||
</>
|
||||
);
|
||||
});
|
||||
{ruleId != null && (
|
||||
<SignalsTable
|
||||
defaultFilters={signalDefaultFilters}
|
||||
from={from}
|
||||
signalsIndex={signalsIndex ?? ''}
|
||||
to={to}
|
||||
/>
|
||||
)}
|
||||
</WrapperPage>
|
||||
</StickyContainer>
|
||||
)}
|
||||
</GlobalTime>
|
||||
) : (
|
||||
<WrapperPage>
|
||||
<HeaderPage border title={i18n.PAGE_TITLE} />
|
||||
|
||||
<DetectionEngineEmptyPage />
|
||||
</WrapperPage>
|
||||
);
|
||||
}}
|
||||
</WithSource>
|
||||
|
||||
<SpyRoute />
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
RuleDetailsComponent.displayName = 'RuleDetailsComponent';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getGlobalInputs = inputsSelectors.globalSelector();
|
||||
return (state: State) => {
|
||||
const globalInputs: InputsRange = getGlobalInputs(state);
|
||||
const { query, filters } = globalInputs;
|
||||
|
||||
return {
|
||||
query,
|
||||
filters,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export const RuleDetails = connect(makeMapStateToProps, {
|
||||
setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker,
|
||||
})(RuleDetailsComponent);
|
||||
|
||||
RuleDetails.displayName = 'RuleDetails';
|
||||
|
|
Loading…
Reference in a new issue