[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:
Xavier Mouligneau 2020-01-24 16:42:53 -05:00 committed by GitHub
parent 5801de0800
commit 459b8c4df8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 327 additions and 233 deletions

View file

@ -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 &&

View file

@ -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,

View file

@ -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)) {

View file

@ -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;

View file

@ -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();

View file

@ -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,
};
};

View file

@ -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();

View file

@ -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 };
};

View file

@ -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 };
};

View file

@ -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}

View file

@ -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),

View file

@ -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,

View file

@ -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 {

View file

@ -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 (

View file

@ -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>

View file

@ -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

View file

@ -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));

View file

@ -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,

View file

@ -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': {

View file

@ -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>

View file

@ -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>

View file

@ -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}

View file

@ -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,

View file

@ -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,