From 2d78ddb511723be00aa8dbaca262028627186cfa Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Wed, 8 Jan 2020 14:02:44 -0500 Subject: [PATCH 01/14] allow read only user with no CRUD --- .../detection_engine/signals/translations.ts | 21 ++ .../signals/use_privilege_user.tsx | 5 + .../signals/use_signal_index.tsx | 7 +- .../components/signals/default_config.tsx | 95 ++--- .../components/signals/index.tsx | 11 +- .../signals/signals_utility_bar/index.tsx | 70 ++-- .../detection_engine/detection_engine.tsx | 10 +- .../public/pages/detection_engine/index.tsx | 25 +- .../detection_engine/rules/all/columns.tsx | 215 +++++------ .../detection_engine/rules/all/index.tsx | 312 ++++++++-------- .../detection_engine/rules/details/index.tsx | 350 +++++++++--------- .../pages/detection_engine/rules/index.tsx | 43 ++- .../index/get_index_exists.ts | 26 +- x-pack/legacy/plugins/siem/server/plugin.ts | 2 +- 14 files changed, 650 insertions(+), 542 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/translations.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/translations.ts index 5b5dc9e9699fe..2b8f54e5438df 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/translations.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/translations.ts @@ -12,3 +12,24 @@ export const SIGNAL_FETCH_FAILURE = i18n.translate( defaultMessage: 'Failed to query signals', } ); + +export const PRIVILEGE_FETCH_FAILURE = i18n.translate( + 'xpack.siem.containers.detectionEngine.signals.errorFetchingSignalsDescription', + { + defaultMessage: 'Failed to query signals', + } +); + +export const SIGNAL_GET_NAME_FAILURE = i18n.translate( + 'xpack.siem.containers.detectionEngine.signals.errorGetSignalDescription', + { + defaultMessage: 'Failed to get signal index name', + } +); + +export const SIGNAL_POST_FAILURE = i18n.translate( + 'xpack.siem.containers.detectionEngine.signals.errorPostSignalDescription', + { + defaultMessage: 'Failed to create signal index', + } +); 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 aa66df53d9fd9..b90cc761a9b34 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 @@ -6,7 +6,10 @@ import { useEffect, useState } from 'react'; +import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; +import { useStateToaster } from '../../../components/toasters'; import { getUserPrivilege } from './api'; +import * as i18n from './translations'; type Return = [boolean, boolean | null, boolean | null]; @@ -18,6 +21,7 @@ export const usePrivilegeUser = (): Return => { const [loading, setLoading] = useState(true); const [isAuthenticated, setAuthenticated] = useState(null); const [hasWrite, setHasWrite] = useState(null); + const [, dispatchToaster] = useStateToaster(); useEffect(() => { let isSubscribed = true; @@ -41,6 +45,7 @@ export const usePrivilegeUser = (): Return => { if (isSubscribed) { setAuthenticated(false); setHasWrite(false); + errorToToaster({ title: i18n.PRIVILEGE_FETCH_FAILURE, error, dispatchToaster }); } } if (isSubscribed) { 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 1ff4422cf6411..c1ee5fd12b8c1 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 @@ -10,7 +10,7 @@ import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; import { useStateToaster } from '../../../components/toasters'; import { createSignalIndex, getSignalIndex } from './api'; import * as i18n from './translations'; -import { PostSignalError } from './types'; +import { PostSignalError, SignalIndexError } from './types'; type Func = () => void; @@ -45,6 +45,9 @@ export const useSignalIndex = (): Return => { if (isSubscribed) { setSignalIndexName(null); setSignalIndexExists(false); + if (error instanceof SignalIndexError && error.statusCode !== 404) { + errorToToaster({ title: i18n.SIGNAL_GET_NAME_FAILURE, error, dispatchToaster }); + } } } if (isSubscribed) { @@ -69,7 +72,7 @@ export const useSignalIndex = (): Return => { } else { setSignalIndexName(null); setSignalIndexExists(false); - errorToToaster({ title: i18n.SIGNAL_FETCH_FAILURE, error, dispatchToaster }); + errorToToaster({ title: i18n.SIGNAL_POST_FAILURE, error, dispatchToaster }); } } } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx index 1a7ad5822a246..5bf661c2f8ca8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx @@ -168,55 +168,64 @@ export const requiredFieldsForActions = [ ]; export const getSignalsActions = ({ + canUserCRUD, setEventsLoading, setEventsDeleted, createTimeline, status, }: { + canUserCRUD: boolean; setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void; setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void; createTimeline: CreateTimeline; status: 'open' | 'closed'; -}): TimelineAction[] => [ - { - getAction: ({ eventId, data }: TimelineActionProps): JSX.Element => ( - - sendSignalsToTimelineAction({ createTimeline, data: [data] })} - iconType="tableDensityNormal" - aria-label="Next" - /> - - ), - id: 'sendSignalToTimeline', - width: 26, - }, - { - getAction: ({ eventId, data }: TimelineActionProps): JSX.Element => ( - - - updateSignalStatusAction({ - signalIds: [eventId], - status, - setEventsLoading, - setEventsDeleted, - }) - } - iconType={status === FILTER_OPEN ? 'indexOpen' : 'indexClose'} - aria-label="Next" - /> - - ), - id: 'updateSignalStatus', - width: 26, - }, -]; +}): TimelineAction[] => { + const actions = [ + { + getAction: ({ eventId, data }: TimelineActionProps): JSX.Element => ( + + sendSignalsToTimelineAction({ createTimeline, data: [data] })} + iconType="tableDensityNormal" + aria-label="Next" + /> + + ), + id: 'sendSignalToTimeline', + width: 26, + }, + ]; + return canUserCRUD + ? [ + ...actions, + { + getAction: ({ eventId, data }: TimelineActionProps): JSX.Element => ( + + + updateSignalStatusAction({ + signalIds: [eventId], + status, + setEventsLoading, + setEventsDeleted, + }) + } + iconType={status === FILTER_OPEN ? 'indexOpen' : 'indexClose'} + aria-label="Next" + /> + + ), + id: 'updateSignalStatus', + width: 26, + }, + ] + : actions; +}; 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 47a78482cfb6e..4b226350eceb4 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 @@ -88,6 +88,7 @@ interface DispatchProps { } interface OwnProps { + canUserCRUD: boolean; defaultFilters?: esFilters.Filter[]; from: number; signalsIndex: string; @@ -98,6 +99,7 @@ type SignalsTableComponentProps = OwnProps & ReduxProps & DispatchProps; export const SignalsTableComponent = React.memo( ({ + canUserCRUD, createTimeline, clearEventsDeleted, clearEventsLoading, @@ -228,6 +230,7 @@ export const SignalsTableComponent = React.memo( (totalCount: number) => { return ( 0} clearSelection={clearSelectionCallback} isFilteredToOpen={filterGroup === FILTER_OPEN} @@ -241,6 +244,7 @@ export const SignalsTableComponent = React.memo( ); }, [ + canUserCRUD, clearSelectionCallback, filterGroup, loadingEventIds.length, @@ -254,12 +258,13 @@ export const SignalsTableComponent = React.memo( const additionalActions = useMemo( () => getSignalsActions({ + canUserCRUD, createTimeline: createTimelineCallback, setEventsLoading: setEventsLoadingCallback, setEventsDeleted: setEventsDeletedCallback, status: filterGroup === FILTER_OPEN ? FILTER_CLOSED : FILTER_OPEN, }), - [createTimelineCallback, filterGroup] + [canUserCRUD, createTimelineCallback, filterGroup] ); const defaultIndices = useMemo(() => [signalsIndex], [signalsIndex]); @@ -279,9 +284,9 @@ export const SignalsTableComponent = React.memo( queryFields: requiredFieldsForActions, timelineActions: additionalActions, title: i18n.SIGNALS_TABLE_TITLE, - selectAll, + selectAll: canUserCRUD ? selectAll : false, }), - [additionalActions, selectAll] + [additionalActions, canUserCRUD, selectAll] ); return ( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx index f80de053b59bd..c5ffb519fb1b3 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx @@ -22,6 +22,7 @@ import { TimelineNonEcsData } from '../../../../../graphql/types'; import { SendSignalsToTimeline, UpdateSignalsStatus } from '../types'; interface SignalsUtilityBarProps { + canUserCRUD: boolean; areEventsLoading: boolean; clearSelection: () => void; isFilteredToOpen: boolean; @@ -34,6 +35,7 @@ interface SignalsUtilityBarProps { } const SignalsUtilityBarComponent: React.FC = ({ + canUserCRUD, areEventsLoading, clearSelection, totalCount, @@ -82,41 +84,43 @@ const SignalsUtilityBarComponent: React.FC = ({ {i18n.SHOWING_SIGNALS(formattedTotalCount, totalCount)} - - {totalCount > 0 && ( - <> - - {i18n.SELECTED_SIGNALS( - showClearSelection ? formattedTotalCount : formattedSelectedEventsCount, - showClearSelection ? totalCount : Object.keys(selectedEventIds).length - )} - + {canUserCRUD && ( + + {totalCount > 0 && ( + <> + + {i18n.SELECTED_SIGNALS( + showClearSelection ? formattedTotalCount : formattedSelectedEventsCount, + showClearSelection ? totalCount : Object.keys(selectedEventIds).length + )} + - - {i18n.BATCH_ACTIONS} - + + {i18n.BATCH_ACTIONS} + - { - if (!showClearSelection) { - selectAll(); - } else { - clearSelection(); - } - }} - > - {showClearSelection - ? i18n.CLEAR_SELECTION - : i18n.SELECT_ALL_SIGNALS(formattedTotalCount, totalCount)} - - - )} - + { + if (!showClearSelection) { + selectAll(); + } else { + clearSelection(); + } + }} + > + {showClearSelection + ? i18n.CLEAR_SELECTION + : i18n.SELECT_ALL_SIGNALS(formattedTotalCount, totalCount)} + + + )} + + )} 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 8e5c3e9f13118..7654614c35074 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 @@ -27,6 +27,7 @@ import * as i18n from './translations'; import { HeaderSection } from '../../components/header_section'; interface DetectionEngineComponentProps { + canUserCRUD: boolean; loading: boolean; isSignalIndexExists: boolean | null; isUserAuthenticated: boolean | null; @@ -34,7 +35,7 @@ interface DetectionEngineComponentProps { } export const DetectionEngineComponent = React.memo( - ({ loading, isSignalIndexExists, isUserAuthenticated, signalsIndex }) => { + ({ canUserCRUD, loading, isSignalIndexExists, isUserAuthenticated, signalsIndex }) => { const [lastSignals] = useSignalInfo({}); if (isUserAuthenticated != null && !isUserAuthenticated && !loading) { return ( @@ -88,7 +89,12 @@ export const DetectionEngineComponent = React.memo !loading ? ( isSignalIndexExists && ( - + ) ) : ( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx index e8a2c98a94a56..02c3359deee46 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx @@ -7,6 +7,8 @@ import React, { useEffect } from 'react'; import { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; + import { useSignalIndex } from '../../containers/detection_engine/signals/use_signal_index'; import { usePrivilegeUser } from '../../containers/detection_engine/signals/use_privilege_user'; @@ -28,6 +30,8 @@ export const DetectionEngineContainer = React.memo(() => { signalIndexName, createSignalIndex, ] = useSignalIndex(); + const uiCapabilities = useKibana().services.application?.capabilities; + const canUserCRUD = (uiCapabilities?.siem?.crud as boolean) ?? false; useEffect(() => { if ( @@ -45,6 +49,7 @@ export const DetectionEngineContainer = React.memo(() => { (() => { {isSignalIndexExists && isAuthenticated && ( <> - - - - + + {canUserCRUD && ( + + + + )} - - - - + + {canUserCRUD && ( + + + + )} )} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx index 42c4bb1d0ef95..a98397ecf6f9d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx @@ -68,111 +68,120 @@ const getActions = (dispatch: React.Dispatch, history: H.History) => [ }, ]; +type RulesColumns = EuiBasicTableColumn | EuiTableActionsColumnType; + // Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes? export const getColumns = ( dispatch: React.Dispatch, - history: H.History -): Array | EuiTableActionsColumnType> => [ - { - field: 'rule', - name: i18n.COLUMN_RULE, - render: (value: TableData['rule']) => {value.name}, - truncateText: true, - width: '24%', - }, - { - field: 'method', - name: i18n.COLUMN_METHOD, - truncateText: true, - }, - { - field: 'severity', - name: i18n.COLUMN_SEVERITY, - render: (value: TableData['severity']) => ( - - {value} - - ), - truncateText: true, - }, - { - field: 'lastCompletedRun', - name: i18n.COLUMN_LAST_COMPLETE_RUN, - render: (value: TableData['lastCompletedRun']) => { - return value == null ? ( - getEmptyTagValue() - ) : ( - - ); + history: H.History, + canUserCRUD: boolean +): RulesColumns[] => { + const cols: RulesColumns[] = [ + { + field: 'rule', + name: i18n.COLUMN_RULE, + render: (value: TableData['rule']) => {value.name}, + truncateText: true, + width: '24%', }, - sortable: true, - truncateText: true, - width: '16%', - }, - { - field: 'lastResponse', - name: i18n.COLUMN_LAST_RESPONSE, - render: (value: TableData['lastResponse']) => { - return value == null ? ( - getEmptyTagValue() - ) : ( - <> - {value.type === 'Fail' ? ( - - {value.type} - - ) : ( - {value.type} - )} - - ); + { + field: 'method', + name: i18n.COLUMN_METHOD, + truncateText: true, }, - truncateText: true, - }, - { - field: 'tags', - name: i18n.COLUMN_TAGS, - render: (value: TableData['tags']) => ( -
- <> - {value.map((tag, i) => ( - - {tag} - - ))} - -
- ), - truncateText: true, - width: '20%', - }, - { - align: 'center', - field: 'activate', - name: i18n.COLUMN_ACTIVATE, - render: (value: TableData['activate'], item: TableData) => ( - - ), - sortable: true, - width: '85px', - }, - { - actions: getActions(dispatch, history), - width: '40px', - } as EuiTableActionsColumnType, -]; + { + field: 'severity', + name: i18n.COLUMN_SEVERITY, + render: (value: TableData['severity']) => ( + + {value} + + ), + truncateText: true, + }, + { + field: 'lastCompletedRun', + name: i18n.COLUMN_LAST_COMPLETE_RUN, + render: (value: TableData['lastCompletedRun']) => { + return value == null ? ( + getEmptyTagValue() + ) : ( + + ); + }, + sortable: true, + truncateText: true, + width: '16%', + }, + { + field: 'lastResponse', + name: i18n.COLUMN_LAST_RESPONSE, + render: (value: TableData['lastResponse']) => { + return value == null ? ( + getEmptyTagValue() + ) : ( + <> + {value.type === 'Fail' ? ( + + {value.type} + + ) : ( + {value.type} + )} + + ); + }, + truncateText: true, + }, + { + field: 'tags', + name: i18n.COLUMN_TAGS, + render: (value: TableData['tags']) => ( +
+ <> + {value.map((tag, i) => ( + + {tag} + + ))} + +
+ ), + truncateText: true, + width: '20%', + }, + ]; + const actions: RulesColumns[] = [ + { + align: 'center', + field: 'activate', + name: i18n.COLUMN_ACTIVATE, + render: (value: TableData['activate'], item: TableData) => ( + + ), + sortable: true, + width: '85px', + }, + { + actions: getActions(dispatch, history), + width: '40px', + } as EuiTableActionsColumnType, + ]; + + return canUserCRUD ? [...cols, ...actions] : cols; +}; 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 060f8baccc3b7..dcc494d99d041 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 @@ -11,7 +11,7 @@ import { EuiLoadingContent, EuiSpacer, } from '@elastic/eui'; -import React, { useCallback, useEffect, useReducer, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react'; import { useHistory } from 'react-router-dom'; import uuid from 'uuid'; @@ -60,163 +60,175 @@ const initialState: State = { * * Delete * * Import/Export */ -export const AllRules = React.memo<{ importCompleteToggle: boolean }>(importCompleteToggle => { - const [ - { - exportPayload, - filterOptions, - isLoading, - refreshToggle, - selectedItems, - tableData, - pagination, - }, - dispatch, - ] = useReducer(allRulesReducer, initialState); - const history = useHistory(); - const [isInitialLoad, setIsInitialLoad] = useState(true); - const [isLoadingRules, rulesData] = useRules(pagination, filterOptions, refreshToggle); - const [, dispatchToaster] = useStateToaster(); - - const getBatchItemsPopoverContent = useCallback( - (closePopover: () => void) => ( - - ), - [selectedItems, dispatch] - ); - - useEffect(() => { - dispatch({ type: 'loading', isLoading: isLoadingRules }); - - if (!isLoadingRules) { - setIsInitialLoad(false); - } - }, [isLoadingRules]); - - useEffect(() => { - if (!isInitialLoad) { - dispatch({ type: 'refresh' }); - } - }, [importCompleteToggle]); - - useEffect(() => { - dispatch({ - type: 'updateRules', - rules: rulesData.data, - pagination: { - page: rulesData.page, - perPage: rulesData.perPage, - total: rulesData.total, +export const AllRules = React.memo<{ canUserCRUD: boolean; importCompleteToggle: boolean }>( + ({ canUserCRUD, importCompleteToggle }) => { + const [ + { + exportPayload, + filterOptions, + isLoading, + refreshToggle, + selectedItems, + tableData, + pagination, }, - }); - }, [rulesData]); - - return ( - <> - { - dispatchToaster({ - type: 'addToaster', - toast: { - id: uuid.v4(), - title: i18n.SUCCESSFULLY_EXPORTED_RULES(exportCount), - color: 'success', - iconType: 'check', - }, - }); - }} - /> - - - - {isInitialLoad ? ( - - ) : ( - <> - - { + dispatch, + ] = useReducer(allRulesReducer, initialState); + const history = useHistory(); + const [isInitialLoad, setIsInitialLoad] = useState(true); + const [isLoadingRules, rulesData] = useRules(pagination, filterOptions, refreshToggle); + const [, dispatchToaster] = useStateToaster(); + + const getBatchItemsPopoverContent = useCallback( + (closePopover: () => void) => ( + + ), + [selectedItems, dispatch] + ); + + useEffect(() => { + dispatch({ type: 'loading', isLoading: isLoadingRules }); + + if (!isLoadingRules) { + setIsInitialLoad(false); + } + }, [isLoadingRules]); + + useEffect(() => { + if (!isInitialLoad) { + dispatch({ type: 'refresh' }); + } + }, [importCompleteToggle]); + + useEffect(() => { + dispatch({ + type: 'updateRules', + rules: rulesData.data, + pagination: { + page: rulesData.page, + perPage: rulesData.perPage, + total: rulesData.total, + }, + }); + }, [rulesData]); + + const euiBasicTableSelectionProps = useMemo( + () => + canUserCRUD + ? { + selectable: (item: TableData) => !item.isLoading, + onSelectionChange: (selected: TableData[]) => + dispatch({ type: 'setSelected', selectedItems: selected }), + } + : {}, + [canUserCRUD] + ); + + return ( + <> + { + dispatchToaster({ + type: 'addToaster', + toast: { + id: uuid.v4(), + title: i18n.SUCCESSFULLY_EXPORTED_RULES(exportCount), + color: 'success', + iconType: 'check', + }, + }); + }} + /> + + + + {isInitialLoad ? ( + + ) : ( + <> + + { + dispatch({ + type: 'updateFilterOptions', + filterOptions: { + ...filterOptions, + filter: filterString, + }, + }); + }} + /> + + + + + + {i18n.SHOWING_RULES(pagination.total ?? 0)} + + + + {i18n.SELECTED_RULES(selectedItems.length)} + {canUserCRUD && ( + + {i18n.BATCH_ACTIONS} + + )} + dispatch({ type: 'refresh' })} + > + {i18n.REFRESH} + + + + + + { + dispatch({ + type: 'updatePagination', + pagination: { ...pagination, page: page.index + 1, perPage: page.size }, + }); dispatch({ type: 'updateFilterOptions', filterOptions: { ...filterOptions, - filter: filterString, + sortField: 'enabled', // Only enabled is supported for sorting currently + sortOrder: sort!.direction, }, }); }} + pagination={{ + pageIndex: pagination.page - 1, + pageSize: pagination.perPage, + totalItemCount: pagination.total, + pageSizeOptions: [5, 10, 20], + }} + sorting={{ sort: { field: 'activate', direction: filterOptions.sortOrder } }} + {...euiBasicTableSelectionProps} /> - - - - - - {i18n.SHOWING_RULES(pagination.total ?? 0)} - - - - {i18n.SELECTED_RULES(selectedItems.length)} - - {i18n.BATCH_ACTIONS} - - dispatch({ type: 'refresh' })} - > - {i18n.REFRESH} - - - - - - { - 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, - }, - }); - }} - pagination={{ - pageIndex: pagination.page - 1, - pageSize: pagination.perPage, - totalItemCount: pagination.total, - pageSizeOptions: [5, 10, 20], - }} - selection={{ - selectable: (item: TableData) => !item.isLoading, - onSelectionChange: (selected: TableData[]) => - dispatch({ type: 'setSelected', selectedItems: selected }), - }} - sorting={{ sort: { field: 'activate', direction: filterOptions.sortOrder } }} - /> - {isLoading && } - - )} - - - ); -}); + {isLoading && } + + )} + + + ); + } +); AllRules.displayName = 'AllRules'; 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 1bc2bc24517e3..39b4f391602fe 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 @@ -41,193 +41,199 @@ import * as i18n from './translations'; import { GlobalTime } from '../../../../containers/global_time'; interface RuleDetailsComponentProps { + canUserCRUD: boolean; signalsIndex: string | null; } -export const RuleDetailsComponent = memo(({ signalsIndex }) => { - const { ruleId } = useParams(); - const [loading, rule] = useRule(ruleId); - const { aboutRuleData, defineRuleData, scheduleRuleData } = getStepsData({ - rule, - detailsView: true, - }); - const [lastSignals] = useSignalInfo({ ruleId }); - - const title = loading === true || rule === null ? : rule.name; - const subTitle = useMemo( - () => - loading === true || rule === null ? ( - - ) : ( - [ - - ), - }} - />, - rule?.updated_by != null ? ( +export const RuleDetailsComponent = memo( + ({ canUserCRUD, signalsIndex }) => { + const { ruleId } = useParams(); + const [loading, rule] = useRule(ruleId); + const { aboutRuleData, defineRuleData, scheduleRuleData } = getStepsData({ + rule, + detailsView: true, + }); + const [lastSignals] = useSignalInfo({ ruleId }); + + const title = loading === true || rule === null ? : rule.name; + const subTitle = useMemo( + () => + loading === true || rule === null ? ( + + ) : ( + [ ), }} - /> - ) : ( - '' - ), - ] - ), - [loading, rule] - ); - - const signalDefaultFilters = useMemo( - () => (ruleId != null ? buildSignalsRuleIdFilter(ruleId) : []), - [ruleId] - ); - return ( - <> - - {({ indicesExist, indexPattern }) => { - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - {({ to, from }) => ( - - - - - - - - {detectionI18n.LAST_SIGNAL} - {': '} - {lastSignals} - - ) : null, - 'Status: Comming Soon', - ]} - title={title} - > - - - - + />, + rule?.updated_by != null ? ( + + ), + }} + /> + ) : ( + '' + ), + ] + ), + [loading, rule] + ); + + const signalDefaultFilters = useMemo( + () => (ruleId != null ? buildSignalsRuleIdFilter(ruleId) : []), + [ruleId] + ); + return ( + <> + + {({ indicesExist, indexPattern }) => { + return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + + {({ to, from }) => ( + + + + + + + + {detectionI18n.LAST_SIGNAL} + {': '} + {lastSignals} + + ) : null, + 'Status: Comming Soon', + ]} + title={title} + > + {canUserCRUD && ( + + + + - - - - {ruleI18n.EDIT_RULE_SETTINGS} - + + + + {ruleI18n.EDIT_RULE_SETTINGS} + + + + )} + + + + + + + + {defineRuleData != null && ( + + )} + + + + + + {aboutRuleData != null && ( + + )} + + + + + + {scheduleRuleData != null && ( + + )} + - - - - - - - - {defineRuleData != null && ( - - )} - - - - - - {aboutRuleData != null && ( - - )} - - - - - - {scheduleRuleData != null && ( - - )} - - - - - - - - - - - {ruleId != null && ( - - )} - - - )} - - ) : ( - - - - - - ); - }} - - - - - ); -}); + + + + + + + + {ruleId != null && ( + + )} + + + )} + + ) : ( + + + + + + ); + }} + + + + + ); + } +); RuleDetailsComponent.displayName = 'RuleDetailsComponent'; 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 8b4cc2a213589..3ff382f3fb797 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 @@ -18,7 +18,10 @@ import { AllRules } from './all'; import { ImportRuleModal } from './components/import_rule_modal'; import * as i18n from './translations'; -export const RulesComponent = React.memo(() => { +interface RulesComponentProps { + canUserCRUD: boolean; +} +export const RulesComponent = React.memo(({ canUserCRUD }) => { const [showImportModal, setShowImportModal] = useState(false); const [importCompleteToggle, setImportCompleteToggle] = useState(false); @@ -48,27 +51,29 @@ export const RulesComponent = React.memo(() => { } title={i18n.PAGE_TITLE} > - - - { - setShowImportModal(true); - }} - > - {i18n.IMPORT_RULE} - - + {canUserCRUD && ( + + + { + setShowImportModal(true); + }} + > + {i18n.IMPORT_RULE} + + - - - {i18n.ADD_NEW_RULE} - - - + + + {i18n.ADD_NEW_RULE} + + + + )} - + diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts index ff65caa59a866..4ccf8691234e8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts @@ -4,15 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IndicesExistsParams } from 'elasticsearch'; -import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; import { CallWithRequest } from '../types'; export const getIndexExists = async ( - callWithRequest: CallWithRequest, + callWithRequest: CallWithRequest< + { index: string; size: number; terminate_after: number; allow_no_indices: boolean }, + {}, + unknown + >, index: string ): Promise => { - return callWithRequest('indices.exists', { - index, - }); + try { + callWithRequest('search', { + index, + size: 0, + terminate_after: 1, + allow_no_indices: true, + }); + return true; + } catch (err) { + if (err.statusCode === 404) { + return false; + } else { + throw err; + } + } }; diff --git a/x-pack/legacy/plugins/siem/server/plugin.ts b/x-pack/legacy/plugins/siem/server/plugin.ts index 8a47aa2a27082..90ae79ef19d5b 100644 --- a/x-pack/legacy/plugins/siem/server/plugin.ts +++ b/x-pack/legacy/plugins/siem/server/plugin.ts @@ -60,7 +60,7 @@ export class Plugin { ], read: ['config'], }, - ui: ['show'], + ui: ['show', 'crud'], }, read: { api: ['siem', 'actions-read', 'actions-all', 'alerting-read', 'alerting-all'], From 4bf638e7702e4953ab4cd5b92d50eca9e00f981a Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Wed, 8 Jan 2020 17:18:37 -0500 Subject: [PATCH 02/14] use ../../lib/kibana --- .../plugins/siem/public/pages/detection_engine/index.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx index 02c3359deee46..861d6c5d7e2b6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx @@ -7,11 +7,9 @@ import React, { useEffect } from 'react'; import { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; - import { useSignalIndex } from '../../containers/detection_engine/signals/use_signal_index'; import { usePrivilegeUser } from '../../containers/detection_engine/signals/use_privilege_user'; - +import { useKibana } from '../../lib/kibana'; import { CreateRuleComponent } from './rules/create'; import { DetectionEngineComponent } from './detection_engine'; import { EditRuleComponent } from './rules/edit'; From 431ccd0cb75518495ec9ec2e70ee808cf9d555d3 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Thu, 9 Jan 2020 13:03:58 -0500 Subject: [PATCH 03/14] fix timeline-template --- .../timeline/search_super_select/index.tsx | 29 ++++++++++--------- .../detection_engine/rules/create/helpers.ts | 8 +++-- .../pages/detection_engine/rules/types.ts | 4 +-- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx index ac47b352a6276..21d58e2f1fb4a 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx @@ -119,20 +119,23 @@ const SearchTimelineSuperSelectComponent: React.FC { - const selectedTimeline = options.filter( - (option: { checked: string }) => option.checked === 'on' - ); - if (selectedTimeline != null && selectedTimeline.length > 0 && onTimelineChange != null) { - onTimelineChange( - isEmpty(selectedTimeline[0].title) - ? i18nTimeline.UNTITLED_TIMELINE - : selectedTimeline[0].title, - selectedTimeline[0].id + const handleTimelineChange = useCallback( + options => { + const selectedTimeline = options.filter( + (option: { checked: string }) => option.checked === 'on' ); - } - setIsPopoverOpen(false); - }, []); + if (selectedTimeline != null && selectedTimeline.length > 0) { + onTimelineChange( + isEmpty(selectedTimeline[0].title) + ? i18nTimeline.UNTITLED_TIMELINE + : selectedTimeline[0].title, + selectedTimeline[0].id === '-1' ? null : selectedTimeline[0].id + ); + } + setIsPopoverOpen(false); + }, + [onTimelineChange] + ); const handleOnScroll = useCallback( ( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts index 12bbdbdfff3e9..ce91e15cdcf0d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts @@ -85,8 +85,12 @@ const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRuleJson => false_positives: falsePositives.filter(item => !isEmpty(item)), references: references.filter(item => !isEmpty(item)), risk_score: riskScore, - timeline_id: timeline.id, - timeline_title: timeline.title, + ...(timeline.id != null && timeline.title != null + ? { + timeline_id: timeline.id, + timeline_title: timeline.title, + } + : {}), threats: threats .filter(threat => threat.tactic.name !== 'none') .map(threat => ({ diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts index ec4206623bad9..541b058951be7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts @@ -109,8 +109,8 @@ export interface AboutStepRuleJson { references: string[]; false_positives: string[]; tags: string[]; - timeline_id: string | null; - timeline_title: string | null; + timeline_id?: string; + timeline_title?: string; threats: IMitreEnterpriseAttack[]; } From 2fdd96766769744b3ee3835fb810c1209c4c62a9 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Thu, 9 Jan 2020 15:29:52 -0500 Subject: [PATCH 04/14] add re-routing on page --- .../containers/detection_engine/rules/api.ts | 25 +- .../detection_engine/rules/types.ts | 4 + .../signals/use_privilege_user.tsx | 2 +- .../signals/use_signal_index.tsx | 2 + .../components/signals/index.tsx | 14 + .../components/user_info/index.tsx | 185 +++++++++ .../detection_engine/detection_engine.tsx | 31 +- .../public/pages/detection_engine/index.tsx | 95 ++--- .../detection_engine/rules/all/columns.tsx | 5 +- .../detection_engine/rules/all/index.tsx | 326 +++++++-------- .../components/read_only_callout/index.tsx | 26 ++ .../read_only_callout/translations.ts | 26 ++ .../rules/components/rule_switch/index.tsx | 4 +- .../detection_engine/rules/create/index.tsx | 20 +- .../detection_engine/rules/details/index.tsx | 378 +++++++++--------- .../detection_engine/rules/edit/index.tsx | 26 +- .../pages/detection_engine/rules/helpers.tsx | 3 + .../pages/detection_engine/rules/index.tsx | 72 ++-- 18 files changed, 778 insertions(+), 466 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/translations.ts 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 f9611995cdb04..b69a8de29e047 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 @@ -15,9 +15,13 @@ import { NewRule, Rule, FetchRuleProps, + BasicFetchProps, } from './types'; import { throwIfNotOk } from '../../../hooks/api/api'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; +import { + DETECTION_ENGINE_RULES_URL, + DETECTION_ENGINE_PREPACKAGED_URL, +} from '../../../../common/constants'; /** * Add provided Rule @@ -199,3 +203,22 @@ export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise>(response => response.json()) ); }; + +/** + * Create Prepackaged Rules + * + * @param signal AbortSignal for cancelling request + */ +export const createPrepackagedRules = async ({ signal }: BasicFetchProps): Promise => { + const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_PREPACKAGED_URL}`, { + method: 'PUT', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-xsrf': 'true', + }, + signal, + }); + await throwIfNotOk(response); + return true; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts index 655299c4a2a34..a329d96d444aa 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts @@ -132,3 +132,7 @@ export interface DeleteRulesProps { export interface DuplicateRulesProps { rules: Rules; } + +export interface BasicFetchProps { + signal: AbortSignal; +} 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 b90cc761a9b34..c229a635a44a0 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 @@ -38,7 +38,7 @@ export const usePrivilegeUser = (): Return => { setAuthenticated(privilege.isAuthenticated); if (privilege.index != null && Object.keys(privilege.index).length > 0) { const indexName = Object.keys(privilege.index)[0]; - setHasWrite(privilege.index[indexName].create_index); + setHasWrite(privilege.index[indexName].manage); } } } catch (error) { 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..189d8a1bf3f75 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 @@ -8,6 +8,7 @@ import { useEffect, useState, useRef } from 'react'; import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; import { useStateToaster } from '../../../components/toasters'; +import { createPrepackagedRules } from '../rules'; import { createSignalIndex, getSignalIndex } from './api'; import * as i18n from './translations'; import { PostSignalError, SignalIndexError } from './types'; @@ -40,6 +41,7 @@ export const useSignalIndex = (): Return => { if (isSubscribed && signal != null) { setSignalIndexName(signal.name); setSignalIndexExists(true); + createPrepackagedRules({ signal: abortCtrl.signal }); } } catch (error) { if (isSubscribed) { 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 4b226350eceb4..aea28ef57f380 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 @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiPanel, EuiLoadingContent } from '@elastic/eui'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; @@ -46,6 +47,8 @@ import { useFetchIndexPatterns } from '../../../../containers/detection_engine/r import { InputsRange } from '../../../../store/inputs/model'; import { Query } from '../../../../../../../../../src/plugins/data/common/query'; +import { HeaderSection } from '../../../../components/header_section'; + const SIGNALS_PAGE_TIMELINE_ID = 'signals-page'; interface ReduxProps { @@ -91,6 +94,7 @@ interface OwnProps { canUserCRUD: boolean; defaultFilters?: esFilters.Filter[]; from: number; + loading: boolean; signalsIndex: string; to: number; } @@ -109,6 +113,7 @@ export const SignalsTableComponent = React.memo( globalFilters, globalQuery, isSelectAllChecked, + loading, loadingEventIds, removeTimelineLinkTo, selectedEventIds, @@ -289,6 +294,15 @@ export const SignalsTableComponent = React.memo( [additionalActions, canUserCRUD, selectAll] ); + if (loading) { + return ( + + + + + ); + } + return ( { + switch (action.type) { + case 'updateLoading': { + return { + ...state, + loading: action.loading, + }; + } + case 'updateHasWrite': { + return { + ...state, + hasWrite: action.hasWrite, + }; + } + case 'updateIsSignalIndexExists': { + return { + ...state, + isSignalIndexExists: action.isSignalIndexExists, + }; + } + case 'updateIsAuthenticated': { + return { + ...state, + isAuthenticated: action.isAuthenticated, + }; + } + case 'updateCanUserCRUD': { + return { + ...state, + canUserCRUD: action.canUserCRUD, + }; + } + case 'updateSignalIndexName': { + return { + ...state, + signalIndexName: action.signalIndexName, + }; + } + default: + return state; + } +}; + +const StateUserInfoContext = createContext<[State, Dispatch]>([initialState, () => noop]); + +const useUserData = () => useContext(StateUserInfoContext); + +interface ManageUserInfoProps { + children: React.ReactNode; +} + +export const ManageUserInfo = ({ children }: ManageUserInfoProps) => ( + + {children} + +); + +export const useUserInfo = (): Return => { + const [ + { canUserCRUD, hasWrite, isSignalIndexExists, isAuthenticated, loading, signalIndexName }, + dispatch, + ] = useUserData(); + const [privilegeLoading, isApiAuthenticated, hasApiWrite] = usePrivilegeUser(); + const [ + indexNameLoading, + isApiSignalIndexExists, + apiSignalIndexName, + createSignalIndex, + ] = useSignalIndex(); + + const uiCapabilities = useKibana().services.application?.capabilities; + const capabilitiesCanUserCRUD = (uiCapabilities?.siem?.crud as boolean) ?? false; + + useEffect(() => { + if (loading !== privilegeLoading || indexNameLoading) { + dispatch({ type: 'updateLoading', loading: privilegeLoading || indexNameLoading }); + } + }, [loading, privilegeLoading, indexNameLoading]); + + useEffect(() => { + if (hasWrite !== hasApiWrite && hasApiWrite != null) { + dispatch({ type: 'updateHasWrite', hasWrite: hasApiWrite }); + } + }, [hasWrite, hasApiWrite]); + + useEffect(() => { + if (isSignalIndexExists !== isApiSignalIndexExists && isApiSignalIndexExists != null) { + dispatch({ type: 'updateIsSignalIndexExists', isSignalIndexExists: isApiSignalIndexExists }); + } + }, [isSignalIndexExists, isApiSignalIndexExists]); + + useEffect(() => { + if (isAuthenticated !== isApiAuthenticated && isApiAuthenticated != null) { + dispatch({ type: 'updateIsAuthenticated', isAuthenticated: isApiAuthenticated }); + } + }, [isAuthenticated, isApiAuthenticated]); + + useEffect(() => { + if (canUserCRUD !== capabilitiesCanUserCRUD && capabilitiesCanUserCRUD != null) { + dispatch({ type: 'updateCanUserCRUD', canUserCRUD: capabilitiesCanUserCRUD }); + } + }, [canUserCRUD, capabilitiesCanUserCRUD]); + + useEffect(() => { + if (signalIndexName !== apiSignalIndexName && apiSignalIndexName != null) { + dispatch({ type: 'updateSignalIndexName', signalIndexName: apiSignalIndexName }); + } + }, [signalIndexName, apiSignalIndexName]); + + useEffect(() => { + if ( + isAuthenticated && + hasApiWrite && + isSignalIndexExists != null && + !isSignalIndexExists && + createSignalIndex != null + ) { + createSignalIndex(); + } + }, [createSignalIndex, isAuthenticated, isSignalIndexExists, hasWrite]); + + return [ + indexNameLoading || privilegeLoading, + isSignalIndexExists, + isAuthenticated, + canUserCRUD ?? false, + signalIndexName, + ]; +}; 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 7654614c35074..a1bd8b2472720 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 @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButton, EuiSpacer, EuiPanel, EuiLoadingContent } from '@elastic/eui'; +import { EuiButton, EuiSpacer } from '@elastic/eui'; import React from 'react'; import { StickyContainer } from 'react-sticky'; @@ -17,14 +17,13 @@ import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../cont import { SpyRoute } from '../../utils/route/spy_routes'; import { SignalsTable } from './components/signals'; -import * as signalsI18n from './components/signals/translations'; + import { SignalsCharts } from './components/signals_chart'; import { useSignalInfo } from './components/signals_info'; import { DetectionEngineEmptyPage } from './detection_engine_empty_page'; import { DetectionEngineNoIndex } from './detection_engine_no_signal_index'; import { DetectionEngineUserUnauthenticated } from './detection_engine_user_unauthenticated'; import * as i18n from './translations'; -import { HeaderSection } from '../../components/header_section'; interface DetectionEngineComponentProps { canUserCRUD: boolean; @@ -86,23 +85,15 @@ export const DetectionEngineComponent = React.memo - {({ to, from }) => - !loading ? ( - isSignalIndexExists && ( - - ) - ) : ( - - - - - ) - } + {({ to, from }) => ( + + )} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx index 04a9e1d1ee330..e6c0b383d2ed5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx @@ -4,84 +4,61 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect } from 'react'; +import React from 'react'; import { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom'; -import { useSignalIndex } from '../../containers/detection_engine/signals/use_signal_index'; -import { usePrivilegeUser } from '../../containers/detection_engine/signals/use_privilege_user'; -import { useKibana } from '../../lib/kibana'; import { CreateRuleComponent } from './rules/create'; import { DetectionEngineComponent } from './detection_engine'; import { EditRuleComponent } from './rules/edit'; import { RuleDetailsComponent } from './rules/details'; import { RulesComponent } from './rules'; +import { useUserInfo, ManageUserInfo } from './components/user_info'; const detectionEnginePath = `/:pageName(detection-engine)`; type Props = Partial> & { url: string }; export const DetectionEngineContainer = React.memo(() => { - const [privilegeLoading, isAuthenticated, hasWrite] = usePrivilegeUser(); const [ - indexNameLoading, + loading, isSignalIndexExists, + isAuthenticated, + canUserCRUD, signalIndexName, - createSignalIndex, - ] = useSignalIndex(); - const uiCapabilities = useKibana().services.application?.capabilities; - const canUserCRUD = (uiCapabilities?.siem?.crud as boolean) ?? false; - - useEffect(() => { - if ( - isAuthenticated && - hasWrite && - isSignalIndexExists != null && - !isSignalIndexExists && - createSignalIndex != null - ) { - createSignalIndex(); - } - }, [createSignalIndex, isAuthenticated, isSignalIndexExists, hasWrite]); + ] = useUserInfo(); return ( - - - - - {isSignalIndexExists && isAuthenticated && ( - <> - - - - {canUserCRUD && ( - - - - )} - - - - {canUserCRUD && ( - - - + + + + + + + + + + + + + + + + + + ( + )} - - )} - - ( - - )} - /> - + /> + + ); }); DetectionEngineContainer.displayName = 'DetectionEngineContainer'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx index a98397ecf6f9d..ca605f4ecbe1d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx @@ -160,8 +160,6 @@ export const getColumns = ( truncateText: true, width: '20%', }, - ]; - const actions: RulesColumns[] = [ { align: 'center', field: 'activate', @@ -171,12 +169,15 @@ export const getColumns = ( dispatch={dispatch} id={item.id} enabled={item.activate} + isDisabled={!canUserCRUD} isLoading={item.isLoading} /> ), sortable: true, width: '85px', }, + ]; + const actions: RulesColumns[] = [ { actions: getActions(dispatch, history), width: '40px', 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 dcc494d99d041..653fe567738e1 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 @@ -60,175 +60,179 @@ const initialState: State = { * * Delete * * Import/Export */ -export const AllRules = React.memo<{ canUserCRUD: boolean; importCompleteToggle: boolean }>( - ({ canUserCRUD, importCompleteToggle }) => { - const [ - { - exportPayload, - filterOptions, - isLoading, - refreshToggle, - selectedItems, - tableData, - pagination, +export const AllRules = React.memo<{ + canUserCRUD: boolean; + importCompleteToggle: boolean; + loading: boolean; +}>(({ canUserCRUD, importCompleteToggle, loading }) => { + const [ + { + exportPayload, + filterOptions, + isLoading, + refreshToggle, + selectedItems, + tableData, + pagination, + }, + dispatch, + ] = useReducer(allRulesReducer, initialState); + const history = useHistory(); + const [isInitialLoad, setIsInitialLoad] = useState(true); + const [isLoadingRules, rulesData] = useRules(pagination, filterOptions, refreshToggle); + const [, dispatchToaster] = useStateToaster(); + + const getBatchItemsPopoverContent = useCallback( + (closePopover: () => void) => ( + + ), + [selectedItems, dispatch] + ); + + useEffect(() => { + dispatch({ type: 'loading', isLoading: isLoadingRules }); + + if (!isLoadingRules) { + setIsInitialLoad(false); + } + }, [isLoadingRules]); + + useEffect(() => { + if (!isInitialLoad) { + dispatch({ type: 'refresh' }); + } + }, [importCompleteToggle]); + + useEffect(() => { + dispatch({ + type: 'updateRules', + rules: rulesData.data, + pagination: { + page: rulesData.page, + perPage: rulesData.perPage, + total: rulesData.total, }, - dispatch, - ] = useReducer(allRulesReducer, initialState); - const history = useHistory(); - const [isInitialLoad, setIsInitialLoad] = useState(true); - const [isLoadingRules, rulesData] = useRules(pagination, filterOptions, refreshToggle); - const [, dispatchToaster] = useStateToaster(); - - const getBatchItemsPopoverContent = useCallback( - (closePopover: () => void) => ( - - ), - [selectedItems, dispatch] - ); - - useEffect(() => { - dispatch({ type: 'loading', isLoading: isLoadingRules }); - - if (!isLoadingRules) { - setIsInitialLoad(false); - } - }, [isLoadingRules]); - - useEffect(() => { - if (!isInitialLoad) { - dispatch({ type: 'refresh' }); - } - }, [importCompleteToggle]); - - useEffect(() => { - dispatch({ - type: 'updateRules', - rules: rulesData.data, - pagination: { - page: rulesData.page, - perPage: rulesData.perPage, - total: rulesData.total, - }, - }); - }, [rulesData]); - - const euiBasicTableSelectionProps = useMemo( - () => - canUserCRUD - ? { - selectable: (item: TableData) => !item.isLoading, - onSelectionChange: (selected: TableData[]) => - dispatch({ type: 'setSelected', selectedItems: selected }), - } - : {}, - [canUserCRUD] - ); - - return ( - <> - { - dispatchToaster({ - type: 'addToaster', - toast: { - id: uuid.v4(), - title: i18n.SUCCESSFULLY_EXPORTED_RULES(exportCount), - color: 'success', - iconType: 'check', - }, - }); - }} - /> - - - - {isInitialLoad ? ( - - ) : ( - <> - - { - dispatch({ - type: 'updateFilterOptions', - filterOptions: { - ...filterOptions, - filter: filterString, - }, - }); - }} - /> - - - - - - {i18n.SHOWING_RULES(pagination.total ?? 0)} - - - - {i18n.SELECTED_RULES(selectedItems.length)} - {canUserCRUD && ( - - {i18n.BATCH_ACTIONS} - - )} - dispatch({ type: 'refresh' })} - > - {i18n.REFRESH} - - - - - - { - dispatch({ - type: 'updatePagination', - pagination: { ...pagination, page: page.index + 1, perPage: page.size }, - }); + }); + }, [rulesData]); + + const euiBasicTableSelectionProps = useMemo( + () => + canUserCRUD + ? { + selectable: (item: TableData) => !item.isLoading, + onSelectionChange: (selected: TableData[]) => + dispatch({ type: 'setSelected', selectedItems: selected }), + } + : {}, + [canUserCRUD] + ); + + return ( + <> + { + dispatchToaster({ + type: 'addToaster', + toast: { + id: uuid.v4(), + title: i18n.SUCCESSFULLY_EXPORTED_RULES(exportCount), + color: 'success', + iconType: 'check', + }, + }); + }} + /> + + + + {isInitialLoad ? ( + + ) : ( + <> + + { dispatch({ type: 'updateFilterOptions', filterOptions: { ...filterOptions, - sortField: 'enabled', // Only enabled is supported for sorting currently - sortOrder: sort!.direction, + filter: filterString, }, }); }} - pagination={{ - pageIndex: pagination.page - 1, - pageSize: pagination.perPage, - totalItemCount: pagination.total, - pageSizeOptions: [5, 10, 20], - }} - sorting={{ sort: { field: 'activate', direction: filterOptions.sortOrder } }} - {...euiBasicTableSelectionProps} /> - {isLoading && } - - )} - - - ); - } -); + + + + + + {i18n.SHOWING_RULES(pagination.total ?? 0)} + + + + {i18n.SELECTED_RULES(selectedItems.length)} + {canUserCRUD && ( + + {i18n.BATCH_ACTIONS} + + )} + dispatch({ type: 'refresh' })} + > + {i18n.REFRESH} + + + + + + { + 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, + }, + }); + }} + pagination={{ + pageIndex: pagination.page - 1, + pageSize: pagination.perPage, + totalItemCount: pagination.total, + pageSizeOptions: [5, 10, 20], + }} + sorting={{ sort: { field: 'activate', direction: filterOptions.sortOrder } }} + {...euiBasicTableSelectionProps} + /> + {(isLoading || loading) && ( + + )} + + )} + + + ); +}); AllRules.displayName = 'AllRules'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.tsx new file mode 100644 index 0000000000000..94ddd0257297e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiCallOut, EuiButton } from '@elastic/eui'; +import React, { memo, useCallback, useState } from 'react'; + +import * as i18n from './translations'; + +const ReadOnlyCallOutComponent = () => { + const [showCAllout, setShowCallOut] = useState(true); + const handleCallOut = useCallback(() => setShowCallOut(false), [setShowCallOut]); + + return showCAllout ? ( + +

{i18n.READ_ONLY_CALLOUT_MSG}

+ + {i18n.DISMISS_CALLOUT} + +
+ ) : null; +}; + +export const ReadOnlyCallOut = memo(ReadOnlyCallOutComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/translations.ts new file mode 100644 index 0000000000000..006982b6ce73d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/translations.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const READ_ONLY_CALLOUT_TITLE = i18n.translate( + 'xpack.siem.detectionEngine.readOnlyCallOutTitle', + { + defaultMessage: 'Edit rule permissions required', + } +); + +export const READ_ONLY_CALLOUT_MSG = i18n.translate( + 'xpack.siem.detectionEngine.readOnlyCallOutMsg', + { + defaultMessage: + 'You are currently missing the required permissions to create/edit detection engine rule. Please contact your administrator for further assistance.', + } +); + +export const DISMISS_CALLOUT = i18n.translate('xpack.siem.detectionEngine.dismissButton', { + defaultMessage: 'Dismiss', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx index a1fa4770a41ac..09be3df7d6929 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx @@ -32,6 +32,7 @@ export interface RuleSwitchProps { dispatch?: React.Dispatch; id: string; enabled: boolean; + isDisabled?: boolean; isLoading?: boolean; optionLabel?: string; } @@ -42,6 +43,7 @@ export interface RuleSwitchProps { export const RuleSwitchComponent = ({ dispatch, id, + isDisabled, isLoading, enabled, optionLabel, @@ -92,7 +94,7 @@ export const RuleSwitchComponent = ({ data-test-subj="rule-switch" label={optionLabel ?? ''} showLabel={!isEmpty(optionLabel)} - disabled={false} + disabled={isDisabled} checked={myEnabled} onChange={onRuleStateChange} /> diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx index 848b17aadbff4..bc07115386db4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx @@ -14,6 +14,7 @@ import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redir import { WrapperPage } from '../../../../components/wrapper_page'; import { usePersistRule } from '../../../../containers/detection_engine/rules'; import { SpyRoute } from '../../../../utils/route/spy_routes'; +import { useUserInfo } from '../../components/user_info'; import { AccordionTitle } from '../components/accordion_title'; import { FormData, FormHook } from '../components/shared_imports'; import { StepAboutRule } from '../components/step_about_rule'; @@ -56,6 +57,7 @@ const MyEuiPanel = styled(EuiPanel)` `; export const CreateRuleComponent = React.memo(() => { + const [loading, isSignalIndexExists, isAuthenticated, canUserCRUD] = useUserInfo(); const [heightAccordion, setHeightAccordion] = useState(-1); const [openAccordionId, setOpenAccordionId] = useState(RuleStep.defineRule); const defineRuleRef = useRef(null); @@ -78,6 +80,16 @@ export const CreateRuleComponent = React.memo(() => { }); const [{ isLoading, isSaved }, setRule] = usePersistRule(); + if ( + isSignalIndexExists != null && + isAuthenticated != null && + (!isSignalIndexExists || !isAuthenticated) + ) { + return ; + } else if (!canUserCRUD) { + return ; + } + const setStepData = useCallback( (step: RuleStep, data: unknown, isValid: boolean) => { stepsData.current[step] = { ...stepsData.current[step], data, isValid }; @@ -216,7 +228,7 @@ export const CreateRuleComponent = React.memo(() => { @@ -242,7 +254,7 @@ export const CreateRuleComponent = React.memo(() => { setHeightAccordion(height)} @@ -273,7 +285,7 @@ export const CreateRuleComponent = React.memo(() => { @@ -303,7 +315,7 @@ export const CreateRuleComponent = React.memo(() => { 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 3a7fd16c8c8bb..fbdfd608a7bc8 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 @@ -7,7 +7,7 @@ import { EuiButton, EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { memo, useMemo } from 'react'; -import { useParams } from 'react-router-dom'; +import { useParams, Redirect } from 'react-router-dom'; import { StickyContainer } from 'react-sticky'; import { FiltersGlobal } from '../../../../components/filters_global'; @@ -23,9 +23,10 @@ import { WithSource, } from '../../../../containers/source'; import { SpyRoute } from '../../../../utils/route/spy_routes'; - +import { GlobalTime } from '../../../../containers/global_time'; import { SignalsCharts } from '../../components/signals_chart'; import { SignalsTable } from '../../components/signals'; +import { useUserInfo } from '../../components/user_info'; import { DetectionEngineEmptyPage } from '../../detection_engine_empty_page'; import { useSignalInfo } from '../../components/signals_info'; import { StepAboutRule } from '../components/step_about_rule'; @@ -38,201 +39,210 @@ import { StepPanel } from '../components/step_panel'; import { getStepsData } from '../helpers'; import * as ruleI18n from '../translations'; import * as i18n from './translations'; -import { GlobalTime } from '../../../../containers/global_time'; +import { ReadOnlyCallOut } from '../components/read_only_callout'; + +export const RuleDetailsComponent = memo(() => { + const [ + loading, + isSignalIndexExists, + isAuthenticated, + canUserCRUD, + signalIndexName, + ] = useUserInfo(); + const { ruleId } = useParams(); + const [isLoading, rule] = useRule(ruleId); + const { aboutRuleData, defineRuleData, scheduleRuleData } = getStepsData({ + rule, + detailsView: true, + }); + const [lastSignals] = useSignalInfo({ ruleId }); + + if ( + isSignalIndexExists != null && + isAuthenticated != null && + (!isSignalIndexExists || !isAuthenticated) + ) { + return ; + } -interface RuleDetailsComponentProps { - canUserCRUD: boolean; - signalsIndex: string | null; -} - -export const RuleDetailsComponent = memo( - ({ canUserCRUD, signalsIndex }) => { - const { ruleId } = useParams(); - const [loading, rule] = useRule(ruleId); - const { aboutRuleData, defineRuleData, scheduleRuleData } = getStepsData({ - rule, - detailsView: true, - }); - const [lastSignals] = useSignalInfo({ ruleId }); - - const title = loading === true || rule === null ? : rule.name; - const subTitle = useMemo( - () => - loading === true || rule === null ? ( - - ) : ( - [ + const title = loading === true || rule === null ? : rule.name; + const subTitle = useMemo( + () => + loading || isLoading || rule === null ? ( + + ) : ( + [ + + ), + }} + />, + rule?.updated_by != null ? ( ), }} - />, - rule?.updated_by != null ? ( - - ), - }} - /> - ) : ( - '' - ), - ] - ), - [loading, rule] - ); - - const signalDefaultFilters = useMemo( - () => (ruleId != null ? buildSignalsRuleIdFilter(ruleId) : []), - [ruleId] - ); - return ( - <> - - {({ indicesExist, indexPattern }) => { - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - {({ to, from }) => ( - - - - - - - - {detectionI18n.LAST_SIGNAL} - {': '} - {lastSignals} - - ) : null, - 'Status: Comming Soon', - ]} - title={title} - > - {canUserCRUD && ( - - - - + /> + ) : ( + '' + ), + ] + ), + [loading, rule] + ); + + const signalDefaultFilters = useMemo( + () => (ruleId != null ? buildSignalsRuleIdFilter(ruleId) : []), + [ruleId] + ); + return ( + <> + {!canUserCRUD && } + + {({ indicesExist, indexPattern }) => { + return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + + {({ to, from }) => ( + + + + + + + + {detectionI18n.LAST_SIGNAL} + {': '} + {lastSignals} + + ) : null, + 'Status: Comming Soon', + ]} + title={title} + > + + + + + + - - - - {ruleI18n.EDIT_RULE_SETTINGS} - - - + + {ruleI18n.EDIT_RULE_SETTINGS} + - )} - - - - - - - - {defineRuleData != null && ( - - )} - - - - - - {aboutRuleData != null && ( - - )} - - - - - - {scheduleRuleData != null && ( - - )} - - - - - - - - - {ruleId != null && ( - - )} - - - )} - - ) : ( - - - - - ); - }} - - - - - ); - } -); + + + + + + + + {defineRuleData != null && ( + + )} + + + + + + {aboutRuleData != null && ( + + )} + + + + + + {scheduleRuleData != null && ( + + )} + + + + + + + + + + + {ruleId != null && ( + + )} + + + )} + + ) : ( + + + + + ); + }} + + + + + ); +}); RuleDetailsComponent.displayName = 'RuleDetailsComponent'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx index 10b7f0e832f19..20beee28950df 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx @@ -22,6 +22,7 @@ import { WrapperPage } from '../../../../components/wrapper_page'; import { SpyRoute } from '../../../../utils/route/spy_routes'; import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine'; import { useRule, usePersistRule } from '../../../../containers/detection_engine/rules'; +import { useUserInfo } from '../../components/user_info'; import { FormHook, FormData } from '../components/shared_imports'; import { StepPanel } from '../components/step_panel'; import { StepAboutRule } from '../components/step_about_rule'; @@ -47,8 +48,18 @@ interface ScheduleStepRuleForm extends StepRuleForm { } export const EditRuleComponent = memo(() => { + const [initLoading, isSignalIndexExists, isAuthenticated, canUserCRUD] = useUserInfo(); const { ruleId } = useParams(); const [loading, rule] = useRule(ruleId); + if ( + isSignalIndexExists != null && + isAuthenticated != null && + (!isSignalIndexExists || !isAuthenticated) + ) { + return ; + } else if (!canUserCRUD) { + return ; + } const [initForm, setInitForm] = useState(false); const [myAboutRuleForm, setMyAboutRuleForm] = useState({ @@ -89,7 +100,7 @@ export const EditRuleComponent = memo(() => { content: ( <> - + {myDefineRuleForm.data != null && ( { content: ( <> - + {myAboutRuleForm.data != null && ( { content: ( <> - + {myScheduleRuleForm.data != null && ( { ], [ loading, + initLoading, isLoading, myAboutRuleForm, myDefineRuleForm, @@ -310,7 +322,13 @@ export const EditRuleComponent = memo(() => { - + {i18n.SAVE_CHANGES} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx index 47b5c1051bcfc..cc0882dd7e426 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx @@ -5,6 +5,7 @@ */ import { pick } from 'lodash/fp'; +import { useLocation } from 'react-router-dom'; import { esFilters } from '../../../../../../../../src/plugins/data/public'; import { Rule } from '../../../containers/detection_engine/rules'; @@ -64,3 +65,5 @@ export const getStepsData = ({ return { aboutRuleData, defineRuleData, scheduleRuleData }; }; + +export const useQuery = () => new URLSearchParams(useLocation().search); 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 ace73001ae0ed..d67c93f86c1a9 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,8 +5,9 @@ */ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useState } from 'react'; +import { Redirect } from 'react-router-dom'; import { DETECTION_ENGINE_PAGE_NAME } from '../../../components/link_to/redirect_to_detection_engine'; import { FormattedRelativePreferenceDate } from '../../../components/formatted_date'; @@ -17,18 +18,27 @@ import { SpyRoute } from '../../../utils/route/spy_routes'; import { AllRules } from './all'; import { ImportRuleModal } from './components/import_rule_modal'; +import { ReadOnlyCallOut } from './components/read_only_callout'; +import { useUserInfo } from '../components/user_info'; import * as i18n from './translations'; -interface RulesComponentProps { - canUserCRUD: boolean; -} -export const RulesComponent = React.memo(({ canUserCRUD }) => { +export const RulesComponent = React.memo(() => { const [showImportModal, setShowImportModal] = useState(false); const [importCompleteToggle, setImportCompleteToggle] = useState(false); + const [loading, isSignalIndexExists, isAuthenticated, canUserCRUD] = useUserInfo(); + + if ( + isSignalIndexExists != null && + isAuthenticated != null && + (!isSignalIndexExists || !isAuthenticated) + ) { + return ; + } const lastCompletedRun = undefined; return ( <> + {!canUserCRUD && } setShowImportModal(false)} @@ -55,31 +65,35 @@ export const RulesComponent = React.memo(({ canUserCRUD }) } title={i18n.PAGE_TITLE} > - {canUserCRUD && ( - - - { - setShowImportModal(true); - }} - > - {i18n.IMPORT_RULE} - - - - - {i18n.ADD_NEW_RULE} - - - - )} + + + { + setShowImportModal(true); + }} + > + {i18n.IMPORT_RULE} + + + + + {i18n.ADD_NEW_RULE} + + + - + From 550a3197e567902fbe767c86b56735ab952c5b2d Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Thu, 9 Jan 2020 16:21:59 -0500 Subject: [PATCH 05/14] bug --- .../pages/detection_engine/components/user_info/index.tsx | 4 ++-- .../siem/public/pages/detection_engine/rules/create/index.tsx | 2 +- .../siem/public/pages/detection_engine/rules/edit/index.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) 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 f8857c540d4a5..25d9c7ca3b5a1 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 @@ -11,7 +11,7 @@ import { usePrivilegeUser } from '../../../../containers/detection_engine/signal import { useSignalIndex } from '../../../../containers/detection_engine/signals/use_signal_index'; import { useKibana } from '../../../../lib/kibana'; -type Return = [boolean, boolean | null, boolean | null, boolean, string | null]; +type Return = [boolean, boolean | null, boolean | null, boolean | null, string | null]; export interface State { canUserCRUD: boolean | null; @@ -179,7 +179,7 @@ export const useUserInfo = (): Return => { indexNameLoading || privilegeLoading, isSignalIndexExists, isAuthenticated, - canUserCRUD ?? false, + canUserCRUD, signalIndexName, ]; }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx index bc07115386db4..1a9f781e2a681 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx @@ -86,7 +86,7 @@ export const CreateRuleComponent = React.memo(() => { (!isSignalIndexExists || !isAuthenticated) ) { return ; - } else if (!canUserCRUD) { + } else if (canUserCRUD != null && !canUserCRUD) { return ; } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx index 20beee28950df..86a59bcb6ac28 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx @@ -57,7 +57,7 @@ export const EditRuleComponent = memo(() => { (!isSignalIndexExists || !isAuthenticated) ) { return ; - } else if (!canUserCRUD) { + } else if (canUserCRUD != null && !canUserCRUD) { return ; } From 605d26479b50bca3bdf194512e99b4d7238658f1 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Fri, 10 Jan 2020 10:12:42 -0500 Subject: [PATCH 06/14] cleanup --- .../legacy/plugins/siem/public/pages/detection_engine/index.tsx | 2 +- .../siem/public/pages/detection_engine/rules/all/index.tsx | 2 +- .../components/rule_switch/__snapshots__/index.test.tsx.snap | 1 - .../siem/public/pages/detection_engine/rules/details/index.tsx | 2 +- .../plugins/siem/public/pages/detection_engine/rules/index.tsx | 2 +- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx index e6c0b383d2ed5..ba9704ce7c5c9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx @@ -32,7 +32,7 @@ export const DetectionEngineContainer = React.memo(() => { { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap index f264dde07c594..604f86866d565 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap @@ -11,7 +11,6 @@ exports[`RuleSwitch renders correctly against snapshot 1`] = ` { {ruleId != null && ( { From eefd3fddd474739c7b2ef62b316eb2edae2b4a28 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Fri, 10 Jan 2020 11:53:45 -0500 Subject: [PATCH 07/14] review I --- .../pages/detection_engine/components/user_info/index.tsx | 7 ++++--- .../rules/components/read_only_callout/index.tsx | 4 ++-- .../rules/components/step_about_rule/schema.tsx | 2 +- .../public/pages/detection_engine/rules/details/index.tsx | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) 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 25d9c7ca3b5a1..570e9ba0ecc87 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 @@ -124,8 +124,9 @@ export const useUserInfo = (): Return => { createSignalIndex, ] = useSignalIndex(); - const uiCapabilities = useKibana().services.application?.capabilities; - const capabilitiesCanUserCRUD = (uiCapabilities?.siem?.crud as boolean) ?? false; + const uiCapabilities = useKibana().services.application.capabilities; + const capabilitiesCanUserCRUD: boolean = + typeof uiCapabilities.siem.crud === 'boolean' ? uiCapabilities.siem.crud : false; useEffect(() => { if (loading !== privilegeLoading || indexNameLoading) { @@ -166,7 +167,7 @@ export const useUserInfo = (): Return => { useEffect(() => { if ( isAuthenticated && - hasApiWrite && + hasWrite && isSignalIndexExists != null && !isSignalIndexExists && createSignalIndex != null diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.tsx index 94ddd0257297e..6ec76bacc2323 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.tsx @@ -10,10 +10,10 @@ import React, { memo, useCallback, useState } from 'react'; import * as i18n from './translations'; const ReadOnlyCallOutComponent = () => { - const [showCAllout, setShowCallOut] = useState(true); + const [showCallOut, setShowCallOut] = useState(true); const handleCallOut = useCallback(() => setShowCallOut(false), [setShowCallOut]); - return showCAllout ? ( + return showCallOut ? (

{i18n.READ_ONLY_CALLOUT_MSG}

diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx index 9355f1c8bfefa..78000fbd82af7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx @@ -145,7 +145,7 @@ export const schema: FormSchema = { label: i18n.translate( 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldMitreThreatLabel', { - defaultMessage: 'MITRE ATT&CK', + defaultMessage: 'MITRE ATT&CK ™', } ), labelAppend: {RuleI18n.OPTIONAL_FIELD}, 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 0532839264ff3..b60b11001e671 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 @@ -158,7 +158,7 @@ export const RuleDetailsComponent = memo(() => { {ruleI18n.EDIT_RULE_SETTINGS} From be4d2a154a16cb8950f137580485a3eab655c2be Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Fri, 10 Jan 2020 12:55:39 -0500 Subject: [PATCH 08/14] review II --- .../index/get_index_exists.test.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.test.ts new file mode 100644 index 0000000000000..014c319180552 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.test.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getIndexExists } from './get_index_exists'; + +class StatusCode extends Error { + statusCode: number = -1; + constructor(statusCode: number, message: string) { + super(message); + this.statusCode = statusCode; + } +} + +describe('get_index_exists', () => { + test('it should return a true if no errors', async () => { + const callWithRequest = jest.fn().mockResolvedValue(''); + const indexExists = await getIndexExists(callWithRequest, 'some-index'); + expect(indexExists).toEqual(true); + }); + + test('it should return a false if it encounters a 404', async () => { + const callWithRequest = jest.fn().mockImplementation(() => { + throw new StatusCode(404, 'I am a 404 error'); + }); + const indexExists = await getIndexExists(callWithRequest, 'some-index'); + expect(indexExists).toEqual(false); + }); + + test('it should reject if it encounters a non 404', async () => { + const callWithRequest = jest.fn().mockImplementation(() => { + throw new StatusCode(500, 'I am a 500 error'); + }); + await expect(getIndexExists(callWithRequest, 'some-index')).rejects.toThrow('I am a 500 error'); + }); +}); From cd5a6fd6aca3fe1ee74737f1e3564ede0bf916eb Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Fri, 10 Jan 2020 13:55:51 -0500 Subject: [PATCH 09/14] a pretty shameful bug I will live thanks Frank --- .../detection_engine/detection_engine.tsx | 34 ++++----- .../public/pages/detection_engine/index.tsx | 72 ++++++++----------- .../index/get_index_exists.test.ts | 10 ++- .../index/get_index_exists.ts | 6 +- .../routes/rules/create_rules_route.test.ts | 4 +- 5 files changed, 55 insertions(+), 71 deletions(-) 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 20afd33735e39..253b9634acb18 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 @@ -33,14 +33,7 @@ import { DetectionEngineNoIndex } from './detection_engine_no_signal_index'; import { DetectionEngineUserUnauthenticated } from './detection_engine_user_unauthenticated'; import * as i18n from './translations'; import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; - -interface OwnProps { - canUserCRUD: boolean; - loading: boolean; - isSignalIndexExists: boolean | null; - isUserAuthenticated: boolean | null; - signalsIndex: string | null; -} +import { useUserInfo } from './components/user_info'; interface ReduxProps { filters: esFilters.Filter[]; @@ -55,19 +48,18 @@ export interface DispatchProps { }>; } -type DetectionEngineComponentProps = OwnProps & ReduxProps & DispatchProps; +type DetectionEngineComponentProps = ReduxProps & DispatchProps; const DetectionEngineComponent = React.memo( - ({ - canUserCRUD, - filters, - loading, - isSignalIndexExists, - isUserAuthenticated, - query, - setAbsoluteRangeDatePicker, - signalsIndex, - }) => { + ({ filters, query, setAbsoluteRangeDatePicker }) => { + const [ + loading, + isSignalIndexExists, + isUserAuthenticated, + canUserCRUD, + signalIndexName, + ] = useUserInfo(); + const [lastSignals] = useSignalInfo({}); const updateDateRangeCallback = useCallback( @@ -138,9 +130,9 @@ const DetectionEngineComponent = React.memo( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx index 4edfc6dba867e..c4e83429aebdb 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx @@ -12,53 +12,37 @@ import { DetectionEngine } from './detection_engine'; import { EditRuleComponent } from './rules/edit'; import { RuleDetails } from './rules/details'; import { RulesComponent } from './rules'; -import { useUserInfo, ManageUserInfo } from './components/user_info'; +import { ManageUserInfo } from './components/user_info'; const detectionEnginePath = `/:pageName(detection-engine)`; type Props = Partial> & { url: string }; -export const DetectionEngineContainer = React.memo(() => { - const [ - loading, - isSignalIndexExists, - isAuthenticated, - canUserCRUD, - signalIndexName, - ] = useUserInfo(); - - return ( - - - - - - - - - - - - - - - - - - ( - - )} - /> - - - ); -}); +export const DetectionEngineContainer = React.memo(() => ( + + + + + + + + + + + + + + + + + + ( + + )} + /> + + +)); DetectionEngineContainer.displayName = 'DetectionEngineContainer'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.test.ts index 014c319180552..34f07bc8a916c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.test.ts @@ -15,12 +15,18 @@ class StatusCode extends Error { } describe('get_index_exists', () => { - test('it should return a true if no errors', async () => { - const callWithRequest = jest.fn().mockResolvedValue(''); + test('it should return a true if you have _shards', async () => { + const callWithRequest = jest.fn().mockResolvedValue({ _shards: { total: 1 } }); const indexExists = await getIndexExists(callWithRequest, 'some-index'); expect(indexExists).toEqual(true); }); + test('it should return a false if you do NOT have _shards', async () => { + const callWithRequest = jest.fn().mockResolvedValue({ _shards: { total: 0 } }); + const indexExists = await getIndexExists(callWithRequest, 'some-index'); + expect(indexExists).toEqual(false); + }); + test('it should return a false if it encounters a 404', async () => { const callWithRequest = jest.fn().mockImplementation(() => { throw new StatusCode(404, 'I am a 404 error'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts index 4ccf8691234e8..ed5d87531d9aa 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts @@ -10,18 +10,18 @@ export const getIndexExists = async ( callWithRequest: CallWithRequest< { index: string; size: number; terminate_after: number; allow_no_indices: boolean }, {}, - unknown + { _shards: { total: number } } >, index: string ): Promise => { try { - callWithRequest('search', { + const response = await callWithRequest('search', { index, size: 0, terminate_after: 1, allow_no_indices: true, }); - return true; + return response._shards.total > 0; } catch (err) { if (err.statusCode === 404) { return false; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index 094449a5f61ac..10dc14f7ed610 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -28,7 +28,9 @@ describe('create_rules', () => { jest.resetAllMocks(); ({ server, alertsClient, actionsClient, elasticsearch } = createMockServer()); elasticsearch.getCluster = jest.fn().mockImplementation(() => ({ - callWithRequest: jest.fn().mockImplementation(() => true), + callWithRequest: jest + .fn() + .mockImplementation((endpoint, params) => ({ _shards: { total: 1 } })), })); createRulesRoute(server); From ade1ea5397a5007a56e370caecbbfcba650879ff Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Fri, 10 Jan 2020 15:35:59 -0500 Subject: [PATCH 10/14] bug select rule --- .../pages/detection_engine/rules/all/index.tsx | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) 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 8c252f872a213..de11fae5fc9f9 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 @@ -116,15 +116,12 @@ export const AllRules = React.memo<{ }, [rulesData]); const euiBasicTableSelectionProps = useMemo( - () => - canUserCRUD - ? { - selectable: (item: TableData) => !item.isLoading, - onSelectionChange: (selected: TableData[]) => - dispatch({ type: 'setSelected', selectedItems: selected }), - } - : {}, - [canUserCRUD] + () => ({ + selectable: (item: TableData) => !item.isLoading, + onSelectionChange: (selected: TableData[]) => + dispatch({ type: 'setSelected', selectedItems: selected }), + }), + [] ); return ( @@ -223,7 +220,7 @@ export const AllRules = React.memo<{ pageSizeOptions: [5, 10, 20], }} sorting={{ sort: { field: 'activate', direction: filterOptions.sortOrder } }} - {...euiBasicTableSelectionProps} + selection={canUserCRUD ? euiBasicTableSelectionProps : undefined} /> {(isLoading || loading) && ( From 4734cf51c17a520174d019de4f0db5b517a5c5cf Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Fri, 10 Jan 2020 16:53:41 -0500 Subject: [PATCH 11/14] only activate deactivate if user has the manage permission --- .../detection_engine/components/user_info/index.tsx | 10 +++++++++- .../pages/detection_engine/rules/all/columns.tsx | 5 +++-- .../public/pages/detection_engine/rules/all/index.tsx | 5 +++-- .../pages/detection_engine/rules/details/index.tsx | 3 ++- .../siem/public/pages/detection_engine/rules/index.tsx | 9 ++++++++- 5 files changed, 25 insertions(+), 7 deletions(-) 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 570e9ba0ecc87..ae9bb307d4b34 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 @@ -11,7 +11,14 @@ import { usePrivilegeUser } from '../../../../containers/detection_engine/signal import { useSignalIndex } from '../../../../containers/detection_engine/signals/use_signal_index'; import { useKibana } from '../../../../lib/kibana'; -type Return = [boolean, boolean | null, boolean | null, boolean | null, string | null]; +type Return = [ + boolean, + boolean | null, + boolean | null, + boolean | null, + string | null, + boolean | null +]; export interface State { canUserCRUD: boolean | null; @@ -182,5 +189,6 @@ export const useUserInfo = (): Return => { isAuthenticated, canUserCRUD, signalIndexName, + hasWrite, ]; }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx index ca605f4ecbe1d..b35a0f7daab1f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx @@ -74,7 +74,8 @@ type RulesColumns = EuiBasicTableColumn | EuiTableActionsColumnType, history: H.History, - canUserCRUD: boolean + canUserCRUD: boolean, + hasWriteToChangeActivation: boolean ): RulesColumns[] => { const cols: RulesColumns[] = [ { @@ -169,7 +170,7 @@ export const getColumns = ( dispatch={dispatch} id={item.id} enabled={item.activate} - isDisabled={!canUserCRUD} + isDisabled={!canUserCRUD || !hasWriteToChangeActivation} isLoading={item.isLoading} /> ), 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 de11fae5fc9f9..f04f8f010e180 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 @@ -62,9 +62,10 @@ const initialState: State = { */ export const AllRules = React.memo<{ canUserCRUD: boolean; + hasWriteToChangeActivation: boolean; importCompleteToggle: boolean; loading: boolean; -}>(({ canUserCRUD, importCompleteToggle, loading }) => { +}>(({ canUserCRUD, hasWriteToChangeActivation, importCompleteToggle, loading }) => { const [ { exportPayload, @@ -195,7 +196,7 @@ export const AllRules = React.memo<{ ( isAuthenticated, canUserCRUD, signalIndexName, + hasWriteToChangeActivation, ] = useUserInfo(); const { ruleId } = useParams(); const [isLoading, rule] = useRule(ruleId); @@ -187,7 +188,7 @@ const RuleDetailsComponent = memo( 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 b7e991adbc367..dee259f9d15b9 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 @@ -25,7 +25,13 @@ import * as i18n from './translations'; export const RulesComponent = React.memo(() => { const [showImportModal, setShowImportModal] = useState(false); const [importCompleteToggle, setImportCompleteToggle] = useState(false); - const [loading, isSignalIndexExists, isAuthenticated, canUserCRUD] = useUserInfo(); + const [ + loading, + isSignalIndexExists, + isAuthenticated, + canUserCRUD, + hasWriteToChangeActivation, + ] = useUserInfo(); if ( isSignalIndexExists != null && @@ -91,6 +97,7 @@ export const RulesComponent = React.memo(() => { From 08b5a44940bf449aa5b0d960998ff9424c08c208 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Fri, 10 Jan 2020 18:26:44 -0500 Subject: [PATCH 12/14] add permissions rule with manage api key --- .../signals/use_privilege_user.tsx | 16 +- .../components/user_info/index.tsx | 105 +++++++----- .../detection_engine/detection_engine.tsx | 16 +- .../detection_engine/rules/all/columns.tsx | 39 +++-- .../detection_engine/rules/all/index.tsx | 149 +++++++++--------- .../read_only_callout/translations.ts | 2 +- .../detection_engine/rules/create/index.tsx | 14 +- .../detection_engine/rules/details/index.tsx | 76 ++++----- .../detection_engine/rules/edit/index.tsx | 13 +- .../pages/detection_engine/rules/index.tsx | 22 +-- .../index/get_index_exists.ts | 2 +- 11 files changed, 251 insertions(+), 203 deletions(-) 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 c229a635a44a0..496481043b66c 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 @@ -11,7 +11,7 @@ import { useStateToaster } from '../../../components/toasters'; import { getUserPrivilege } from './api'; import * as i18n from './translations'; -type Return = [boolean, boolean | null, boolean | null]; +type Return = [boolean, boolean | null, boolean | null, boolean | null]; /** * Hook to get user privilege from @@ -20,7 +20,8 @@ type Return = [boolean, boolean | null, boolean | null]; export const usePrivilegeUser = (): Return => { const [loading, setLoading] = useState(true); const [isAuthenticated, setAuthenticated] = useState(null); - const [hasWrite, setHasWrite] = useState(null); + const [hasIndexWrite, setHasIndexWrite] = useState(null); + const [hasManageApiKey, setHasManageApiKey] = useState(null); const [, dispatchToaster] = useStateToaster(); useEffect(() => { @@ -38,13 +39,18 @@ export const usePrivilegeUser = (): Return => { setAuthenticated(privilege.isAuthenticated); if (privilege.index != null && Object.keys(privilege.index).length > 0) { const indexName = Object.keys(privilege.index)[0]; - setHasWrite(privilege.index[indexName].manage); + setHasIndexWrite(privilege.index[indexName].manage); + setHasManageApiKey( + privilege.cluster.manage_security || + privilege.cluster.manage_api_key || + privilege.cluster.manage_own_api_key + ); } } } catch (error) { if (isSubscribed) { setAuthenticated(false); - setHasWrite(false); + setHasIndexWrite(false); errorToToaster({ title: i18n.PRIVILEGE_FETCH_FAILURE, error, dispatchToaster }); } } @@ -60,5 +66,5 @@ export const usePrivilegeUser = (): Return => { }; }, []); - return [loading, isAuthenticated, hasWrite]; + return [loading, isAuthenticated, hasIndexWrite, hasManageApiKey]; }; 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 ae9bb307d4b34..a7b9ba4dec4d7 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 @@ -11,27 +11,20 @@ import { usePrivilegeUser } from '../../../../containers/detection_engine/signal import { useSignalIndex } from '../../../../containers/detection_engine/signals/use_signal_index'; import { useKibana } from '../../../../lib/kibana'; -type Return = [ - boolean, - boolean | null, - boolean | null, - boolean | null, - string | null, - boolean | null -]; - export interface State { canUserCRUD: boolean | null; - hasWrite: boolean | null; + hasIndexWrite: boolean | null; + hasManageApiKey: boolean | null; isSignalIndexExists: boolean | null; isAuthenticated: boolean | null; - loading: boolean | null; + loading: boolean; signalIndexName: string | null; } const initialState: State = { canUserCRUD: null, - hasWrite: null, + hasIndexWrite: null, + hasManageApiKey: null, isSignalIndexExists: null, isAuthenticated: null, loading: true, @@ -41,25 +34,29 @@ const initialState: State = { export type Action = | { type: 'updateLoading'; loading: boolean } | { - type: 'updateHasWrite'; - hasWrite: boolean | null; - } + type: 'updateHasManageApiKey'; + hasManageApiKey: boolean | null; + } | { - type: 'updateIsSignalIndexExists'; - isSignalIndexExists: boolean | null; - } + type: 'updateHasIndexWrite'; + hasIndexWrite: boolean | null; + } | { - type: 'updateIsAuthenticated'; - isAuthenticated: boolean | null; - } + type: 'updateIsSignalIndexExists'; + isSignalIndexExists: boolean | null; + } | { - type: 'updateCanUserCRUD'; - canUserCRUD: boolean | null; - } + type: 'updateIsAuthenticated'; + isAuthenticated: boolean | null; + } | { - type: 'updateSignalIndexName'; - signalIndexName: string | null; - }; + type: 'updateCanUserCRUD'; + canUserCRUD: boolean | null; + } + | { + type: 'updateSignalIndexName'; + signalIndexName: string | null; + }; export const userInfoReducer = (state: State, action: Action): State => { switch (action.type) { @@ -69,10 +66,16 @@ export const userInfoReducer = (state: State, action: Action): State => { loading: action.loading, }; } - case 'updateHasWrite': { + case 'updateHasIndexWrite': { + return { + ...state, + hasIndexWrite: action.hasIndexWrite, + }; + } + case 'updateHasManageApiKey': { return { ...state, - hasWrite: action.hasWrite, + hasManageApiKey: action.hasManageApiKey, }; } case 'updateIsSignalIndexExists': { @@ -118,12 +121,25 @@ export const ManageUserInfo = ({ children }: ManageUserInfoProps) => ( ); -export const useUserInfo = (): Return => { +export const useUserInfo = (): State => { const [ - { canUserCRUD, hasWrite, isSignalIndexExists, isAuthenticated, loading, signalIndexName }, + { + canUserCRUD, + hasIndexWrite, + hasManageApiKey, + isSignalIndexExists, + isAuthenticated, + loading, + signalIndexName, + }, dispatch, ] = useUserData(); - const [privilegeLoading, isApiAuthenticated, hasApiWrite] = usePrivilegeUser(); + const [ + privilegeLoading, + isApiAuthenticated, + hasApiIndexWrite, + hasApiManageApiKey, + ] = usePrivilegeUser(); const [ indexNameLoading, isApiSignalIndexExists, @@ -142,10 +158,16 @@ export const useUserInfo = (): Return => { }, [loading, privilegeLoading, indexNameLoading]); useEffect(() => { - if (hasWrite !== hasApiWrite && hasApiWrite != null) { - dispatch({ type: 'updateHasWrite', hasWrite: hasApiWrite }); + if (hasIndexWrite !== hasApiIndexWrite && hasApiIndexWrite != null) { + dispatch({ type: 'updateHasIndexWrite', hasIndexWrite: hasApiIndexWrite }); + } + }, [hasIndexWrite, hasApiIndexWrite]); + + useEffect(() => { + if (hasManageApiKey !== hasApiManageApiKey && hasApiManageApiKey != null) { + dispatch({ type: 'updateHasManageApiKey', hasManageApiKey: hasApiManageApiKey }); } - }, [hasWrite, hasApiWrite]); + }, [hasManageApiKey, hasApiManageApiKey]); useEffect(() => { if (isSignalIndexExists !== isApiSignalIndexExists && isApiSignalIndexExists != null) { @@ -174,21 +196,22 @@ export const useUserInfo = (): Return => { useEffect(() => { if ( isAuthenticated && - hasWrite && + hasIndexWrite && isSignalIndexExists != null && !isSignalIndexExists && createSignalIndex != null ) { createSignalIndex(); } - }, [createSignalIndex, isAuthenticated, isSignalIndexExists, hasWrite]); + }, [createSignalIndex, isAuthenticated, isSignalIndexExists, hasIndexWrite]); - return [ - indexNameLoading || privilegeLoading, + return { + loading, isSignalIndexExists, isAuthenticated, canUserCRUD, + hasIndexWrite, + hasManageApiKey, signalIndexName, - hasWrite, - ]; + }; }; 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 253b9634acb18..9ffbbb3de2068 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 @@ -52,13 +52,13 @@ type DetectionEngineComponentProps = ReduxProps & DispatchProps; const DetectionEngineComponent = React.memo( ({ filters, query, setAbsoluteRangeDatePicker }) => { - const [ + const { loading, isSignalIndexExists, - isUserAuthenticated, + isAuthenticated: isUserAuthenticated, canUserCRUD, signalIndexName, - ] = useUserInfo(); + } = useUserInfo(); const [lastSignals] = useSignalInfo({}); @@ -141,11 +141,11 @@ const DetectionEngineComponent = React.memo( ) : ( - - - - - ); + + + + + ); }} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx index b35a0f7daab1f..ddab3c3865d6c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx @@ -74,8 +74,7 @@ type RulesColumns = EuiBasicTableColumn | EuiTableActionsColumnType, history: H.History, - canUserCRUD: boolean, - hasWriteToChangeActivation: boolean + hasNoPermissions: boolean ): RulesColumns[] => { const cols: RulesColumns[] = [ { @@ -99,10 +98,10 @@ export const getColumns = ( value === 'low' ? euiLightVars.euiColorVis0 : value === 'medium' - ? euiLightVars.euiColorVis5 - : value === 'high' - ? euiLightVars.euiColorVis7 - : euiLightVars.euiColorVis9 + ? euiLightVars.euiColorVis5 + : value === 'high' + ? euiLightVars.euiColorVis7 + : euiLightVars.euiColorVis9 } > {value} @@ -117,8 +116,8 @@ export const getColumns = ( return value == null ? ( getEmptyTagValue() ) : ( - - ); + + ); }, sortable: true, truncateText: true, @@ -131,16 +130,16 @@ export const getColumns = ( return value == null ? ( getEmptyTagValue() ) : ( - <> - {value.type === 'Fail' ? ( - - {value.type} - - ) : ( - {value.type} - )} - - ); + <> + {value.type === 'Fail' ? ( + + {value.type} + + ) : ( + {value.type} + )} + + ); }, truncateText: true, }, @@ -170,7 +169,7 @@ export const getColumns = ( dispatch={dispatch} id={item.id} enabled={item.activate} - isDisabled={!canUserCRUD || !hasWriteToChangeActivation} + isDisabled={hasNoPermissions} isLoading={item.isLoading} /> ), @@ -185,5 +184,5 @@ export const getColumns = ( } as EuiTableActionsColumnType, ]; - return canUserCRUD ? [...cols, ...actions] : cols; + return hasNoPermissions ? cols : [...cols, ...actions]; }; 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 f04f8f010e180..c65aaae0e5f84 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 @@ -61,11 +61,10 @@ const initialState: State = { * * Import/Export */ export const AllRules = React.memo<{ - canUserCRUD: boolean; - hasWriteToChangeActivation: boolean; + hasNoPermissions: boolean; importCompleteToggle: boolean; loading: boolean; -}>(({ canUserCRUD, hasWriteToChangeActivation, importCompleteToggle, loading }) => { +}>(({ hasNoPermissions, importCompleteToggle, loading }) => { const [ { exportPayload, @@ -148,86 +147,86 @@ export const AllRules = React.memo<{ {isInitialLoad ? ( ) : ( - <> - - { + <> + + { + dispatch({ + type: 'updateFilterOptions', + filterOptions: { + ...filterOptions, + filter: filterString, + }, + }); + }} + /> + + + + + + {i18n.SHOWING_RULES(pagination.total ?? 0)} + + + + {i18n.SELECTED_RULES(selectedItems.length)} + {hasNoPermissions && ( + + {i18n.BATCH_ACTIONS} + + )} + dispatch({ type: 'refresh' })} + > + {i18n.REFRESH} + + + + + + { + dispatch({ + type: 'updatePagination', + pagination: { ...pagination, page: page.index + 1, perPage: page.size }, + }); dispatch({ type: 'updateFilterOptions', filterOptions: { ...filterOptions, - filter: filterString, + sortField: 'enabled', // Only enabled is supported for sorting currently + sortOrder: sort!.direction, }, }); }} + pagination={{ + pageIndex: pagination.page - 1, + pageSize: pagination.perPage, + totalItemCount: pagination.total, + pageSizeOptions: [5, 10, 20], + }} + sorting={{ sort: { field: 'activate', direction: filterOptions.sortOrder } }} + selection={hasNoPermissions ? undefined : euiBasicTableSelectionProps} /> - - - - - - {i18n.SHOWING_RULES(pagination.total ?? 0)} - - - - {i18n.SELECTED_RULES(selectedItems.length)} - {canUserCRUD && ( - - {i18n.BATCH_ACTIONS} - - )} - dispatch({ type: 'refresh' })} - > - {i18n.REFRESH} - - - - - - { - 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, - }, - }); - }} - pagination={{ - pageIndex: pagination.page - 1, - pageSize: pagination.perPage, - totalItemCount: pagination.total, - pageSizeOptions: [5, 10, 20], - }} - sorting={{ sort: { field: 'activate', direction: filterOptions.sortOrder } }} - selection={canUserCRUD ? euiBasicTableSelectionProps : undefined} - /> - {(isLoading || loading) && ( - - )} - - )} + {(isLoading || loading) && ( + + )} + + )} ); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/translations.ts index 006982b6ce73d..c3429f4365031 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/translations.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; export const READ_ONLY_CALLOUT_TITLE = i18n.translate( 'xpack.siem.detectionEngine.readOnlyCallOutTitle', { - defaultMessage: 'Edit rule permissions required', + defaultMessage: 'Rule permissions required', } ); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx index 1a9f781e2a681..2b9e5fca88f59 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx @@ -27,7 +27,7 @@ import * as i18n from './translations'; const stepsRuleOrder = [RuleStep.defineRule, RuleStep.aboutRule, RuleStep.scheduleRule]; -const ResizeEuiPanel = styled(EuiPanel)<{ +const ResizeEuiPanel = styled(EuiPanel) <{ height?: number; }>` .euiAccordion__iconWrapper { @@ -57,7 +57,13 @@ const MyEuiPanel = styled(EuiPanel)` `; export const CreateRuleComponent = React.memo(() => { - const [loading, isSignalIndexExists, isAuthenticated, canUserCRUD] = useUserInfo(); + const { + loading, + isSignalIndexExists, + isAuthenticated, + canUserCRUD, + hasManageApiKey, + } = useUserInfo(); const [heightAccordion, setHeightAccordion] = useState(-1); const [openAccordionId, setOpenAccordionId] = useState(RuleStep.defineRule); const defineRuleRef = useRef(null); @@ -79,6 +85,8 @@ export const CreateRuleComponent = React.memo(() => { [RuleStep.scheduleRule]: false, }); const [{ isLoading, isSaved }, setRule] = usePersistRule(); + const userHasNoPermissions = + canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false; if ( isSignalIndexExists != null && @@ -86,7 +94,7 @@ export const CreateRuleComponent = React.memo(() => { (!isSignalIndexExists || !isAuthenticated) ) { return ; - } else if (canUserCRUD != null && !canUserCRUD) { + } else if (userHasNoPermissions) { return ; } 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 ec219225e4b39..04c5ed3cf72d9 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 @@ -36,6 +36,7 @@ import { StepDefineRule } from '../components/step_define_rule'; import { StepScheduleRule } from '../components/step_schedule_rule'; import { buildSignalsRuleIdFilter } from '../../components/signals/default_config'; import * as detectionI18n from '../../translations'; +import { ReadOnlyCallOut } from '../components/read_only_callout'; import { RuleSwitch } from '../components/rule_switch'; import { StepPanel } from '../components/step_panel'; import { getStepsData } from '../helpers'; @@ -68,14 +69,14 @@ type RuleDetailsComponentProps = ReduxProps & DispatchProps; const RuleDetailsComponent = memo( ({ filters, query, setAbsoluteRangeDatePicker }) => { - const [ + const { loading, isSignalIndexExists, isAuthenticated, canUserCRUD, + hasManageApiKey, signalIndexName, - hasWriteToChangeActivation, - ] = useUserInfo(); + } = useUserInfo(); const { ruleId } = useParams(); const [isLoading, rule] = useRule(ruleId); const { aboutRuleData, defineRuleData, scheduleRuleData } = getStepsData({ @@ -83,6 +84,8 @@ const RuleDetailsComponent = memo( detailsView: true, }); const [lastSignals] = useSignalInfo({ ruleId }); + const userHasNoPermissions = + canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false; if ( isSignalIndexExists != null && @@ -98,39 +101,39 @@ const RuleDetailsComponent = memo( isLoading === true || rule === null ? ( ) : ( - [ - - ), - }} - />, - rule?.updated_by != null ? ( + [ ), }} - /> - ) : ( - '' - ), - ] - ), + />, + rule?.updated_by != null ? ( + + ), + }} + /> + ) : ( + '' + ), + ] + ), [isLoading, rule] ); @@ -153,6 +156,7 @@ const RuleDetailsComponent = memo( return ( <> + {userHasNoPermissions && } {({ indicesExist, indexPattern }) => { return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( @@ -188,7 +192,7 @@ const RuleDetailsComponent = memo( @@ -200,7 +204,7 @@ const RuleDetailsComponent = memo( {ruleI18n.EDIT_RULE_SETTINGS} @@ -280,12 +284,12 @@ const RuleDetailsComponent = memo( )} ) : ( - - + + - - - ); + + + ); }} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx index 86a59bcb6ac28..e583461f52439 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx @@ -48,16 +48,25 @@ interface ScheduleStepRuleForm extends StepRuleForm { } export const EditRuleComponent = memo(() => { - const [initLoading, isSignalIndexExists, isAuthenticated, canUserCRUD] = useUserInfo(); + const { + loading: initLoading, + isSignalIndexExists, + isAuthenticated, + canUserCRUD, + hasManageApiKey, + } = useUserInfo(); const { ruleId } = useParams(); const [loading, rule] = useRule(ruleId); + + const userHasNoPermissions = + canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false; if ( isSignalIndexExists != null && isAuthenticated != null && (!isSignalIndexExists || !isAuthenticated) ) { return ; - } else if (canUserCRUD != null && !canUserCRUD) { + } else if (userHasNoPermissions) { return ; } 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 dee259f9d15b9..bb56f077b0789 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 @@ -25,13 +25,13 @@ import * as i18n from './translations'; export const RulesComponent = React.memo(() => { const [showImportModal, setShowImportModal] = useState(false); const [importCompleteToggle, setImportCompleteToggle] = useState(false); - const [ + const { loading, isSignalIndexExists, isAuthenticated, canUserCRUD, - hasWriteToChangeActivation, - ] = useUserInfo(); + hasManageApiKey, + } = useUserInfo(); if ( isSignalIndexExists != null && @@ -40,11 +40,12 @@ export const RulesComponent = React.memo(() => { ) { return ; } - + const userHasNoPermissions = + canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false; const lastCompletedRun = undefined; return ( <> - {!canUserCRUD && } + {userHasNoPermissions && } setShowImportModal(false)} @@ -66,8 +67,8 @@ export const RulesComponent = React.memo(() => { }} /> ) : ( - getEmptyTagValue() - ) + getEmptyTagValue() + ) } title={i18n.PAGE_TITLE} > @@ -75,7 +76,7 @@ export const RulesComponent = React.memo(() => { { setShowImportModal(true); }} @@ -88,7 +89,7 @@ export const RulesComponent = React.memo(() => { fill href={`#${DETECTION_ENGINE_PAGE_NAME}/rules/create`} iconType="plusInCircle" - isDisabled={!canUserCRUD || loading} + isDisabled={userHasNoPermissions || loading} > {i18n.ADD_NEW_RULE} @@ -97,9 +98,8 @@ export const RulesComponent = React.memo(() => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts index ed5d87531d9aa..705f542b50124 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts @@ -23,7 +23,7 @@ export const getIndexExists = async ( }); return response._shards.total > 0; } catch (err) { - if (err.statusCode === 404) { + if (err.status === 404) { return false; } else { throw err; From 5c2568b66f7bdd0219f2b9f384bf27eb129eeb49 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Fri, 10 Jan 2020 22:33:38 -0500 Subject: [PATCH 13/14] bug on batch action for rules --- .../signals/signals_utility_bar/index.tsx | 68 +++++++++---------- .../detection_engine/rules/all/index.tsx | 2 +- .../index/get_index_exists.test.ts | 6 +- 3 files changed, 37 insertions(+), 39 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx index c5ffb519fb1b3..b843bbb981499 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx @@ -84,43 +84,41 @@ const SignalsUtilityBarComponent: React.FC = ({ {i18n.SHOWING_SIGNALS(formattedTotalCount, totalCount)} - {canUserCRUD && ( - - {totalCount > 0 && ( - <> - - {i18n.SELECTED_SIGNALS( - showClearSelection ? formattedTotalCount : formattedSelectedEventsCount, - showClearSelection ? totalCount : Object.keys(selectedEventIds).length - )} - + + {canUserCRUD && ( + <> + + {i18n.SELECTED_SIGNALS( + showClearSelection ? formattedTotalCount : formattedSelectedEventsCount, + showClearSelection ? totalCount : Object.keys(selectedEventIds).length + )} + - - {i18n.BATCH_ACTIONS} - + + {i18n.BATCH_ACTIONS} + - { - if (!showClearSelection) { - selectAll(); - } else { - clearSelection(); - } - }} - > - {showClearSelection - ? i18n.CLEAR_SELECTION - : i18n.SELECT_ALL_SIGNALS(formattedTotalCount, totalCount)} - - - )} - - )} + { + if (!showClearSelection) { + selectAll(); + } else { + clearSelection(); + } + }} + > + {showClearSelection + ? i18n.CLEAR_SELECTION + : i18n.SELECT_ALL_SIGNALS(formattedTotalCount, totalCount)} + + + )} + 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 a3d2e167ccca9..e900058b6c53c 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 @@ -174,7 +174,7 @@ export const AllRules = React.memo<{ {i18n.SELECTED_RULES(selectedItems.length)} - {hasNoPermissions && ( + {!hasNoPermissions && ( Date: Sat, 11 Jan 2020 00:06:04 -0500 Subject: [PATCH 14/14] add permissions to write status on signal --- .../siem/public/components/inspect/index.tsx | 2 +- .../__snapshots__/index.test.tsx.snap | 6 +-- .../signals/use_privilege_user.tsx | 16 ++++++-- .../no_write_signals_callout/index.tsx | 26 +++++++++++++ .../no_write_signals_callout/translations.ts | 29 +++++++++++++++ .../components/signals/default_config.tsx | 4 +- .../components/signals/index.tsx | 5 +++ .../signals_utility_bar/batch_actions.tsx | 27 +++++++++----- .../signals/signals_utility_bar/index.tsx | 13 ++++--- .../components/user_info/index.tsx | 37 +++++++++++++++---- .../detection_engine/detection_engine.tsx | 8 +++- .../detection_engine/rules/details/index.tsx | 4 ++ 12 files changed, 144 insertions(+), 33 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx b/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx index 04d6d94d6624d..a2a0ffdde34a5 100644 --- a/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx @@ -20,7 +20,7 @@ import * as i18n from './translations'; const InspectContainer = styled.div<{ showInspect: boolean }>` .euiButtonIcon { - ${props => (props.showInspect ? 'opacity: 1;' : 'opacity: 0')} + ${props => (props.showInspect ? 'opacity: 1;' : 'opacity: 0;')} transition: opacity ${props => getOr(250, 'theme.eui.euiAnimSpeedNormal', props)} ease; } `; diff --git a/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap index 098f54640e4b2..5ed750b519cbf 100644 --- a/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap @@ -105,7 +105,7 @@ exports[`Stat Items Component disable charts it renders the default widget 1`] = showInspect={false} >
{ 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 [, dispatchToaster] = useStateToaster(); @@ -39,7 +45,8 @@ export const usePrivilegeUser = (): Return => { setAuthenticated(privilege.isAuthenticated); if (privilege.index != null && Object.keys(privilege.index).length > 0) { const indexName = Object.keys(privilege.index)[0]; - setHasIndexWrite(privilege.index[indexName].manage); + setHasIndexManage(privilege.index[indexName].manage); + setHasIndexWrite(privilege.index[indexName].write); setHasManageApiKey( privilege.cluster.manage_security || privilege.cluster.manage_api_key || @@ -50,6 +57,7 @@ export const usePrivilegeUser = (): Return => { } catch (error) { if (isSubscribed) { setAuthenticated(false); + setHasIndexManage(false); setHasIndexWrite(false); errorToToaster({ title: i18n.PRIVILEGE_FETCH_FAILURE, error, dispatchToaster }); } @@ -66,5 +74,5 @@ export const usePrivilegeUser = (): Return => { }; }, []); - return [loading, isAuthenticated, hasIndexWrite, hasManageApiKey]; + return { loading, isAuthenticated, hasIndexManage, hasManageApiKey, hasIndexWrite }; }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.tsx new file mode 100644 index 0000000000000..1950531998450 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiCallOut, EuiButton } from '@elastic/eui'; +import React, { memo, useCallback, useState } from 'react'; + +import * as i18n from './translations'; + +const NoWriteSignalsCallOutComponent = () => { + const [showCallOut, setShowCallOut] = useState(true); + const handleCallOut = useCallback(() => setShowCallOut(false), [setShowCallOut]); + + return showCallOut ? ( + +

{i18n.NO_WRITE_SIGNALS_CALLOUT_MSG}

+ + {i18n.DISMISS_CALLOUT} + +
+ ) : null; +}; + +export const NoWriteSignalsCallOut = memo(NoWriteSignalsCallOutComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/translations.ts new file mode 100644 index 0000000000000..065d775e1dc6a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/translations.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const NO_WRITE_SIGNALS_CALLOUT_TITLE = i18n.translate( + 'xpack.siem.detectionEngine.noWriteSignalsCallOutTitle', + { + defaultMessage: 'Signals index permissions required', + } +); + +export const NO_WRITE_SIGNALS_CALLOUT_MSG = i18n.translate( + 'xpack.siem.detectionEngine.noWriteSignalsCallOutMsg', + { + defaultMessage: + 'You are currently missing the required permissions to update signals. Please contact your administrator for further assistance.', + } +); + +export const DISMISS_CALLOUT = i18n.translate( + 'xpack.siem.detectionEngine.dismissNoWriteSignalButton', + { + defaultMessage: 'Dismiss', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx index 5bf661c2f8ca8..83b6ba690ec5b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx @@ -169,12 +169,14 @@ export const requiredFieldsForActions = [ export const getSignalsActions = ({ canUserCRUD, + hasIndexWrite, setEventsLoading, setEventsDeleted, createTimeline, status, }: { canUserCRUD: boolean; + hasIndexWrite: boolean; setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void; setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void; createTimeline: CreateTimeline; @@ -199,7 +201,7 @@ export const getSignalsActions = ({ width: 26, }, ]; - return canUserCRUD + return canUserCRUD && hasIndexWrite ? [ ...actions, { 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 aea28ef57f380..d149eb700ad03 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 @@ -93,6 +93,7 @@ interface DispatchProps { interface OwnProps { canUserCRUD: boolean; defaultFilters?: esFilters.Filter[]; + hasIndexWrite: boolean; from: number; loading: boolean; signalsIndex: string; @@ -112,6 +113,7 @@ export const SignalsTableComponent = React.memo( from, globalFilters, globalQuery, + hasIndexWrite, isSelectAllChecked, loading, loadingEventIds, @@ -238,6 +240,7 @@ export const SignalsTableComponent = React.memo( canUserCRUD={canUserCRUD} areEventsLoading={loadingEventIds.length > 0} clearSelection={clearSelectionCallback} + hasIndexWrite={hasIndexWrite} isFilteredToOpen={filterGroup === FILTER_OPEN} selectAll={selectAllCallback} selectedEventIds={selectedEventIds} @@ -250,6 +253,7 @@ export const SignalsTableComponent = React.memo( }, [ canUserCRUD, + hasIndexWrite, clearSelectionCallback, filterGroup, loadingEventIds.length, @@ -264,6 +268,7 @@ export const SignalsTableComponent = React.memo( () => getSignalsActions({ canUserCRUD, + hasIndexWrite, createTimeline: createTimelineCallback, setEventsLoading: setEventsLoadingCallback, setEventsDeleted: setEventsDeletedCallback, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/batch_actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/batch_actions.tsx index bbbc7728e36a5..b756b2eb75a7a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/batch_actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/batch_actions.tsx @@ -11,6 +11,15 @@ import { TimelineNonEcsData } from '../../../../../graphql/types'; import { SendSignalsToTimeline, UpdateSignalsStatus } from '../types'; import { FILTER_CLOSED, FILTER_OPEN } from '../signals_filter_group'; +interface GetBatchItems { + areEventsLoading: boolean; + allEventsSelected: boolean; + selectedEventIds: Readonly>; + updateSignalsStatus: UpdateSignalsStatus; + sendSignalsToTimeline: SendSignalsToTimeline; + closePopover: () => void; + isFilteredToOpen: boolean; +} /** * Returns ViewInTimeline / UpdateSignalStatus actions to be display within an EuiContextMenuPanel * @@ -22,15 +31,15 @@ import { FILTER_CLOSED, FILTER_OPEN } from '../signals_filter_group'; * @param closePopover * @param isFilteredToOpen currently selected filter options */ -export const getBatchItems = ( - areEventsLoading: boolean, - allEventsSelected: boolean, - selectedEventIds: Readonly>, - updateSignalsStatus: UpdateSignalsStatus, - sendSignalsToTimeline: SendSignalsToTimeline, - closePopover: () => void, - isFilteredToOpen: boolean -) => { +export const getBatchItems = ({ + areEventsLoading, + allEventsSelected, + selectedEventIds, + updateSignalsStatus, + sendSignalsToTimeline, + closePopover, + isFilteredToOpen, +}: GetBatchItems) => { const allDisabled = areEventsLoading || Object.keys(selectedEventIds).length === 0; const sendToTimelineDisabled = allEventsSelected || uniqueRuleCount(selectedEventIds) > 1; const filterString = isFilteredToOpen diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx index b843bbb981499..e28fb3e06870e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx @@ -23,6 +23,7 @@ import { SendSignalsToTimeline, UpdateSignalsStatus } from '../types'; interface SignalsUtilityBarProps { canUserCRUD: boolean; + hasIndexWrite: boolean; areEventsLoading: boolean; clearSelection: () => void; isFilteredToOpen: boolean; @@ -36,6 +37,7 @@ interface SignalsUtilityBarProps { const SignalsUtilityBarComponent: React.FC = ({ canUserCRUD, + hasIndexWrite, areEventsLoading, clearSelection, totalCount, @@ -51,15 +53,15 @@ const SignalsUtilityBarComponent: React.FC = ({ const getBatchItemsPopoverContent = useCallback( (closePopover: () => void) => ( ), [ @@ -68,6 +70,7 @@ const SignalsUtilityBarComponent: React.FC = ({ updateSignalsStatus, sendSignalsToTimeline, isFilteredToOpen, + hasIndexWrite, ] ); @@ -85,7 +88,7 @@ const SignalsUtilityBarComponent: React.FC = ({ - {canUserCRUD && ( + {canUserCRUD && hasIndexWrite && ( <> {i18n.SELECTED_SIGNALS( 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 92769fa793dac..bbaccb7882484 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 @@ -13,6 +13,7 @@ import { useKibana } from '../../../../lib/kibana'; export interface State { canUserCRUD: boolean | null; + hasIndexManage: boolean | null; hasIndexWrite: boolean | null; hasManageApiKey: boolean | null; isSignalIndexExists: boolean | null; @@ -23,6 +24,7 @@ export interface State { const initialState: State = { canUserCRUD: null, + hasIndexManage: null, hasIndexWrite: null, hasManageApiKey: null, isSignalIndexExists: null, @@ -37,6 +39,10 @@ export type Action = type: 'updateHasManageApiKey'; hasManageApiKey: boolean | null; } + | { + type: 'updateHasIndexManage'; + hasIndexManage: boolean | null; + } | { type: 'updateHasIndexWrite'; hasIndexWrite: boolean | null; @@ -66,6 +72,12 @@ export const userInfoReducer = (state: State, action: Action): State => { loading: action.loading, }; } + case 'updateHasIndexManage': { + return { + ...state, + hasIndexManage: action.hasIndexManage, + }; + } case 'updateHasIndexWrite': { return { ...state, @@ -125,6 +137,7 @@ export const useUserInfo = (): State => { const [ { canUserCRUD, + hasIndexManage, hasIndexWrite, hasManageApiKey, isSignalIndexExists, @@ -134,12 +147,13 @@ export const useUserInfo = (): State => { }, dispatch, ] = useUserData(); - const [ - privilegeLoading, - isApiAuthenticated, - hasApiIndexWrite, - hasApiManageApiKey, - ] = usePrivilegeUser(); + const { + loading: privilegeLoading, + isAuthenticated: isApiAuthenticated, + hasIndexManage: hasApiIndexManage, + hasIndexWrite: hasApiIndexWrite, + hasManageApiKey: hasApiManageApiKey, + } = usePrivilegeUser(); const [ indexNameLoading, isApiSignalIndexExists, @@ -157,6 +171,12 @@ export const useUserInfo = (): State => { } }, [loading, privilegeLoading, indexNameLoading]); + useEffect(() => { + if (hasIndexManage !== hasApiIndexManage && hasApiIndexManage != null) { + dispatch({ type: 'updateHasIndexManage', hasIndexManage: hasApiIndexManage }); + } + }, [hasIndexManage, hasApiIndexManage]); + useEffect(() => { if (hasIndexWrite !== hasApiIndexWrite && hasApiIndexWrite != null) { dispatch({ type: 'updateHasIndexWrite', hasIndexWrite: hasApiIndexWrite }); @@ -196,20 +216,21 @@ export const useUserInfo = (): State => { useEffect(() => { if ( isAuthenticated && - hasIndexWrite && + hasIndexManage && isSignalIndexExists != null && !isSignalIndexExists && createSignalIndex != null ) { createSignalIndex(); } - }, [createSignalIndex, isAuthenticated, isSignalIndexExists, hasIndexWrite]); + }, [createSignalIndex, isAuthenticated, isSignalIndexExists, hasIndexManage]); return { loading, isSignalIndexExists, isAuthenticated, canUserCRUD, + hasIndexManage, hasIndexWrite, hasManageApiKey, signalIndexName, 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 415fc94040a3a..e638cf89e77bb 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 @@ -22,18 +22,19 @@ import { Query } from '../../../../../../../src/plugins/data/common/query'; import { esFilters } from '../../../../../../../src/plugins/data/common/es_query'; import { State } from '../../store'; import { inputsSelectors } from '../../store/inputs'; +import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; import { InputsModelId } from '../../store/inputs/constants'; import { InputsRange } from '../../store/inputs/model'; import { useSignalInfo } from './components/signals_info'; import { SignalsTable } from './components/signals'; +import { NoWriteSignalsCallOut } from './components/no_write_signals_callout'; import { SignalsHistogramPanel } from './components/signals_histogram_panel'; import { signalsHistogramOptions } from './components/signals_histogram_panel/config'; +import { useUserInfo } from './components/user_info'; import { DetectionEngineEmptyPage } from './detection_engine_empty_page'; import { DetectionEngineNoIndex } from './detection_engine_no_signal_index'; import { DetectionEngineUserUnauthenticated } from './detection_engine_user_unauthenticated'; import * as i18n from './translations'; -import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; -import { useUserInfo } from './components/user_info'; interface ReduxProps { filters: esFilters.Filter[]; @@ -58,6 +59,7 @@ const DetectionEngineComponent = React.memo( isAuthenticated: isUserAuthenticated, canUserCRUD, signalIndexName, + hasIndexWrite, } = useUserInfo(); const [lastSignals] = useSignalInfo({}); @@ -87,6 +89,7 @@ const DetectionEngineComponent = React.memo( } return ( <> + {hasIndexWrite != null && !hasIndexWrite && } {({ indicesExist, indexPattern }) => { return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( @@ -130,6 +133,7 @@ const DetectionEngineComponent = React.memo( ( isAuthenticated, canUserCRUD, hasManageApiKey, + hasIndexWrite, signalIndexName, } = useUserInfo(); const { ruleId } = useParams(); @@ -156,6 +158,7 @@ const RuleDetailsComponent = memo( return ( <> + {hasIndexWrite != null && !hasIndexWrite && } {userHasNoPermissions && } {({ indicesExist, indexPattern }) => { @@ -273,6 +276,7 @@ const RuleDetailsComponent = memo(