From aa2378cb646d2636a3b72e34bb399bfd3a64678c Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Fri, 24 Jan 2020 16:42:53 -0500 Subject: [PATCH] [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 Co-authored-by: Garrett Spong --- .../events_viewer/events_viewer.tsx | 12 +-- .../public/components/events_viewer/index.tsx | 26 +++--- .../siem/public/components/timeline/index.tsx | 2 +- .../containers/detection_engine/rules/api.ts | 1 + .../rules/fetch_index_patterns.tsx | 8 +- .../rules/use_pre_packaged_rules.tsx | 68 +++++++++----- .../detection_engine/rules/use_rules.tsx | 4 +- .../signals/use_privilege_user.tsx | 44 +++++---- .../signals/use_signal_index.tsx | 43 ++++++--- .../siem/public/containers/source/index.tsx | 3 +- .../siem/public/containers/timeline/index.tsx | 15 ++-- .../components/signals/index.tsx | 49 +++++----- .../components/signals/types.ts | 5 ++ .../components/user_info/index.tsx | 44 ++++----- .../detection_engine/detection_engine.tsx | 6 +- .../detection_engine/rules/all/actions.tsx | 7 +- .../detection_engine/rules/all/helpers.ts | 13 +++ .../detection_engine/rules/all/index.tsx | 89 +++++++++---------- .../detection_engine/rules/all/reducer.ts | 39 ++++---- .../rules_table_filters.tsx | 14 ++- .../detection_engine/rules/details/index.tsx | 22 +++-- .../pages/detection_engine/rules/index.tsx | 30 ++----- .../get_prepackaged_rule_status_route.test.ts | 6 +- .../get_prepackaged_rules_status_route.ts | 10 +++ 24 files changed, 327 insertions(+), 233 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx index 163b345da40bd..f0f28f1dc246c 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx @@ -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 = ({ @@ -171,7 +173,7 @@ const EventsViewerComponent: React.FC = ({ {headerFilterGroup} - {utilityBar?.(totalCountMinusDeleted)} + {utilityBar?.(refetch, totalCountMinusDeleted)}
= ({ 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 && diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx index 99d174d74f3f8..d56898cae7d23 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx @@ -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 = ({ createTimeline, columns, @@ -99,16 +103,14 @@ const StatefulEventsViewerComponent: React.FC = ({ 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 = ({ [columns, id, upsertColumn, removeColumn] ); + const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]); + return ( { 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, diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx index ff556a1a9bdfc..a224e0355b5d3 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx @@ -166,7 +166,7 @@ const StatefulTimelineComponent = React.memo( updateItemsPerPage, upsertColumn, }) => { - const [loading, signalIndexExists, signalIndexName] = useSignalIndex(); + const { loading, signalIndexExists, signalIndexName } = useSignalIndex(); const indexToAdd = useMemo(() => { if (signalIndexExists && signalIndexName != null && ['signal', 'all'].includes(eventType)) { diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts index 22fb837ffb801..6b3578bacf24c 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts @@ -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; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx index f7a30766ad7d8..d376a1d6ad178 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx @@ -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>]; @@ -35,8 +35,8 @@ export const useFetchIndexPatterns = (defaultIndices: string[] = []): Return => const apolloClient = useApolloClient(); const [indices, setIndices] = useState(defaultIndices); const [indicesExists, setIndicesExists] = useState(false); - const [indexPatterns, setIndexPatterns] = useState(null); - const [browserFields, setBrowserFields] = useState(null); + const [indexPatterns, setIndexPatterns] = useState({ fields: [], title: '' }); + const [browserFields, setBrowserFields] = useState({}); const [isLoading, setIsLoading] = useState(false); const [, dispatchToaster] = useStateToaster(); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx index ee34cad873021..14d40f9ffbc37 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx @@ -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(null); - const [rulesNotInstalled, setRulesNotInstalled] = useState(null); - const [rulesNotUpdated, setRulesNotUpdated] = useState(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); - const refetchPrePackagedRules = useRef(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, }; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx index 254e8cbdc9a88..af6e437255acd 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx @@ -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(); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx index d225241875d4b..b93009c8ce2c2 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx @@ -24,10 +24,14 @@ interface Return { */ export const usePrivilegeUser = (): Return => { const [loading, setLoading] = useState(true); - const [isAuthenticated, setAuthenticated] = useState(null); - const [hasIndexManage, setHasIndexManage] = useState(null); - const [hasIndexWrite, setHasIndexWrite] = useState(null); - const [hasManageApiKey, setHasManageApiKey] = useState(null); + const [privilegeUser, setPrivilegeUser] = useState< + Pick + >({ + 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 }; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx index c1ee5fd12b8c1..2635d6e5540f1 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx @@ -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(null); - const [signalIndexExists, setSignalIndexExists] = useState(null); - const createDeSignalIndex = useRef(null); + const [signalIndex, setSignalIndex] = useState< + Pick + >({ + 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 }; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/source/index.tsx b/x-pack/legacy/plugins/siem/public/containers/source/index.tsx index e995d123b1b44..0336e4a9a977b 100644 --- a/x-pack/legacy/plugins/siem/public/containers/source/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/source/index.tsx @@ -89,7 +89,8 @@ export const WithSource = React.memo(({ children, indexToAdd, s return [...configIndex, ...indexToAdd]; } return configIndex; - }, [configIndex, DEFAULT_INDEX_KEY, indexToAdd]); + }, [configIndex, indexToAdd]); + return ( query={sourceQuery} diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx b/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx index 03fe68ca1398d..f4eb088b6ad94 100644 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx @@ -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(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), diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx index 51d8e2630459c..e65adcf3a6920 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx @@ -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 = ({ clearEventsDeleted, clearEventsLoading, clearSelected, - defaultFilters = [], + defaultFilters, from, globalFilters, globalQuery, @@ -118,7 +117,9 @@ const SignalsTableComponent: React.FC = ({ const [showClearSelectionAction, setShowClearSelectionAction] = useState(false); const [filterGroup, setFilterGroup] = useState(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 = ({ 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 = ({ setEventsDeleted: setEventsDeletedCallback, setEventsLoading: setEventsLoadingCallback, }); + refetchQuery(); }, [ getGlobalQuery, @@ -228,7 +230,7 @@ const SignalsTableComponent: React.FC = ({ // Callback for creating the SignalUtilityBar which receives totalCount from EventsViewer component const utilityBarCallback = useCallback( - (totalCount: number) => { + (refetchQuery: inputsModel.Refetch, totalCount: number) => { return ( = ({ selectedEventIds={selectedEventIds} showClearSelection={showClearSelectionAction} totalCount={totalCount} - updateSignalsStatus={updateSignalsStatusCallback} + updateSignalsStatus={updateSignalsStatusCallback.bind(null, refetchQuery)} /> ); }, @@ -283,13 +285,16 @@ const SignalsTableComponent: React.FC = ({ ); 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 = ({ [additionalActions, canUserCRUD, selectAll] ); + const headerFilterGroup = useMemo( + () => , + [onFilterGroupChangedCallback] + ); + if (loading || isEmpty(signalsIndex)) { return ( @@ -319,9 +329,7 @@ const SignalsTableComponent: React.FC = ({ pageFilters={defaultFiltersMemo} defaultModel={signalsDefaultModel} end={to} - headerFilterGroup={ - - } + 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, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/types.ts index 2366a103492ec..b3e7ed75cfb99 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/types.ts @@ -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 { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx index bbaccb7882484..0f6a51e52cd2e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx @@ -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 ( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx index 9e292fa69b2c4..cdc7d335f5fe9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx @@ -111,6 +111,10 @@ const DetectionEnginePageComponent: React.FC [detectionsTabs, tabName] ); + const indexToAdd = useMemo(() => (signalIndexName == null ? [] : [signalIndexName]), [ + signalIndexName, + ]); + if (isUserAuthenticated != null && !isUserAuthenticated && !loading) { return ( @@ -131,7 +135,7 @@ const DetectionEnginePageComponent: React.FC return ( <> {hasIndexWrite != null && !hasIndexWrite && } - + {({ indicesExist, indexPattern }) => { return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx index d6bf8643fff1c..990344772a99d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx @@ -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 diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts index 3616d4dbaad24..9a523536694d9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts @@ -50,3 +50,16 @@ export const bucketRulesResponse = (response: Array) => }, { 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)); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx index a4e7d7a3615cc..b304d77f2e276 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx @@ -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( loading, loadingCreatePrePackagedRules, refetchPrePackagedRulesStatus, + rulesCustomInstalled, rulesInstalled, rulesNotInstalled, rulesNotUpdated, @@ -109,6 +110,7 @@ export const AllRules = React.memo( 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( 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( }, [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,16 +225,18 @@ export const AllRules = React.memo( dispatch({ type: 'updateFilterOptions', filterOptions: { - ...filterOptions, ...newFilterOptions, }, - }); - dispatch({ - type: 'updatePagination', - pagination: { ...pagination, page: 1 }, + pagination: { page: 1 }, }); }, []); + const emptyPrompt = useMemo(() => { + return ( + {i18n.NO_RULES}} titleSize="xs" body={i18n.NO_RULES_BODY} /> + ); + }, []); + return ( <> ( <> - {rulesInstalled != null && rulesInstalled > 0 && ( + {((rulesCustomInstalled && rulesCustomInstalled > 0) || + (rulesInstalled != null && rulesInstalled > 0)) && ( - + )} - {isInitialLoad && isEmpty(tableData) && ( + {isInitialLoad && ( )} - {isGlobalLoading && !isEmpty(tableData) && ( + {isGlobalLoading && !isEmpty(tableData) && !isInitialLoad && ( )} - {isEmpty(tableData) && prePackagedRuleStatus === 'ruleNotInstalled' && ( - - )} - {!isEmpty(tableData) && ( + {rulesCustomInstalled != null && + rulesCustomInstalled === 0 && + prePackagedRuleStatus === 'ruleNotInstalled' && ( + + )} + {showRulesTable({ isInitialLoad, rulesCustomInstalled, rulesInstalled }) && ( <> @@ -304,24 +318,7 @@ export const AllRules = React.memo( isSelectable={!hasNoPermissions ?? false} itemId="id" items={tableData} - noItemsMessage={ - {i18n.NO_RULES}} - titleSize="xs" - body={i18n.NO_RULES_BODY} - actions={ - - {i18n.ADD_NEW_RULE} - - } - /> - } + noItemsMessage={emptyPrompt} onChange={tableOnChangeCallback} pagination={{ pageIndex: pagination.page - 1, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/reducer.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/reducer.ts index 74ce8f2847faa..3634a16cbf6ac 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/reducer.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/reducer.ts @@ -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 } + | { + type: 'updateFilterOptions'; + filterOptions: Partial; + pagination: Partial; + } | { 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': { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx index daf519f5af695..c54a2e8d49844 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx @@ -21,6 +21,8 @@ import { TagsFilterPopover } from './tags_filter_popover'; interface RulesTableFiltersProps { onFilterChanged: (filterOptions: Partial) => 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(''); const [selectedTags, setSelectedTags] = useState([]); const [showCustomRules, setShowCustomRules] = useState(false); @@ -84,13 +90,17 @@ const RulesTableFiltersComponent = ({ onFilterChanged }: RulesTableFiltersProps) withNext > {i18n.ELASTIC_RULES} + {rulesInstalled != null ? ` (${rulesInstalled})` : ''} - {i18n.CUSTOM_RULES} + <> + {i18n.CUSTOM_RULES} + {rulesCustomInstalled != null ? ` (${rulesCustomInstalled})` : ''} + diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx index 3406d5bcd6950..f9103a9d6e081 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx @@ -127,14 +127,6 @@ const RuleDetailsPageComponent: FC = ({ const userHasNoPermissions = canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false; - if ( - isSignalIndexExists != null && - isAuthenticated != null && - (!isSignalIndexExists || !isAuthenticated) - ) { - return ; - } - const title = isLoading === true || rule === null ? : rule.name; const subTitle = useMemo( () => @@ -217,6 +209,10 @@ const RuleDetailsPageComponent: FC = ({ [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 = ({ [ruleEnabled, setRuleEnabled] ); + if ( + isSignalIndexExists != null && + isAuthenticated != null && + (!isSignalIndexExists || !isAuthenticated) + ) { + return ; + } + return ( <> {hasIndexWrite != null && !hasIndexWrite && } {userHasNoPermissions && } - + {({ indicesExist, indexPattern }) => { return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx index 75b1ce71efbb6..1c0ed34e92793 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx @@ -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 ; + return ; } return ( @@ -102,22 +102,9 @@ const RulesPageComponent: React.FC = () => { , - }} - /> - ) : ( - getEmptyTagValue() - ) - } title={i18n.PAGE_TITLE} > @@ -159,7 +146,7 @@ const RulesPageComponent: React.FC = () => { @@ -182,6 +169,7 @@ const RulesPageComponent: React.FC = () => { hasNoPermissions={userHasNoPermissions} importCompleteToggle={importCompleteToggle} refetchPrePackagedRulesStatus={handleRefetchPrePackagedRulesStatus} + rulesCustomInstalled={rulesCustomInstalled} rulesInstalled={rulesInstalled} rulesNotInstalled={rulesNotInstalled} rulesNotUpdated={rulesNotUpdated} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rule_status_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rule_status_route.test.ts index 67680a8f86eec..de7f0fe26cc74 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rule_status_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rule_status_route.test.ts @@ -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, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts index 0208a209c5eae..ab6ee8e97a70f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts @@ -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,