[SIEM] Detections bugs rules (#55885)
* Fix flow of all rules * fix the multitude http request + fix table timeline re-rendering * Update x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx Co-Authored-By: Garrett Spong <spong@users.noreply.github.com> Co-authored-by: Garrett Spong <spong@users.noreply.github.com>
This commit is contained in:
parent
5801de0800
commit
459b8c4df8
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { EuiPanel } from '@elastic/eui';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { getOr, isEmpty, isEqual, union } from 'lodash/fp';
|
||||
import React, { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
@ -34,6 +35,7 @@ import {
|
|||
IIndexPattern,
|
||||
Query,
|
||||
} from '../../../../../../../src/plugins/data/public';
|
||||
import { inputsModel } from '../../store';
|
||||
|
||||
const DEFAULT_EVENTS_VIEWER_HEIGHT = 500;
|
||||
|
||||
|
@ -67,7 +69,7 @@ interface Props {
|
|||
sort: Sort;
|
||||
timelineTypeContext: TimelineTypeContextProps;
|
||||
toggleColumn: (column: ColumnHeader) => void;
|
||||
utilityBar?: (totalCount: number) => React.ReactNode;
|
||||
utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode;
|
||||
}
|
||||
|
||||
const EventsViewerComponent: React.FC<Props> = ({
|
||||
|
@ -171,7 +173,7 @@ const EventsViewerComponent: React.FC<Props> = ({
|
|||
{headerFilterGroup}
|
||||
</HeaderSection>
|
||||
|
||||
{utilityBar?.(totalCountMinusDeleted)}
|
||||
{utilityBar?.(refetch, totalCountMinusDeleted)}
|
||||
|
||||
<div
|
||||
data-test-subj={`events-container-loading-${loading}`}
|
||||
|
@ -234,15 +236,15 @@ const EventsViewerComponent: React.FC<Props> = ({
|
|||
export const EventsViewer = React.memo(
|
||||
EventsViewerComponent,
|
||||
(prevProps, nextProps) =>
|
||||
prevProps.browserFields === nextProps.browserFields &&
|
||||
isEqual(prevProps.browserFields, nextProps.browserFields) &&
|
||||
prevProps.columns === nextProps.columns &&
|
||||
prevProps.dataProviders === nextProps.dataProviders &&
|
||||
prevProps.deletedEventIds === nextProps.deletedEventIds &&
|
||||
prevProps.end === nextProps.end &&
|
||||
isEqual(prevProps.filters, nextProps.filters) &&
|
||||
deepEqual(prevProps.filters, nextProps.filters) &&
|
||||
prevProps.height === nextProps.height &&
|
||||
prevProps.id === nextProps.id &&
|
||||
prevProps.indexPattern === nextProps.indexPattern &&
|
||||
deepEqual(prevProps.indexPattern, nextProps.indexPattern) &&
|
||||
prevProps.isLive === nextProps.isLive &&
|
||||
prevProps.itemsPerPage === nextProps.itemsPerPage &&
|
||||
prevProps.itemsPerPageOptions === nextProps.itemsPerPageOptions &&
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { isEqual } from 'lodash/fp';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import React, { useCallback, useMemo, useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { ActionCreator } from 'typescript-fsa';
|
||||
import { inputsModel, inputsSelectors, State, timelineSelectors } from '../../store';
|
||||
|
@ -35,7 +35,7 @@ export interface OwnProps {
|
|||
headerFilterGroup?: React.ReactNode;
|
||||
pageFilters?: esFilters.Filter[];
|
||||
timelineTypeContext?: TimelineTypeContextProps;
|
||||
utilityBar?: (totalCount: number) => React.ReactNode;
|
||||
utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode;
|
||||
}
|
||||
|
||||
interface StateReduxProps {
|
||||
|
@ -84,6 +84,10 @@ interface DispatchProps {
|
|||
|
||||
type Props = OwnProps & StateReduxProps & DispatchProps;
|
||||
|
||||
const defaultTimelineTypeContext = {
|
||||
loadingText: i18n.LOADING_EVENTS,
|
||||
};
|
||||
|
||||
const StatefulEventsViewerComponent: React.FC<Props> = ({
|
||||
createTimeline,
|
||||
columns,
|
||||
|
@ -99,16 +103,14 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
|
|||
itemsPerPage,
|
||||
itemsPerPageOptions,
|
||||
kqlMode,
|
||||
pageFilters = [],
|
||||
pageFilters,
|
||||
query,
|
||||
removeColumn,
|
||||
start,
|
||||
showCheckboxes,
|
||||
showRowRenderers,
|
||||
sort,
|
||||
timelineTypeContext = {
|
||||
loadingText: i18n.LOADING_EVENTS,
|
||||
},
|
||||
timelineTypeContext = defaultTimelineTypeContext,
|
||||
updateItemsPerPage,
|
||||
upsertColumn,
|
||||
utilityBar,
|
||||
|
@ -153,18 +155,20 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
|
|||
[columns, id, upsertColumn, removeColumn]
|
||||
);
|
||||
|
||||
const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]);
|
||||
|
||||
return (
|
||||
<InspectButtonContainer>
|
||||
<EventsViewer
|
||||
browserFields={browserFields ?? {}}
|
||||
browserFields={browserFields}
|
||||
columns={columns}
|
||||
id={id}
|
||||
dataProviders={dataProviders!}
|
||||
deletedEventIds={deletedEventIds}
|
||||
end={end}
|
||||
filters={filters}
|
||||
filters={globalFilters}
|
||||
headerFilterGroup={headerFilterGroup}
|
||||
indexPattern={indexPatterns ?? { fields: [], title: '' }}
|
||||
indexPattern={indexPatterns}
|
||||
isLive={isLive}
|
||||
itemsPerPage={itemsPerPage!}
|
||||
itemsPerPageOptions={itemsPerPageOptions!}
|
||||
|
@ -186,7 +190,7 @@ const makeMapStateToProps = () => {
|
|||
const getGlobalQuerySelector = inputsSelectors.globalQuerySelector();
|
||||
const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector();
|
||||
const getEvents = timelineSelectors.getEventsByIdSelector();
|
||||
const mapStateToProps = (state: State, { id, pageFilters = [], defaultModel }: OwnProps) => {
|
||||
const mapStateToProps = (state: State, { id, defaultModel }: OwnProps) => {
|
||||
const input: inputsModel.InputsRange = getInputsTimeline(state);
|
||||
const events: TimelineModel = getEvents(state, id) ?? defaultModel;
|
||||
const {
|
||||
|
@ -205,7 +209,7 @@ const makeMapStateToProps = () => {
|
|||
columns,
|
||||
dataProviders,
|
||||
deletedEventIds,
|
||||
filters: [...getGlobalFiltersQuerySelector(state), ...pageFilters],
|
||||
filters: getGlobalFiltersQuerySelector(state),
|
||||
id,
|
||||
isLive: input.policy.kind === 'interval',
|
||||
itemsPerPage,
|
||||
|
|
|
@ -166,7 +166,7 @@ const StatefulTimelineComponent = React.memo<Props>(
|
|||
updateItemsPerPage,
|
||||
upsertColumn,
|
||||
}) => {
|
||||
const [loading, signalIndexExists, signalIndexName] = useSignalIndex();
|
||||
const { loading, signalIndexExists, signalIndexName } = useSignalIndex();
|
||||
|
||||
const indexToAdd = useMemo<string[]>(() => {
|
||||
if (signalIndexExists && signalIndexName != null && ['signal', 'all'].includes(eventType)) {
|
||||
|
|
|
@ -389,6 +389,7 @@ export const getPrePackagedRulesStatus = async ({
|
|||
}: {
|
||||
signal: AbortSignal;
|
||||
}): Promise<{
|
||||
rules_custom_installed: number;
|
||||
rules_installed: number;
|
||||
rules_not_installed: number;
|
||||
rules_not_updated: number;
|
||||
|
|
|
@ -22,11 +22,11 @@ import { useApolloClient } from '../../../utils/apollo_context';
|
|||
import * as i18n from './translations';
|
||||
|
||||
interface FetchIndexPatternReturn {
|
||||
browserFields: BrowserFields | null;
|
||||
browserFields: BrowserFields;
|
||||
isLoading: boolean;
|
||||
indices: string[];
|
||||
indicesExists: boolean;
|
||||
indexPatterns: IIndexPattern | null;
|
||||
indexPatterns: IIndexPattern;
|
||||
}
|
||||
|
||||
type Return = [FetchIndexPatternReturn, Dispatch<SetStateAction<string[]>>];
|
||||
|
@ -35,8 +35,8 @@ export const useFetchIndexPatterns = (defaultIndices: string[] = []): Return =>
|
|||
const apolloClient = useApolloClient();
|
||||
const [indices, setIndices] = useState<string[]>(defaultIndices);
|
||||
const [indicesExists, setIndicesExists] = useState(false);
|
||||
const [indexPatterns, setIndexPatterns] = useState<IIndexPattern | null>(null);
|
||||
const [browserFields, setBrowserFields] = useState<BrowserFields | null>(null);
|
||||
const [indexPatterns, setIndexPatterns] = useState<IIndexPattern>({ fields: [], title: '' });
|
||||
const [browserFields, setBrowserFields] = useState<BrowserFields>({});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useStateToaster, displaySuccessToast } from '../../../components/toasters';
|
||||
import { errorToToaster } from '../../../components/ml/api/error_to_toaster';
|
||||
|
@ -18,6 +18,7 @@ interface Return {
|
|||
loading: boolean;
|
||||
loadingCreatePrePackagedRules: boolean;
|
||||
refetchPrePackagedRulesStatus: Func | null;
|
||||
rulesCustomInstalled: number | null;
|
||||
rulesInstalled: number | null;
|
||||
rulesNotInstalled: number | null;
|
||||
rulesNotUpdated: number | null;
|
||||
|
@ -47,13 +48,26 @@ export const usePrePackagedRules = ({
|
|||
isAuthenticated,
|
||||
isSignalIndexExists,
|
||||
}: UsePrePackagedRuleProps): Return => {
|
||||
const [rulesInstalled, setRulesInstalled] = useState<number | null>(null);
|
||||
const [rulesNotInstalled, setRulesNotInstalled] = useState<number | null>(null);
|
||||
const [rulesNotUpdated, setRulesNotUpdated] = useState<number | null>(null);
|
||||
const [rulesStatus, setRuleStatus] = useState<
|
||||
Pick<
|
||||
Return,
|
||||
| 'createPrePackagedRules'
|
||||
| 'refetchPrePackagedRulesStatus'
|
||||
| 'rulesCustomInstalled'
|
||||
| 'rulesInstalled'
|
||||
| 'rulesNotInstalled'
|
||||
| 'rulesNotUpdated'
|
||||
>
|
||||
>({
|
||||
createPrePackagedRules: null,
|
||||
refetchPrePackagedRulesStatus: null,
|
||||
rulesCustomInstalled: null,
|
||||
rulesInstalled: null,
|
||||
rulesNotInstalled: null,
|
||||
rulesNotUpdated: null,
|
||||
});
|
||||
const [loadingCreatePrePackagedRules, setLoadingCreatePrePackagedRules] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const createPrePackagedRules = useRef<null | CreatePreBuiltRules>(null);
|
||||
const refetchPrePackagedRules = useRef<Func | null>(null);
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -68,15 +82,25 @@ export const usePrePackagedRules = ({
|
|||
});
|
||||
|
||||
if (isSubscribed) {
|
||||
setRulesInstalled(prePackagedRuleStatusResponse.rules_installed);
|
||||
setRulesNotInstalled(prePackagedRuleStatusResponse.rules_not_installed);
|
||||
setRulesNotUpdated(prePackagedRuleStatusResponse.rules_not_updated);
|
||||
setRuleStatus({
|
||||
createPrePackagedRules: createElasticRules,
|
||||
refetchPrePackagedRulesStatus: fetchPrePackagedRules,
|
||||
rulesCustomInstalled: prePackagedRuleStatusResponse.rules_custom_installed,
|
||||
rulesInstalled: prePackagedRuleStatusResponse.rules_installed,
|
||||
rulesNotInstalled: prePackagedRuleStatusResponse.rules_not_installed,
|
||||
rulesNotUpdated: prePackagedRuleStatusResponse.rules_not_updated,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (isSubscribed) {
|
||||
setRulesInstalled(null);
|
||||
setRulesNotInstalled(null);
|
||||
setRulesNotUpdated(null);
|
||||
setRuleStatus({
|
||||
createPrePackagedRules: null,
|
||||
refetchPrePackagedRulesStatus: null,
|
||||
rulesCustomInstalled: null,
|
||||
rulesInstalled: null,
|
||||
rulesNotInstalled: null,
|
||||
rulesNotUpdated: null,
|
||||
});
|
||||
errorToToaster({ title: i18n.RULE_FETCH_FAILURE, error, dispatchToaster });
|
||||
}
|
||||
}
|
||||
|
@ -122,9 +146,14 @@ export const usePrePackagedRules = ({
|
|||
iterationTryOfFetchingPrePackagedCount > 100)
|
||||
) {
|
||||
setLoadingCreatePrePackagedRules(false);
|
||||
setRulesInstalled(prePackagedRuleStatusResponse.rules_installed);
|
||||
setRulesNotInstalled(prePackagedRuleStatusResponse.rules_not_installed);
|
||||
setRulesNotUpdated(prePackagedRuleStatusResponse.rules_not_updated);
|
||||
setRuleStatus({
|
||||
createPrePackagedRules: createElasticRules,
|
||||
refetchPrePackagedRulesStatus: fetchPrePackagedRules,
|
||||
rulesCustomInstalled: prePackagedRuleStatusResponse.rules_custom_installed,
|
||||
rulesInstalled: prePackagedRuleStatusResponse.rules_installed,
|
||||
rulesNotInstalled: prePackagedRuleStatusResponse.rules_not_installed,
|
||||
rulesNotUpdated: prePackagedRuleStatusResponse.rules_not_updated,
|
||||
});
|
||||
displaySuccessToast(i18n.RULE_PREPACKAGED_SUCCESS, dispatchToaster);
|
||||
stopTimeOut();
|
||||
resolve(true);
|
||||
|
@ -146,8 +175,7 @@ export const usePrePackagedRules = ({
|
|||
};
|
||||
|
||||
fetchPrePackagedRules();
|
||||
createPrePackagedRules.current = createElasticRules;
|
||||
refetchPrePackagedRules.current = fetchPrePackagedRules;
|
||||
|
||||
return () => {
|
||||
isSubscribed = false;
|
||||
abortCtrl.abort();
|
||||
|
@ -157,10 +185,6 @@ export const usePrePackagedRules = ({
|
|||
return {
|
||||
loading,
|
||||
loadingCreatePrePackagedRules,
|
||||
refetchPrePackagedRulesStatus: refetchPrePackagedRules.current,
|
||||
rulesInstalled,
|
||||
rulesNotInstalled,
|
||||
rulesNotUpdated,
|
||||
createPrePackagedRules: createPrePackagedRules.current,
|
||||
...rulesStatus,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -36,7 +36,7 @@ export const useRules = (pagination: PaginationOptions, filterOptions: FilterOpt
|
|||
let isSubscribed = true;
|
||||
const abortCtrl = new AbortController();
|
||||
|
||||
async function fetchData() {
|
||||
async function fetchData(forceReload: boolean = false) {
|
||||
try {
|
||||
setLoading(true);
|
||||
const fetchRulesResult = await fetchRules({
|
||||
|
@ -59,7 +59,7 @@ export const useRules = (pagination: PaginationOptions, filterOptions: FilterOpt
|
|||
}
|
||||
|
||||
fetchData();
|
||||
reFetchRules.current = fetchData;
|
||||
reFetchRules.current = fetchData.bind(null, true);
|
||||
return () => {
|
||||
isSubscribed = false;
|
||||
abortCtrl.abort();
|
||||
|
|
|
@ -24,10 +24,14 @@ interface Return {
|
|||
*/
|
||||
export const usePrivilegeUser = (): Return => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isAuthenticated, setAuthenticated] = useState<boolean | null>(null);
|
||||
const [hasIndexManage, setHasIndexManage] = useState<boolean | null>(null);
|
||||
const [hasIndexWrite, setHasIndexWrite] = useState<boolean | null>(null);
|
||||
const [hasManageApiKey, setHasManageApiKey] = useState<boolean | null>(null);
|
||||
const [privilegeUser, setPrivilegeUser] = useState<
|
||||
Pick<Return, 'isAuthenticated' | 'hasIndexManage' | 'hasManageApiKey' | 'hasIndexWrite'>
|
||||
>({
|
||||
isAuthenticated: null,
|
||||
hasIndexManage: null,
|
||||
hasManageApiKey: null,
|
||||
hasIndexWrite: null,
|
||||
});
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -42,29 +46,31 @@ export const usePrivilegeUser = (): Return => {
|
|||
});
|
||||
|
||||
if (isSubscribed && privilege != null) {
|
||||
setAuthenticated(privilege.is_authenticated);
|
||||
if (privilege.index != null && Object.keys(privilege.index).length > 0) {
|
||||
const indexName = Object.keys(privilege.index)[0];
|
||||
setHasIndexManage(privilege.index[indexName].manage);
|
||||
setHasIndexWrite(
|
||||
privilege.index[indexName].create ||
|
||||
setPrivilegeUser({
|
||||
isAuthenticated: privilege.is_authenticated,
|
||||
hasIndexManage: privilege.index[indexName].manage,
|
||||
hasIndexWrite:
|
||||
privilege.index[indexName].create ||
|
||||
privilege.index[indexName].create_doc ||
|
||||
privilege.index[indexName].index ||
|
||||
privilege.index[indexName].write
|
||||
);
|
||||
setHasManageApiKey(
|
||||
privilege.cluster.manage_security ||
|
||||
privilege.index[indexName].write,
|
||||
hasManageApiKey:
|
||||
privilege.cluster.manage_security ||
|
||||
privilege.cluster.manage_api_key ||
|
||||
privilege.cluster.manage_own_api_key
|
||||
);
|
||||
privilege.cluster.manage_own_api_key,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (isSubscribed) {
|
||||
setAuthenticated(false);
|
||||
setHasIndexManage(false);
|
||||
setHasIndexWrite(false);
|
||||
setHasManageApiKey(false);
|
||||
setPrivilegeUser({
|
||||
isAuthenticated: false,
|
||||
hasIndexManage: false,
|
||||
hasManageApiKey: false,
|
||||
hasIndexWrite: false,
|
||||
});
|
||||
errorToToaster({ title: i18n.PRIVILEGE_FETCH_FAILURE, error, dispatchToaster });
|
||||
}
|
||||
}
|
||||
|
@ -80,5 +86,5 @@ export const usePrivilegeUser = (): Return => {
|
|||
};
|
||||
}, []);
|
||||
|
||||
return { loading, isAuthenticated, hasIndexManage, hasManageApiKey, hasIndexWrite };
|
||||
return { loading, ...privilegeUser };
|
||||
};
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { errorToToaster } from '../../../components/ml/api/error_to_toaster';
|
||||
import { useStateToaster } from '../../../components/toasters';
|
||||
|
@ -14,7 +14,12 @@ import { PostSignalError, SignalIndexError } from './types';
|
|||
|
||||
type Func = () => void;
|
||||
|
||||
type Return = [boolean, boolean | null, string | null, Func | null];
|
||||
interface Return {
|
||||
loading: boolean;
|
||||
signalIndexExists: boolean | null;
|
||||
signalIndexName: string | null;
|
||||
createDeSignalIndex: Func | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for managing signal index
|
||||
|
@ -23,9 +28,13 @@ type Return = [boolean, boolean | null, string | null, Func | null];
|
|||
*/
|
||||
export const useSignalIndex = (): Return => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [signalIndexName, setSignalIndexName] = useState<string | null>(null);
|
||||
const [signalIndexExists, setSignalIndexExists] = useState<boolean | null>(null);
|
||||
const createDeSignalIndex = useRef<Func | null>(null);
|
||||
const [signalIndex, setSignalIndex] = useState<
|
||||
Pick<Return, 'signalIndexExists' | 'signalIndexName' | 'createDeSignalIndex'>
|
||||
>({
|
||||
signalIndexExists: null,
|
||||
signalIndexName: null,
|
||||
createDeSignalIndex: null,
|
||||
});
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -38,13 +47,19 @@ export const useSignalIndex = (): Return => {
|
|||
const signal = await getSignalIndex({ signal: abortCtrl.signal });
|
||||
|
||||
if (isSubscribed && signal != null) {
|
||||
setSignalIndexName(signal.name);
|
||||
setSignalIndexExists(true);
|
||||
setSignalIndex({
|
||||
signalIndexExists: true,
|
||||
signalIndexName: signal.name,
|
||||
createDeSignalIndex: createIndex,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (isSubscribed) {
|
||||
setSignalIndexName(null);
|
||||
setSignalIndexExists(false);
|
||||
setSignalIndex({
|
||||
signalIndexExists: false,
|
||||
signalIndexName: null,
|
||||
createDeSignalIndex: createIndex,
|
||||
});
|
||||
if (error instanceof SignalIndexError && error.statusCode !== 404) {
|
||||
errorToToaster({ title: i18n.SIGNAL_GET_NAME_FAILURE, error, dispatchToaster });
|
||||
}
|
||||
|
@ -70,8 +85,11 @@ export const useSignalIndex = (): Return => {
|
|||
if (error instanceof PostSignalError && error.statusCode === 409) {
|
||||
fetchData();
|
||||
} else {
|
||||
setSignalIndexName(null);
|
||||
setSignalIndexExists(false);
|
||||
setSignalIndex({
|
||||
signalIndexExists: false,
|
||||
signalIndexName: null,
|
||||
createDeSignalIndex: createIndex,
|
||||
});
|
||||
errorToToaster({ title: i18n.SIGNAL_POST_FAILURE, error, dispatchToaster });
|
||||
}
|
||||
}
|
||||
|
@ -82,12 +100,11 @@ export const useSignalIndex = (): Return => {
|
|||
};
|
||||
|
||||
fetchData();
|
||||
createDeSignalIndex.current = createIndex;
|
||||
return () => {
|
||||
isSubscribed = false;
|
||||
abortCtrl.abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return [loading, signalIndexExists, signalIndexName, createDeSignalIndex.current];
|
||||
return { loading, ...signalIndex };
|
||||
};
|
||||
|
|
|
@ -89,7 +89,8 @@ export const WithSource = React.memo<WithSourceProps>(({ children, indexToAdd, s
|
|||
return [...configIndex, ...indexToAdd];
|
||||
}
|
||||
return configIndex;
|
||||
}, [configIndex, DEFAULT_INDEX_KEY, indexToAdd]);
|
||||
}, [configIndex, indexToAdd]);
|
||||
|
||||
return (
|
||||
<Query<SourceQuery.Query, SourceQuery.Variables>
|
||||
query={sourceQuery}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { getOr, isEmpty } from 'lodash/fp';
|
||||
import { getOr } from 'lodash/fp';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import React from 'react';
|
||||
import { Query } from 'react-apollo';
|
||||
|
@ -84,12 +84,13 @@ class TimelineQueryComponent extends QueryTemplate<
|
|||
sortField,
|
||||
} = this.props;
|
||||
const defaultKibanaIndex = kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY);
|
||||
const defaultIndex = isEmpty(indexPattern)
|
||||
? [
|
||||
...(['all', 'raw'].includes(eventType) ? defaultKibanaIndex : []),
|
||||
...(['all', 'signal'].includes(eventType) ? indexToAdd : []),
|
||||
]
|
||||
: indexPattern?.title.split(',') ?? [];
|
||||
const defaultIndex =
|
||||
indexPattern == null || (indexPattern != null && indexPattern.title === '')
|
||||
? [
|
||||
...(['all', 'raw'].includes(eventType) ? defaultKibanaIndex : []),
|
||||
...(['all', 'signal'].includes(eventType) ? indexToAdd : []),
|
||||
]
|
||||
: indexPattern?.title.split(',') ?? [];
|
||||
const variables: GetTimelineQuery.Variables = {
|
||||
fieldRequested: fields,
|
||||
filterQuery: createFilter(filterQuery),
|
||||
|
|
|
@ -20,8 +20,7 @@ import { DispatchUpdateTimeline } from '../../../../components/open_timeline/typ
|
|||
import { combineQueries } from '../../../../components/timeline/helpers';
|
||||
import { TimelineNonEcsData } from '../../../../graphql/types';
|
||||
import { useKibana } from '../../../../lib/kibana';
|
||||
import { inputsSelectors, State } from '../../../../store';
|
||||
import { InputsRange } from '../../../../store/inputs/model';
|
||||
import { inputsSelectors, State, inputsModel } from '../../../../store';
|
||||
import { timelineActions, timelineSelectors } from '../../../../store/timeline';
|
||||
import { timelineDefaults, TimelineModel } from '../../../../store/timeline/model';
|
||||
import { useApolloClient } from '../../../../utils/apollo_context';
|
||||
|
@ -46,7 +45,7 @@ import {
|
|||
CreateTimelineProps,
|
||||
SetEventsDeletedProps,
|
||||
SetEventsLoadingProps,
|
||||
UpdateSignalsStatus,
|
||||
UpdateSignalsStatusCallback,
|
||||
UpdateSignalsStatusProps,
|
||||
} from './types';
|
||||
import { dispatchUpdateTimeline } from '../../../../components/open_timeline/helpers';
|
||||
|
@ -97,7 +96,7 @@ const SignalsTableComponent: React.FC<SignalsTableComponentProps> = ({
|
|||
clearEventsDeleted,
|
||||
clearEventsLoading,
|
||||
clearSelected,
|
||||
defaultFilters = [],
|
||||
defaultFilters,
|
||||
from,
|
||||
globalFilters,
|
||||
globalQuery,
|
||||
|
@ -118,7 +117,9 @@ const SignalsTableComponent: React.FC<SignalsTableComponentProps> = ({
|
|||
|
||||
const [showClearSelectionAction, setShowClearSelectionAction] = useState(false);
|
||||
const [filterGroup, setFilterGroup] = useState<SignalFilterOption>(FILTER_OPEN);
|
||||
const [{ browserFields, indexPatterns }] = useFetchIndexPatterns([signalsIndex]);
|
||||
const [{ browserFields, indexPatterns }] = useFetchIndexPatterns(
|
||||
signalsIndex !== '' ? [signalsIndex] : []
|
||||
);
|
||||
const kibana = useKibana();
|
||||
|
||||
const getGlobalQuery = useCallback(() => {
|
||||
|
@ -207,8 +208,8 @@ const SignalsTableComponent: React.FC<SignalsTableComponentProps> = ({
|
|||
setShowClearSelectionAction(true);
|
||||
}, [setSelectAll, setShowClearSelectionAction]);
|
||||
|
||||
const updateSignalsStatusCallback: UpdateSignalsStatus = useCallback(
|
||||
async ({ signalIds, status }: UpdateSignalsStatusProps) => {
|
||||
const updateSignalsStatusCallback: UpdateSignalsStatusCallback = useCallback(
|
||||
async (refetchQuery: inputsModel.Refetch, { signalIds, status }: UpdateSignalsStatusProps) => {
|
||||
await updateSignalStatusAction({
|
||||
query: showClearSelectionAction ? getGlobalQuery()?.filterQuery : undefined,
|
||||
signalIds: Object.keys(selectedEventIds),
|
||||
|
@ -216,6 +217,7 @@ const SignalsTableComponent: React.FC<SignalsTableComponentProps> = ({
|
|||
setEventsDeleted: setEventsDeletedCallback,
|
||||
setEventsLoading: setEventsLoadingCallback,
|
||||
});
|
||||
refetchQuery();
|
||||
},
|
||||
[
|
||||
getGlobalQuery,
|
||||
|
@ -228,7 +230,7 @@ const SignalsTableComponent: React.FC<SignalsTableComponentProps> = ({
|
|||
|
||||
// Callback for creating the SignalUtilityBar which receives totalCount from EventsViewer component
|
||||
const utilityBarCallback = useCallback(
|
||||
(totalCount: number) => {
|
||||
(refetchQuery: inputsModel.Refetch, totalCount: number) => {
|
||||
return (
|
||||
<SignalsUtilityBar
|
||||
canUserCRUD={canUserCRUD}
|
||||
|
@ -240,7 +242,7 @@ const SignalsTableComponent: React.FC<SignalsTableComponentProps> = ({
|
|||
selectedEventIds={selectedEventIds}
|
||||
showClearSelection={showClearSelectionAction}
|
||||
totalCount={totalCount}
|
||||
updateSignalsStatus={updateSignalsStatusCallback}
|
||||
updateSignalsStatus={updateSignalsStatusCallback.bind(null, refetchQuery)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
@ -283,13 +285,16 @@ const SignalsTableComponent: React.FC<SignalsTableComponentProps> = ({
|
|||
);
|
||||
|
||||
const defaultIndices = useMemo(() => [signalsIndex], [signalsIndex]);
|
||||
const defaultFiltersMemo = useMemo(
|
||||
() => [
|
||||
...defaultFilters,
|
||||
...(filterGroup === FILTER_OPEN ? signalsOpenFilters : signalsClosedFilters),
|
||||
],
|
||||
[defaultFilters, filterGroup]
|
||||
);
|
||||
const defaultFiltersMemo = useMemo(() => {
|
||||
if (isEmpty(defaultFilters)) {
|
||||
return filterGroup === FILTER_OPEN ? signalsOpenFilters : signalsClosedFilters;
|
||||
} else if (defaultFilters != null && !isEmpty(defaultFilters)) {
|
||||
return [
|
||||
...defaultFilters,
|
||||
...(filterGroup === FILTER_OPEN ? signalsOpenFilters : signalsClosedFilters),
|
||||
];
|
||||
}
|
||||
}, [defaultFilters, filterGroup]);
|
||||
|
||||
const timelineTypeContext = useMemo(
|
||||
() => ({
|
||||
|
@ -304,6 +309,11 @@ const SignalsTableComponent: React.FC<SignalsTableComponentProps> = ({
|
|||
[additionalActions, canUserCRUD, selectAll]
|
||||
);
|
||||
|
||||
const headerFilterGroup = useMemo(
|
||||
() => <SignalsTableFilterGroup onFilterGroupChanged={onFilterGroupChangedCallback} />,
|
||||
[onFilterGroupChangedCallback]
|
||||
);
|
||||
|
||||
if (loading || isEmpty(signalsIndex)) {
|
||||
return (
|
||||
<EuiPanel>
|
||||
|
@ -319,9 +329,7 @@ const SignalsTableComponent: React.FC<SignalsTableComponentProps> = ({
|
|||
pageFilters={defaultFiltersMemo}
|
||||
defaultModel={signalsDefaultModel}
|
||||
end={to}
|
||||
headerFilterGroup={
|
||||
<SignalsTableFilterGroup onFilterGroupChanged={onFilterGroupChangedCallback} />
|
||||
}
|
||||
headerFilterGroup={headerFilterGroup}
|
||||
id={SIGNALS_PAGE_TIMELINE_ID}
|
||||
start={from}
|
||||
timelineTypeContext={timelineTypeContext}
|
||||
|
@ -338,9 +346,8 @@ const makeMapStateToProps = () => {
|
|||
getTimeline(state, SIGNALS_PAGE_TIMELINE_ID) ?? timelineDefaults;
|
||||
const { deletedEventIds, isSelectAllChecked, loadingEventIds, selectedEventIds } = timeline;
|
||||
|
||||
const globalInputs: InputsRange = getGlobalInputs(state);
|
||||
const globalInputs: inputsModel.InputsRange = getGlobalInputs(state);
|
||||
const { query, filters } = globalInputs;
|
||||
|
||||
return {
|
||||
globalQuery: query,
|
||||
globalFilters: filters,
|
||||
|
|
|
@ -8,6 +8,7 @@ import ApolloClient from 'apollo-client';
|
|||
|
||||
import { Ecs } from '../../../../graphql/types';
|
||||
import { TimelineModel } from '../../../../store/timeline/model';
|
||||
import { inputsModel } from '../../../../store';
|
||||
|
||||
export interface SetEventsLoadingProps {
|
||||
eventIds: string[];
|
||||
|
@ -24,6 +25,10 @@ export interface UpdateSignalsStatusProps {
|
|||
status: 'open' | 'closed';
|
||||
}
|
||||
|
||||
export type UpdateSignalsStatusCallback = (
|
||||
refetchQuery: inputsModel.Refetch,
|
||||
{ signalIds, status }: UpdateSignalsStatusProps
|
||||
) => void;
|
||||
export type UpdateSignalsStatus = ({ signalIds, status }: UpdateSignalsStatusProps) => void;
|
||||
|
||||
export interface UpdateSignalStatusActionProps {
|
||||
|
|
|
@ -154,12 +154,12 @@ export const useUserInfo = (): State => {
|
|||
hasIndexWrite: hasApiIndexWrite,
|
||||
hasManageApiKey: hasApiManageApiKey,
|
||||
} = usePrivilegeUser();
|
||||
const [
|
||||
indexNameLoading,
|
||||
isApiSignalIndexExists,
|
||||
apiSignalIndexName,
|
||||
createSignalIndex,
|
||||
] = useSignalIndex();
|
||||
const {
|
||||
loading: indexNameLoading,
|
||||
signalIndexExists: isApiSignalIndexExists,
|
||||
signalIndexName: apiSignalIndexName,
|
||||
createDeSignalIndex: createSignalIndex,
|
||||
} = useSignalIndex();
|
||||
|
||||
const uiCapabilities = useKibana().services.application.capabilities;
|
||||
const capabilitiesCanUserCRUD: boolean =
|
||||
|
@ -172,46 +172,50 @@ export const useUserInfo = (): State => {
|
|||
}, [loading, privilegeLoading, indexNameLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasIndexManage !== hasApiIndexManage && hasApiIndexManage != null) {
|
||||
if (!loading && hasIndexManage !== hasApiIndexManage && hasApiIndexManage != null) {
|
||||
dispatch({ type: 'updateHasIndexManage', hasIndexManage: hasApiIndexManage });
|
||||
}
|
||||
}, [hasIndexManage, hasApiIndexManage]);
|
||||
}, [loading, hasIndexManage, hasApiIndexManage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasIndexWrite !== hasApiIndexWrite && hasApiIndexWrite != null) {
|
||||
if (!loading && hasIndexWrite !== hasApiIndexWrite && hasApiIndexWrite != null) {
|
||||
dispatch({ type: 'updateHasIndexWrite', hasIndexWrite: hasApiIndexWrite });
|
||||
}
|
||||
}, [hasIndexWrite, hasApiIndexWrite]);
|
||||
}, [loading, hasIndexWrite, hasApiIndexWrite]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasManageApiKey !== hasApiManageApiKey && hasApiManageApiKey != null) {
|
||||
if (!loading && hasManageApiKey !== hasApiManageApiKey && hasApiManageApiKey != null) {
|
||||
dispatch({ type: 'updateHasManageApiKey', hasManageApiKey: hasApiManageApiKey });
|
||||
}
|
||||
}, [hasManageApiKey, hasApiManageApiKey]);
|
||||
}, [loading, hasManageApiKey, hasApiManageApiKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSignalIndexExists !== isApiSignalIndexExists && isApiSignalIndexExists != null) {
|
||||
if (
|
||||
!loading &&
|
||||
isSignalIndexExists !== isApiSignalIndexExists &&
|
||||
isApiSignalIndexExists != null
|
||||
) {
|
||||
dispatch({ type: 'updateIsSignalIndexExists', isSignalIndexExists: isApiSignalIndexExists });
|
||||
}
|
||||
}, [isSignalIndexExists, isApiSignalIndexExists]);
|
||||
}, [loading, isSignalIndexExists, isApiSignalIndexExists]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated !== isApiAuthenticated && isApiAuthenticated != null) {
|
||||
if (!loading && isAuthenticated !== isApiAuthenticated && isApiAuthenticated != null) {
|
||||
dispatch({ type: 'updateIsAuthenticated', isAuthenticated: isApiAuthenticated });
|
||||
}
|
||||
}, [isAuthenticated, isApiAuthenticated]);
|
||||
}, [loading, isAuthenticated, isApiAuthenticated]);
|
||||
|
||||
useEffect(() => {
|
||||
if (canUserCRUD !== capabilitiesCanUserCRUD && capabilitiesCanUserCRUD != null) {
|
||||
if (!loading && canUserCRUD !== capabilitiesCanUserCRUD && capabilitiesCanUserCRUD != null) {
|
||||
dispatch({ type: 'updateCanUserCRUD', canUserCRUD: capabilitiesCanUserCRUD });
|
||||
}
|
||||
}, [canUserCRUD, capabilitiesCanUserCRUD]);
|
||||
}, [loading, canUserCRUD, capabilitiesCanUserCRUD]);
|
||||
|
||||
useEffect(() => {
|
||||
if (signalIndexName !== apiSignalIndexName && apiSignalIndexName != null) {
|
||||
if (!loading && signalIndexName !== apiSignalIndexName && apiSignalIndexName != null) {
|
||||
dispatch({ type: 'updateSignalIndexName', signalIndexName: apiSignalIndexName });
|
||||
}
|
||||
}, [signalIndexName, apiSignalIndexName]);
|
||||
}, [loading, signalIndexName, apiSignalIndexName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
|
|
|
@ -111,6 +111,10 @@ const DetectionEnginePageComponent: React.FC<DetectionEnginePageComponentProps>
|
|||
[detectionsTabs, tabName]
|
||||
);
|
||||
|
||||
const indexToAdd = useMemo(() => (signalIndexName == null ? [] : [signalIndexName]), [
|
||||
signalIndexName,
|
||||
]);
|
||||
|
||||
if (isUserAuthenticated != null && !isUserAuthenticated && !loading) {
|
||||
return (
|
||||
<WrapperPage>
|
||||
|
@ -131,7 +135,7 @@ const DetectionEnginePageComponent: React.FC<DetectionEnginePageComponentProps>
|
|||
return (
|
||||
<>
|
||||
{hasIndexWrite != null && !hasIndexWrite && <NoWriteSignalsCallOut />}
|
||||
<WithSource sourceId="default">
|
||||
<WithSource sourceId="default" indexToAdd={indexToAdd}>
|
||||
{({ indicesExist, indexPattern }) => {
|
||||
return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? (
|
||||
<StickyContainer>
|
||||
|
|
|
@ -38,12 +38,7 @@ export const duplicateRulesAction = async (
|
|||
const ruleIds = rules.map(r => r.id);
|
||||
dispatch({ type: 'updateLoading', ids: ruleIds, isLoading: true });
|
||||
const duplicatedRules = await duplicateRules({ rules });
|
||||
dispatch({ type: 'updateLoading', ids: ruleIds, isLoading: false });
|
||||
dispatch({
|
||||
type: 'updateRules',
|
||||
rules: duplicatedRules,
|
||||
appendRuleId: rules[rules.length - 1].id,
|
||||
});
|
||||
dispatch({ type: 'refresh' });
|
||||
displaySuccessToast(
|
||||
i18n.SUCCESSFULLY_DUPLICATED_RULES(duplicatedRules.length),
|
||||
dispatchToaster
|
||||
|
|
|
@ -50,3 +50,16 @@ export const bucketRulesResponse = (response: Array<Rule | RuleError>) =>
|
|||
},
|
||||
{ rules: [], errors: [] }
|
||||
);
|
||||
|
||||
export const showRulesTable = ({
|
||||
isInitialLoad,
|
||||
rulesCustomInstalled,
|
||||
rulesInstalled,
|
||||
}: {
|
||||
isInitialLoad: boolean;
|
||||
rulesCustomInstalled: number | null;
|
||||
rulesInstalled: number | null;
|
||||
}) =>
|
||||
!isInitialLoad &&
|
||||
((rulesCustomInstalled != null && rulesCustomInstalled > 0) ||
|
||||
(rulesInstalled != null && rulesInstalled > 0));
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiButton,
|
||||
EuiContextMenuPanel,
|
||||
EuiEmptyPrompt,
|
||||
EuiLoadingContent,
|
||||
|
@ -40,9 +39,9 @@ import * as i18n from '../translations';
|
|||
import { EuiBasicTableOnChange, TableData } from '../types';
|
||||
import { getBatchItems } from './batch_actions';
|
||||
import { getColumns } from './columns';
|
||||
import { showRulesTable } from './helpers';
|
||||
import { allRulesReducer, State } from './reducer';
|
||||
import { RulesTableFilters } from './rules_table_filters/rules_table_filters';
|
||||
import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine';
|
||||
|
||||
const initialState: State = {
|
||||
isLoading: true,
|
||||
|
@ -69,6 +68,7 @@ interface AllRulesProps {
|
|||
loading: boolean;
|
||||
loadingCreatePrePackagedRules: boolean;
|
||||
refetchPrePackagedRulesStatus: () => void;
|
||||
rulesCustomInstalled: number | null;
|
||||
rulesInstalled: number | null;
|
||||
rulesNotInstalled: number | null;
|
||||
rulesNotUpdated: number | null;
|
||||
|
@ -91,6 +91,7 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
loading,
|
||||
loadingCreatePrePackagedRules,
|
||||
refetchPrePackagedRulesStatus,
|
||||
rulesCustomInstalled,
|
||||
rulesInstalled,
|
||||
rulesNotInstalled,
|
||||
rulesNotUpdated,
|
||||
|
@ -109,6 +110,7 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
dispatch,
|
||||
] = useReducer(allRulesReducer, initialState);
|
||||
const history = useHistory();
|
||||
const [oldRefreshToggle, setOldRefreshToggle] = useState(refreshToggle);
|
||||
const [isInitialLoad, setIsInitialLoad] = useState(true);
|
||||
const [isGlobalLoading, setIsGlobalLoad] = useState(false);
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
|
@ -131,20 +133,16 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
|
||||
const tableOnChangeCallback = useCallback(
|
||||
({ page, sort }: EuiBasicTableOnChange) => {
|
||||
dispatch({
|
||||
type: 'updatePagination',
|
||||
pagination: { ...pagination, page: page.index + 1, perPage: page.size },
|
||||
});
|
||||
dispatch({
|
||||
type: 'updateFilterOptions',
|
||||
filterOptions: {
|
||||
...filterOptions,
|
||||
sortField: 'enabled', // Only enabled is supported for sorting currently
|
||||
sortOrder: sort?.direction ?? 'desc',
|
||||
},
|
||||
pagination: { page: page.index + 1, perPage: page.size },
|
||||
});
|
||||
},
|
||||
[dispatch, filterOptions, pagination]
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const columns = useMemo(() => {
|
||||
|
@ -176,11 +174,18 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
}, [importCompleteToggle]);
|
||||
|
||||
useEffect(() => {
|
||||
if (reFetchRulesData != null) {
|
||||
if (!isInitialLoad && reFetchRulesData != null && oldRefreshToggle !== refreshToggle) {
|
||||
setOldRefreshToggle(refreshToggle);
|
||||
reFetchRulesData();
|
||||
refetchPrePackagedRulesStatus();
|
||||
}
|
||||
refetchPrePackagedRulesStatus();
|
||||
}, [refreshToggle, reFetchRulesData, refetchPrePackagedRulesStatus]);
|
||||
}, [
|
||||
isInitialLoad,
|
||||
refreshToggle,
|
||||
oldRefreshToggle,
|
||||
reFetchRulesData,
|
||||
refetchPrePackagedRulesStatus,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (reFetchRulesData != null) {
|
||||
|
@ -220,14 +225,16 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
dispatch({
|
||||
type: 'updateFilterOptions',
|
||||
filterOptions: {
|
||||
...filterOptions,
|
||||
...newFilterOptions,
|
||||
},
|
||||
pagination: { page: 1 },
|
||||
});
|
||||
dispatch({
|
||||
type: 'updatePagination',
|
||||
pagination: { ...pagination, page: 1 },
|
||||
});
|
||||
}, []);
|
||||
|
||||
const emptyPrompt = useMemo(() => {
|
||||
return (
|
||||
<EuiEmptyPrompt title={<h3>{i18n.NO_RULES}</h3>} titleSize="xs" body={i18n.NO_RULES_BODY} />
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
@ -251,25 +258,32 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
|
||||
<Panel loading={isGlobalLoading}>
|
||||
<>
|
||||
{rulesInstalled != null && rulesInstalled > 0 && (
|
||||
{((rulesCustomInstalled && rulesCustomInstalled > 0) ||
|
||||
(rulesInstalled != null && rulesInstalled > 0)) && (
|
||||
<HeaderSection split title={i18n.ALL_RULES}>
|
||||
<RulesTableFilters onFilterChanged={onFilterChangedCallback} />
|
||||
<RulesTableFilters
|
||||
onFilterChanged={onFilterChangedCallback}
|
||||
rulesCustomInstalled={rulesCustomInstalled}
|
||||
rulesInstalled={rulesInstalled}
|
||||
/>
|
||||
</HeaderSection>
|
||||
)}
|
||||
{isInitialLoad && isEmpty(tableData) && (
|
||||
{isInitialLoad && (
|
||||
<EuiLoadingContent data-test-subj="initialLoadingPanelAllRulesTable" lines={10} />
|
||||
)}
|
||||
{isGlobalLoading && !isEmpty(tableData) && (
|
||||
{isGlobalLoading && !isEmpty(tableData) && !isInitialLoad && (
|
||||
<Loader data-test-subj="loadingPanelAllRulesTable" overlay size="xl" />
|
||||
)}
|
||||
{isEmpty(tableData) && prePackagedRuleStatus === 'ruleNotInstalled' && (
|
||||
<PrePackagedRulesPrompt
|
||||
createPrePackagedRules={handleCreatePrePackagedRules}
|
||||
loading={loadingCreatePrePackagedRules}
|
||||
userHasNoPermissions={hasNoPermissions}
|
||||
/>
|
||||
)}
|
||||
{!isEmpty(tableData) && (
|
||||
{rulesCustomInstalled != null &&
|
||||
rulesCustomInstalled === 0 &&
|
||||
prePackagedRuleStatus === 'ruleNotInstalled' && (
|
||||
<PrePackagedRulesPrompt
|
||||
createPrePackagedRules={handleCreatePrePackagedRules}
|
||||
loading={loadingCreatePrePackagedRules}
|
||||
userHasNoPermissions={hasNoPermissions}
|
||||
/>
|
||||
)}
|
||||
{showRulesTable({ isInitialLoad, rulesCustomInstalled, rulesInstalled }) && (
|
||||
<>
|
||||
<UtilityBar border>
|
||||
<UtilityBarSection>
|
||||
|
@ -304,24 +318,7 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
isSelectable={!hasNoPermissions ?? false}
|
||||
itemId="id"
|
||||
items={tableData}
|
||||
noItemsMessage={
|
||||
<EuiEmptyPrompt
|
||||
title={<h3>{i18n.NO_RULES}</h3>}
|
||||
titleSize="xs"
|
||||
body={i18n.NO_RULES_BODY}
|
||||
actions={
|
||||
<EuiButton
|
||||
fill
|
||||
size="s"
|
||||
href={`#${DETECTION_ENGINE_PAGE_NAME}/rules/create`}
|
||||
iconType="plusInCircle"
|
||||
isDisabled={hasNoPermissions}
|
||||
>
|
||||
{i18n.ADD_NEW_RULE}
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
}
|
||||
noItemsMessage={emptyPrompt}
|
||||
onChange={tableOnChangeCallback}
|
||||
pagination={{
|
||||
pageIndex: pagination.page - 1,
|
||||
|
|
|
@ -31,9 +31,13 @@ export type Action =
|
|||
| { type: 'setExportPayload'; exportPayload?: Rule[] }
|
||||
| { type: 'setSelected'; selectedItems: TableData[] }
|
||||
| { type: 'updateLoading'; ids: string[]; isLoading: boolean }
|
||||
| { type: 'updateRules'; rules: Rule[]; appendRuleId?: string; pagination?: PaginationOptions }
|
||||
| { type: 'updatePagination'; pagination: PaginationOptions }
|
||||
| { type: 'updateFilterOptions'; filterOptions: FilterOptions }
|
||||
| { type: 'updateRules'; rules: Rule[]; pagination?: PaginationOptions }
|
||||
| { type: 'updatePagination'; pagination: Partial<PaginationOptions> }
|
||||
| {
|
||||
type: 'updateFilterOptions';
|
||||
filterOptions: Partial<FilterOptions>;
|
||||
pagination: Partial<PaginationOptions>;
|
||||
}
|
||||
| { type: 'failure' };
|
||||
|
||||
export const allRulesReducer = (state: State, action: Action): State => {
|
||||
|
@ -56,18 +60,10 @@ export const allRulesReducer = (state: State, action: Action): State => {
|
|||
}
|
||||
|
||||
const ruleIds = state.rules.map(r => r.rule_id);
|
||||
const appendIdx =
|
||||
action.appendRuleId != null ? state.rules.findIndex(r => r.id === action.appendRuleId) : -1;
|
||||
const updatedRules = action.rules.reverse().reduce((rules, updatedRule) => {
|
||||
let newRules = rules;
|
||||
if (ruleIds.includes(updatedRule.rule_id)) {
|
||||
newRules = newRules.map(r => (updatedRule.rule_id === r.rule_id ? updatedRule : r));
|
||||
} else if (appendIdx !== -1) {
|
||||
newRules = [
|
||||
...newRules.slice(0, appendIdx + 1),
|
||||
updatedRule,
|
||||
...newRules.slice(appendIdx + 1, newRules.length),
|
||||
];
|
||||
} else {
|
||||
newRules = [...newRules, updatedRule];
|
||||
}
|
||||
|
@ -90,25 +86,28 @@ export const allRulesReducer = (state: State, action: Action): State => {
|
|||
rules: updatedRules,
|
||||
tableData: formatRules(updatedRules),
|
||||
selectedItems: updatedSelectedItems,
|
||||
pagination: {
|
||||
...state.pagination,
|
||||
total:
|
||||
action.appendRuleId != null
|
||||
? state.pagination.total + action.rules.length
|
||||
: state.pagination.total,
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'updatePagination': {
|
||||
return {
|
||||
...state,
|
||||
pagination: action.pagination,
|
||||
pagination: {
|
||||
...state.pagination,
|
||||
...action.pagination,
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'updateFilterOptions': {
|
||||
return {
|
||||
...state,
|
||||
filterOptions: action.filterOptions,
|
||||
filterOptions: {
|
||||
...state.filterOptions,
|
||||
...action.filterOptions,
|
||||
},
|
||||
pagination: {
|
||||
...state.pagination,
|
||||
...action.pagination,
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'deleteRules': {
|
||||
|
|
|
@ -21,6 +21,8 @@ import { TagsFilterPopover } from './tags_filter_popover';
|
|||
|
||||
interface RulesTableFiltersProps {
|
||||
onFilterChanged: (filterOptions: Partial<FilterOptions>) => void;
|
||||
rulesCustomInstalled: number | null;
|
||||
rulesInstalled: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -29,7 +31,11 @@ interface RulesTableFiltersProps {
|
|||
*
|
||||
* @param onFilterChanged change listener to be notified on filter changes
|
||||
*/
|
||||
const RulesTableFiltersComponent = ({ onFilterChanged }: RulesTableFiltersProps) => {
|
||||
const RulesTableFiltersComponent = ({
|
||||
onFilterChanged,
|
||||
rulesCustomInstalled,
|
||||
rulesInstalled,
|
||||
}: RulesTableFiltersProps) => {
|
||||
const [filter, setFilter] = useState<string>('');
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
const [showCustomRules, setShowCustomRules] = useState<boolean>(false);
|
||||
|
@ -84,13 +90,17 @@ const RulesTableFiltersComponent = ({ onFilterChanged }: RulesTableFiltersProps)
|
|||
withNext
|
||||
>
|
||||
{i18n.ELASTIC_RULES}
|
||||
{rulesInstalled != null ? ` (${rulesInstalled})` : ''}
|
||||
</EuiFilterButton>
|
||||
<EuiFilterButton
|
||||
hasActiveFilters={showCustomRules}
|
||||
onClick={handleCustomRulesClick}
|
||||
data-test-subj="show-custom-rules-filter-button"
|
||||
>
|
||||
{i18n.CUSTOM_RULES}
|
||||
<>
|
||||
{i18n.CUSTOM_RULES}
|
||||
{rulesCustomInstalled != null ? ` (${rulesCustomInstalled})` : ''}
|
||||
</>
|
||||
</EuiFilterButton>
|
||||
</EuiFilterGroup>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -127,14 +127,6 @@ const RuleDetailsPageComponent: FC<RuleDetailsComponentProps> = ({
|
|||
const userHasNoPermissions =
|
||||
canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false;
|
||||
|
||||
if (
|
||||
isSignalIndexExists != null &&
|
||||
isAuthenticated != null &&
|
||||
(!isSignalIndexExists || !isAuthenticated)
|
||||
) {
|
||||
return <Redirect to={`/${DETECTION_ENGINE_PAGE_NAME}`} />;
|
||||
}
|
||||
|
||||
const title = isLoading === true || rule === null ? <EuiLoadingSpinner size="m" /> : rule.name;
|
||||
const subTitle = useMemo(
|
||||
() =>
|
||||
|
@ -217,6 +209,10 @@ const RuleDetailsPageComponent: FC<RuleDetailsComponentProps> = ({
|
|||
[rule, ruleDetailTab]
|
||||
);
|
||||
|
||||
const indexToAdd = useMemo(() => (signalIndexName == null ? [] : [signalIndexName]), [
|
||||
signalIndexName,
|
||||
]);
|
||||
|
||||
const updateDateRangeCallback = useCallback(
|
||||
(min: number, max: number) => {
|
||||
setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max });
|
||||
|
@ -233,11 +229,19 @@ const RuleDetailsPageComponent: FC<RuleDetailsComponentProps> = ({
|
|||
[ruleEnabled, setRuleEnabled]
|
||||
);
|
||||
|
||||
if (
|
||||
isSignalIndexExists != null &&
|
||||
isAuthenticated != null &&
|
||||
(!isSignalIndexExists || !isAuthenticated)
|
||||
) {
|
||||
return <Redirect to={`/${DETECTION_ENGINE_PAGE_NAME}`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasIndexWrite != null && !hasIndexWrite && <NoWriteSignalsCallOut />}
|
||||
{userHasNoPermissions && <ReadOnlyCallOut />}
|
||||
<WithSource sourceId="default">
|
||||
<WithSource sourceId="default" indexToAdd={indexToAdd}>
|
||||
{({ indicesExist, indexPattern }) => {
|
||||
return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? (
|
||||
<GlobalTime>
|
||||
|
|
|
@ -5,14 +5,14 @@
|
|||
*/
|
||||
|
||||
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import { usePrePackagedRules } from '../../../containers/detection_engine/rules';
|
||||
import { DETECTION_ENGINE_PAGE_NAME } from '../../../components/link_to/redirect_to_detection_engine';
|
||||
import { FormattedRelativePreferenceDate } from '../../../components/formatted_date';
|
||||
import { getEmptyTagValue } from '../../../components/empty_value';
|
||||
import {
|
||||
getDetectionEngineUrl,
|
||||
getCreateRuleUrl,
|
||||
} from '../../../components/link_to/redirect_to_detection_engine';
|
||||
import { DetectionEngineHeaderPage } from '../components/detection_engine_header_page';
|
||||
import { WrapperPage } from '../../../components/wrapper_page';
|
||||
import { SpyRoute } from '../../../utils/route/spy_routes';
|
||||
|
@ -44,6 +44,7 @@ const RulesPageComponent: React.FC = () => {
|
|||
loading: prePackagedRuleLoading,
|
||||
loadingCreatePrePackagedRules,
|
||||
refetchPrePackagedRulesStatus,
|
||||
rulesCustomInstalled,
|
||||
rulesInstalled,
|
||||
rulesNotInstalled,
|
||||
rulesNotUpdated,
|
||||
|
@ -62,7 +63,6 @@ const RulesPageComponent: React.FC = () => {
|
|||
|
||||
const userHasNoPermissions =
|
||||
canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false;
|
||||
const lastCompletedRun = undefined;
|
||||
|
||||
const handleCreatePrePackagedRules = useCallback(async () => {
|
||||
if (createPrePackagedRules != null) {
|
||||
|
@ -88,7 +88,7 @@ const RulesPageComponent: React.FC = () => {
|
|||
isAuthenticated != null &&
|
||||
(!isSignalIndexExists || !isAuthenticated)
|
||||
) {
|
||||
return <Redirect to={`/${DETECTION_ENGINE_PAGE_NAME}`} />;
|
||||
return <Redirect to={getDetectionEngineUrl()} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -102,22 +102,9 @@ const RulesPageComponent: React.FC = () => {
|
|||
<WrapperPage>
|
||||
<DetectionEngineHeaderPage
|
||||
backOptions={{
|
||||
href: `#${DETECTION_ENGINE_PAGE_NAME}`,
|
||||
href: getDetectionEngineUrl(),
|
||||
text: i18n.BACK_TO_DETECTION_ENGINE,
|
||||
}}
|
||||
subtitle={
|
||||
lastCompletedRun ? (
|
||||
<FormattedMessage
|
||||
id="xpack.siem.headerPage.rules.pageSubtitle"
|
||||
defaultMessage="Last completed run: {lastCompletedRun}"
|
||||
values={{
|
||||
lastCompletedRun: <FormattedRelativePreferenceDate value={lastCompletedRun} />,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
getEmptyTagValue()
|
||||
)
|
||||
}
|
||||
title={i18n.PAGE_TITLE}
|
||||
>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false} wrap={true}>
|
||||
|
@ -159,7 +146,7 @@ const RulesPageComponent: React.FC = () => {
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
href={`#${DETECTION_ENGINE_PAGE_NAME}/rules/create`}
|
||||
href={getCreateRuleUrl()}
|
||||
iconType="plusInCircle"
|
||||
isDisabled={userHasNoPermissions || loading}
|
||||
>
|
||||
|
@ -182,6 +169,7 @@ const RulesPageComponent: React.FC = () => {
|
|||
hasNoPermissions={userHasNoPermissions}
|
||||
importCompleteToggle={importCompleteToggle}
|
||||
refetchPrePackagedRulesStatus={handleRefetchPrePackagedRulesStatus}
|
||||
rulesCustomInstalled={rulesCustomInstalled}
|
||||
rulesInstalled={rulesInstalled}
|
||||
rulesNotInstalled={rulesNotInstalled}
|
||||
rulesNotUpdated={rulesNotUpdated}
|
||||
|
|
|
@ -74,26 +74,28 @@ describe('get_prepackaged_rule_status_route', () => {
|
|||
});
|
||||
|
||||
describe('payload', () => {
|
||||
test('0 rules installed, 1 rules not installed, and 1 rule not updated', async () => {
|
||||
test('0 rules installed, 0 custom rules, 1 rules not installed, and 1 rule not updated', async () => {
|
||||
alertsClient.find.mockResolvedValue(getFindResult());
|
||||
alertsClient.get.mockResolvedValue(getResult());
|
||||
actionsClient.create.mockResolvedValue(createActionResult());
|
||||
alertsClient.create.mockResolvedValue(getResult());
|
||||
const { payload } = await server.inject(getPrepackagedRulesStatusRequest());
|
||||
expect(JSON.parse(payload)).toEqual({
|
||||
rules_custom_installed: 0,
|
||||
rules_installed: 0,
|
||||
rules_not_installed: 1,
|
||||
rules_not_updated: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test('1 rule installed, 0 rules not installed, and 1 rule to not updated', async () => {
|
||||
test('1 rule installed, 1 custom rules, 0 rules not installed, and 1 rule to not updated', async () => {
|
||||
alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
|
||||
alertsClient.get.mockResolvedValue(getResult());
|
||||
actionsClient.create.mockResolvedValue(createActionResult());
|
||||
alertsClient.create.mockResolvedValue(getResult());
|
||||
const { payload } = await server.inject(getPrepackagedRulesStatusRequest());
|
||||
expect(JSON.parse(payload)).toEqual({
|
||||
rules_custom_installed: 1,
|
||||
rules_installed: 1,
|
||||
rules_not_installed: 0,
|
||||
rules_not_updated: 1,
|
||||
|
|
|
@ -13,6 +13,7 @@ import { transformError } from '../utils';
|
|||
import { getPrepackagedRules } from '../../rules/get_prepackaged_rules';
|
||||
import { getRulesToInstall } from '../../rules/get_rules_to_install';
|
||||
import { getRulesToUpdate } from '../../rules/get_rules_to_update';
|
||||
import { findRules } from '../../rules/find_rules';
|
||||
import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackaged_rules';
|
||||
|
||||
export const createGetPrepackagedRulesStatusRoute = (): Hapi.ServerRoute => {
|
||||
|
@ -36,10 +37,19 @@ export const createGetPrepackagedRulesStatusRoute = (): Hapi.ServerRoute => {
|
|||
|
||||
try {
|
||||
const rulesFromFileSystem = getPrepackagedRules();
|
||||
const customRules = await findRules({
|
||||
alertsClient,
|
||||
perPage: 1,
|
||||
page: 1,
|
||||
sortField: 'enabled',
|
||||
sortOrder: 'desc',
|
||||
filter: 'alert.attributes.tags:"__internal_immutable:false"',
|
||||
});
|
||||
const prepackagedRules = await getExistingPrepackagedRules({ alertsClient });
|
||||
const rulesToInstall = getRulesToInstall(rulesFromFileSystem, prepackagedRules);
|
||||
const rulesToUpdate = getRulesToUpdate(rulesFromFileSystem, prepackagedRules);
|
||||
return {
|
||||
rules_custom_installed: customRules.total,
|
||||
rules_installed: prepackagedRules.length,
|
||||
rules_not_installed: rulesToInstall.length,
|
||||
rules_not_updated: rulesToUpdate.length,
|
||||
|
|
Loading…
Reference in a new issue