From b057f18d16f7c862d82c0d3b75444e4905bd5e51 Mon Sep 17 00:00:00 2001
From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
Date: Sat, 11 Jan 2020 08:19:01 -0500
Subject: [PATCH] [SIEM] [Detection engine] Permission II (#54292)
* allow read only user with no CRUD
* use ../../lib/kibana
* fix timeline-template
* add re-routing on page
* bug
* cleanup
* review I
* review II
* a pretty shameful bug I will live thanks Frank
* bug select rule
* only activate deactivate if user has the manage permission
* add permissions rule with manage api key
* bug on batch action for rules
* add permissions to write status on signal
---
.../siem/public/components/inspect/index.tsx | 2 +-
.../__snapshots__/index.test.tsx.snap | 6 +-
.../timeline/search_super_select/index.tsx | 29 ++-
.../containers/detection_engine/rules/api.ts | 25 +-
.../detection_engine/rules/types.ts | 4 +
.../detection_engine/signals/translations.ts | 21 ++
.../signals/use_privilege_user.tsx | 31 ++-
.../signals/use_signal_index.tsx | 9 +-
.../no_write_signals_callout/index.tsx | 26 ++
.../no_write_signals_callout/translations.ts | 29 +++
.../components/signals/default_config.tsx | 97 +++----
.../components/signals/index.tsx | 30 ++-
.../signals_utility_bar/batch_actions.tsx | 27 +-
.../signals/signals_utility_bar/index.tsx | 15 +-
.../components/user_info/index.tsx | 238 ++++++++++++++++++
.../detection_engine/detection_engine.tsx | 69 +++--
.../public/pages/detection_engine/index.tsx | 70 ++----
.../detection_engine/rules/all/columns.tsx | 216 ++++++++--------
.../detection_engine/rules/all/index.tsx | 47 ++--
.../components/read_only_callout/index.tsx | 26 ++
.../read_only_callout/translations.ts | 26 ++
.../__snapshots__/index.test.tsx.snap | 1 -
.../rules/components/rule_switch/index.tsx | 4 +-
.../detection_engine/rules/create/helpers.ts | 8 +-
.../detection_engine/rules/create/index.tsx | 28 ++-
.../detection_engine/rules/details/index.tsx | 56 +++--
.../detection_engine/rules/edit/index.tsx | 35 ++-
.../pages/detection_engine/rules/helpers.tsx | 3 +
.../pages/detection_engine/rules/index.tsx | 32 ++-
.../pages/detection_engine/rules/types.ts | 4 +-
.../index/get_index_exists.test.ts | 44 ++++
.../index/get_index_exists.ts | 26 +-
.../routes/rules/create_rules_route.test.ts | 4 +-
x-pack/legacy/plugins/siem/server/plugin.ts | 2 +-
34 files changed, 952 insertions(+), 338 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
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
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/public/components/inspect/index.tsx b/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx
index 04d6d94d6624..a2a0ffdde34a 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 098f54640e4b..5ed750b519cb 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 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/containers/detection_engine/rules/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts
index f9611995cdb0..b69a8de29e04 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 655299c4a2a3..a329d96d444a 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/translations.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/translations.ts
index 5b5dc9e9699f..2b8f54e5438d 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 aa66df53d9fd..792ff29ad248 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,10 +6,18 @@
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];
-
+interface Return {
+ loading: boolean;
+ isAuthenticated: boolean | null;
+ hasIndexManage: boolean | null;
+ hasManageApiKey: boolean | null;
+ hasIndexWrite: boolean | null;
+}
/**
* Hook to get user privilege from
*
@@ -17,7 +25,10 @@ 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 [hasIndexManage, setHasIndexManage] = useState(null);
+ const [hasIndexWrite, setHasIndexWrite] = useState(null);
+ const [hasManageApiKey, setHasManageApiKey] = useState(null);
+ const [, dispatchToaster] = useStateToaster();
useEffect(() => {
let isSubscribed = true;
@@ -34,13 +45,21 @@ 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);
+ setHasIndexManage(privilege.index[indexName].manage);
+ setHasIndexWrite(privilege.index[indexName].write);
+ setHasManageApiKey(
+ privilege.cluster.manage_security ||
+ privilege.cluster.manage_api_key ||
+ privilege.cluster.manage_own_api_key
+ );
}
}
} catch (error) {
if (isSubscribed) {
setAuthenticated(false);
- setHasWrite(false);
+ setHasIndexManage(false);
+ setHasIndexWrite(false);
+ errorToToaster({ title: i18n.PRIVILEGE_FETCH_FAILURE, error, dispatchToaster });
}
}
if (isSubscribed) {
@@ -55,5 +74,5 @@ export const usePrivilegeUser = (): Return => {
};
}, []);
- return [loading, isAuthenticated, hasWrite];
+ return { loading, isAuthenticated, hasIndexManage, hasManageApiKey, hasIndexWrite };
};
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 1ff4422cf641..189d8a1bf3f7 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,9 +8,10 @@ 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 } from './types';
+import { PostSignalError, SignalIndexError } from './types';
type Func = () => void;
@@ -40,11 +41,15 @@ export const useSignalIndex = (): Return => {
if (isSubscribed && signal != null) {
setSignalIndexName(signal.name);
setSignalIndexExists(true);
+ createPrepackagedRules({ signal: abortCtrl.signal });
}
} catch (error) {
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 +74,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/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 000000000000..195053199845
--- /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 000000000000..065d775e1dc6
--- /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 1a7ad5822a24..83b6ba690ec5 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,66 @@ 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;
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 && hasIndexWrite
+ ? [
+ ...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 47a78482cfb6..d149eb700ad0 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 {
@@ -88,8 +91,11 @@ interface DispatchProps {
}
interface OwnProps {
+ canUserCRUD: boolean;
defaultFilters?: esFilters.Filter[];
+ hasIndexWrite: boolean;
from: number;
+ loading: boolean;
signalsIndex: string;
to: number;
}
@@ -98,6 +104,7 @@ type SignalsTableComponentProps = OwnProps & ReduxProps & DispatchProps;
export const SignalsTableComponent = React.memo(
({
+ canUserCRUD,
createTimeline,
clearEventsDeleted,
clearEventsLoading,
@@ -106,7 +113,9 @@ export const SignalsTableComponent = React.memo(
from,
globalFilters,
globalQuery,
+ hasIndexWrite,
isSelectAllChecked,
+ loading,
loadingEventIds,
removeTimelineLinkTo,
selectedEventIds,
@@ -228,8 +237,10 @@ export const SignalsTableComponent = React.memo(
(totalCount: number) => {
return (
0}
clearSelection={clearSelectionCallback}
+ hasIndexWrite={hasIndexWrite}
isFilteredToOpen={filterGroup === FILTER_OPEN}
selectAll={selectAllCallback}
selectedEventIds={selectedEventIds}
@@ -241,6 +252,8 @@ export const SignalsTableComponent = React.memo(
);
},
[
+ canUserCRUD,
+ hasIndexWrite,
clearSelectionCallback,
filterGroup,
loadingEventIds.length,
@@ -254,12 +267,14 @@ export const SignalsTableComponent = React.memo(
const additionalActions = useMemo(
() =>
getSignalsActions({
+ canUserCRUD,
+ hasIndexWrite,
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,11 +294,20 @@ export const SignalsTableComponent = React.memo(
queryFields: requiredFieldsForActions,
timelineActions: additionalActions,
title: i18n.SIGNALS_TABLE_TITLE,
- selectAll,
+ selectAll: canUserCRUD ? selectAll : false,
}),
- [additionalActions, selectAll]
+ [additionalActions, canUserCRUD, selectAll]
);
+ if (loading) {
+ return (
+
+
+
+
+ );
+ }
+
return (
>;
+ 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 f80de053b59b..e28fb3e06870 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,8 @@ import { TimelineNonEcsData } from '../../../../../graphql/types';
import { SendSignalsToTimeline, UpdateSignalsStatus } from '../types';
interface SignalsUtilityBarProps {
+ canUserCRUD: boolean;
+ hasIndexWrite: boolean;
areEventsLoading: boolean;
clearSelection: () => void;
isFilteredToOpen: boolean;
@@ -34,6 +36,8 @@ interface SignalsUtilityBarProps {
}
const SignalsUtilityBarComponent: React.FC = ({
+ canUserCRUD,
+ hasIndexWrite,
areEventsLoading,
clearSelection,
totalCount,
@@ -49,15 +53,15 @@ const SignalsUtilityBarComponent: React.FC = ({
const getBatchItemsPopoverContent = useCallback(
(closePopover: () => void) => (
),
[
@@ -66,6 +70,7 @@ const SignalsUtilityBarComponent: React.FC = ({
updateSignalsStatus,
sendSignalsToTimeline,
isFilteredToOpen,
+ hasIndexWrite,
]
);
@@ -83,7 +88,7 @@ const SignalsUtilityBarComponent: React.FC = ({
- {totalCount > 0 && (
+ {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
new file mode 100644
index 000000000000..bbaccb788248
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx
@@ -0,0 +1,238 @@
+/*
+ * 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 { noop } from 'lodash/fp';
+import React, { useEffect, useReducer, Dispatch, createContext, useContext } from 'react';
+
+import { usePrivilegeUser } from '../../../../containers/detection_engine/signals/use_privilege_user';
+import { useSignalIndex } from '../../../../containers/detection_engine/signals/use_signal_index';
+import { useKibana } from '../../../../lib/kibana';
+
+export interface State {
+ canUserCRUD: boolean | null;
+ hasIndexManage: boolean | null;
+ hasIndexWrite: boolean | null;
+ hasManageApiKey: boolean | null;
+ isSignalIndexExists: boolean | null;
+ isAuthenticated: boolean | null;
+ loading: boolean;
+ signalIndexName: string | null;
+}
+
+const initialState: State = {
+ canUserCRUD: null,
+ hasIndexManage: null,
+ hasIndexWrite: null,
+ hasManageApiKey: null,
+ isSignalIndexExists: null,
+ isAuthenticated: null,
+ loading: true,
+ signalIndexName: null,
+};
+
+export type Action =
+ | { type: 'updateLoading'; loading: boolean }
+ | {
+ type: 'updateHasManageApiKey';
+ hasManageApiKey: boolean | null;
+ }
+ | {
+ type: 'updateHasIndexManage';
+ hasIndexManage: boolean | null;
+ }
+ | {
+ type: 'updateHasIndexWrite';
+ hasIndexWrite: boolean | null;
+ }
+ | {
+ type: 'updateIsSignalIndexExists';
+ isSignalIndexExists: boolean | null;
+ }
+ | {
+ type: 'updateIsAuthenticated';
+ isAuthenticated: boolean | null;
+ }
+ | {
+ type: 'updateCanUserCRUD';
+ canUserCRUD: boolean | null;
+ }
+ | {
+ type: 'updateSignalIndexName';
+ signalIndexName: string | null;
+ };
+
+export const userInfoReducer = (state: State, action: Action): State => {
+ switch (action.type) {
+ case 'updateLoading': {
+ return {
+ ...state,
+ loading: action.loading,
+ };
+ }
+ case 'updateHasIndexManage': {
+ return {
+ ...state,
+ hasIndexManage: action.hasIndexManage,
+ };
+ }
+ case 'updateHasIndexWrite': {
+ return {
+ ...state,
+ hasIndexWrite: action.hasIndexWrite,
+ };
+ }
+ case 'updateHasManageApiKey': {
+ return {
+ ...state,
+ hasManageApiKey: action.hasManageApiKey,
+ };
+ }
+ 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 = (): State => {
+ const [
+ {
+ canUserCRUD,
+ hasIndexManage,
+ hasIndexWrite,
+ hasManageApiKey,
+ isSignalIndexExists,
+ isAuthenticated,
+ loading,
+ signalIndexName,
+ },
+ dispatch,
+ ] = useUserData();
+ const {
+ loading: privilegeLoading,
+ isAuthenticated: isApiAuthenticated,
+ hasIndexManage: hasApiIndexManage,
+ hasIndexWrite: hasApiIndexWrite,
+ hasManageApiKey: hasApiManageApiKey,
+ } = usePrivilegeUser();
+ const [
+ indexNameLoading,
+ isApiSignalIndexExists,
+ apiSignalIndexName,
+ createSignalIndex,
+ ] = useSignalIndex();
+
+ const uiCapabilities = useKibana().services.application.capabilities;
+ const capabilitiesCanUserCRUD: boolean =
+ typeof uiCapabilities.siem.crud === 'boolean' ? uiCapabilities.siem.crud : false;
+
+ useEffect(() => {
+ if (loading !== privilegeLoading || indexNameLoading) {
+ dispatch({ type: 'updateLoading', loading: privilegeLoading || indexNameLoading });
+ }
+ }, [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 });
+ }
+ }, [hasIndexWrite, hasApiIndexWrite]);
+
+ useEffect(() => {
+ if (hasManageApiKey !== hasApiManageApiKey && hasApiManageApiKey != null) {
+ dispatch({ type: 'updateHasManageApiKey', hasManageApiKey: hasApiManageApiKey });
+ }
+ }, [hasManageApiKey, hasApiManageApiKey]);
+
+ 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 &&
+ hasIndexManage &&
+ isSignalIndexExists != null &&
+ !isSignalIndexExists &&
+ createSignalIndex != null
+ ) {
+ createSignalIndex();
+ }
+ }, [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 2a91a559ec0e..e638cf89e77b 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, EuiLoadingContent, EuiPanel, EuiSpacer } from '@elastic/eui';
+import { EuiButton, EuiSpacer } from '@elastic/eui';
import React, { useCallback } from 'react';
import { StickyContainer } from 'react-sticky';
@@ -18,30 +18,23 @@ import { GlobalTime } from '../../containers/global_time';
import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source';
import { SpyRoute } from '../../utils/route/spy_routes';
-import { SignalsTable } from './components/signals';
-import * as signalsI18n from './components/signals/translations';
-import { SignalsHistogramPanel } from './components/signals_histogram_panel';
import { Query } from '../../../../../../../src/plugins/data/common/query';
import { esFilters } from '../../../../../../../src/plugins/data/common/es_query';
-import { inputsSelectors } from '../../store/inputs';
import { State } from '../../store';
+import { 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 { signalsHistogramOptions } from './components/signals_histogram_panel/config';
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 { HeaderSection } from '../../components/header_section';
-import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions';
-import { InputsModelId } from '../../store/inputs/constants';
-
-interface OwnProps {
- loading: boolean;
- isSignalIndexExists: boolean | null;
- isUserAuthenticated: boolean | null;
- signalsIndex: string | null;
-}
interface ReduxProps {
filters: esFilters.Filter[];
@@ -56,18 +49,19 @@ export interface DispatchProps {
}>;
}
-type DetectionEngineComponentProps = OwnProps & ReduxProps & DispatchProps;
+type DetectionEngineComponentProps = ReduxProps & DispatchProps;
+
+const DetectionEngineComponent = React.memo(
+ ({ filters, query, setAbsoluteRangeDatePicker }) => {
+ const {
+ loading,
+ isSignalIndexExists,
+ isAuthenticated: isUserAuthenticated,
+ canUserCRUD,
+ signalIndexName,
+ hasIndexWrite,
+ } = useUserInfo();
-export const DetectionEngineComponent = React.memo(
- ({
- filters,
- loading,
- isSignalIndexExists,
- isUserAuthenticated,
- query,
- setAbsoluteRangeDatePicker,
- signalsIndex,
- }) => {
const [lastSignals] = useSignalInfo({});
const updateDateRangeCallback = useCallback(
@@ -95,6 +89,7 @@ export const DetectionEngineComponent = React.memo
+ {hasIndexWrite != null && !hasIndexWrite && }
{({ indicesExist, indexPattern }) => {
return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? (
@@ -102,7 +97,6 @@ export const DetectionEngineComponent = React.memo
-
- {!loading ? (
- isSignalIndexExists && (
-
- )
- ) : (
-
-
-
-
- )}
+
>
)}
@@ -160,7 +152,6 @@ export 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 c32cab7f933b..c4e83429aebd 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,70 +4,38 @@
* 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 { CreateRuleComponent } from './rules/create';
import { DetectionEngine } from './detection_engine';
import { EditRuleComponent } from './rules/edit';
import { RuleDetails } from './rules/details';
import { RulesComponent } from './rules';
+import { 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,
- isSignalIndexExists,
- signalIndexName,
- createSignalIndex,
- ] = useSignalIndex();
-
- useEffect(() => {
- if (
- isAuthenticated &&
- hasWrite &&
- isSignalIndexExists != null &&
- !isSignalIndexExists &&
- createSignalIndex != null
- ) {
- createSignalIndex();
- }
- }, [createSignalIndex, isAuthenticated, isSignalIndexExists, hasWrite]);
-
- return (
+export const DetectionEngineContainer = React.memo(() => (
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
- {isSignalIndexExists && isAuthenticated && (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
- >
- )}
-
(
@@ -75,6 +43,6 @@ export const DetectionEngineContainer = React.memo(() => {
)}
/>
- );
-});
+
+));
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 42c4bb1d0ef9..95b9c9324894 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,121 @@ 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,
+ hasNoPermissions: 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%',
+ },
+ {
+ align: 'center',
+ field: 'activate',
+ name: i18n.COLUMN_ACTIVATE,
+ render: (value: TableData['activate'], item: TableData) => (
+
+ ),
+ sortable: true,
+ width: '85px',
+ },
+ ];
+ const actions: RulesColumns[] = [
+ {
+ actions: getActions(dispatch, history),
+ width: '40px',
+ } as EuiTableActionsColumnType,
+ ];
+
+ 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 060f8baccc3b..e900058b6c53 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,7 +60,11 @@ const initialState: State = {
* * Delete
* * Import/Export
*/
-export const AllRules = React.memo<{ importCompleteToggle: boolean }>(importCompleteToggle => {
+export const AllRules = React.memo<{
+ hasNoPermissions: boolean;
+ importCompleteToggle: boolean;
+ loading: boolean;
+}>(({ hasNoPermissions, importCompleteToggle, loading }) => {
const [
{
exportPayload,
@@ -111,6 +115,15 @@ export const AllRules = React.memo<{ importCompleteToggle: boolean }>(importComp
});
}, [rulesData]);
+ const euiBasicTableSelectionProps = useMemo(
+ () => ({
+ selectable: (item: TableData) => !item.isLoading,
+ onSelectionChange: (selected: TableData[]) =>
+ dispatch({ type: 'setSelected', selectedItems: selected }),
+ }),
+ []
+ );
+
return (
<>
(importComp
{i18n.SELECTED_RULES(selectedItems.length)}
-
- {i18n.BATCH_ACTIONS}
-
+ {!hasNoPermissions && (
+
+ {i18n.BATCH_ACTIONS}
+
+ )}
(importComp
{
@@ -204,14 +219,12 @@ export const AllRules = React.memo<{ importCompleteToggle: boolean }>(importComp
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 } }}
+ selection={hasNoPermissions ? undefined : euiBasicTableSelectionProps}
/>
- {isLoading && }
+ {(isLoading || loading) && (
+
+ )}
>
)}
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 000000000000..6ec76bacc232
--- /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 000000000000..c3429f436503
--- /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: '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/__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 f264dde07c59..604f86866d56 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`] = `
;
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/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts
index 12bbdbdfff3e..ce91e15cdcf0 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/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx
index 848b17aadbff..9a0f41bbd8c5 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,13 @@ const MyEuiPanel = styled(EuiPanel)`
`;
export const CreateRuleComponent = React.memo(() => {
+ const {
+ loading,
+ isSignalIndexExists,
+ isAuthenticated,
+ canUserCRUD,
+ hasManageApiKey,
+ } = useUserInfo();
const [heightAccordion, setHeightAccordion] = useState(-1);
const [openAccordionId, setOpenAccordionId] = useState(RuleStep.defineRule);
const defineRuleRef = useRef(null);
@@ -77,6 +85,18 @@ 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 &&
+ isAuthenticated != null &&
+ (!isSignalIndexExists || !isAuthenticated)
+ ) {
+ return ;
+ } else if (userHasNoPermissions) {
+ return ;
+ }
const setStepData = useCallback(
(step: RuleStep, data: unknown, isValid: boolean) => {
@@ -216,7 +236,7 @@ export const CreateRuleComponent = React.memo(() => {
@@ -242,7 +262,7 @@ export const CreateRuleComponent = React.memo(() => {
setHeightAccordion(height)}
@@ -273,7 +293,7 @@ export const CreateRuleComponent = React.memo(() => {
@@ -303,7 +323,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 9b6998ab4a13..679f42f02519 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, useCallback, useMemo } from 'react';
-import { useParams } from 'react-router-dom';
+import { Redirect, useParams } from 'react-router-dom';
import { StickyContainer } from 'react-sticky';
import { ActionCreator } from 'typescript-fsa';
@@ -28,13 +28,16 @@ import { SpyRoute } from '../../../../utils/route/spy_routes';
import { SignalsHistogramPanel } from '../../components/signals_histogram_panel';
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';
import { StepDefineRule } from '../components/step_define_rule';
import { StepScheduleRule } from '../components/step_schedule_rule';
import { buildSignalsRuleIdFilter } from '../../components/signals/default_config';
+import { NoWriteSignalsCallOut } from '../../components/no_write_signals_callout';
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';
@@ -50,10 +53,6 @@ import { State } from '../../../../store';
import { InputsRange } from '../../../../store/inputs/model';
import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../../../store/inputs/actions';
-interface OwnProps {
- signalsIndex: string | null;
-}
-
interface ReduxProps {
filters: esFilters.Filter[];
query: Query;
@@ -67,22 +66,41 @@ export interface DispatchProps {
}>;
}
-type RuleDetailsComponentProps = OwnProps & ReduxProps & DispatchProps;
+type RuleDetailsComponentProps = ReduxProps & DispatchProps;
const RuleDetailsComponent = memo(
- ({ filters, query, setAbsoluteRangeDatePicker, signalsIndex }) => {
+ ({ filters, query, setAbsoluteRangeDatePicker }) => {
+ const {
+ loading,
+ isSignalIndexExists,
+ isAuthenticated,
+ canUserCRUD,
+ hasManageApiKey,
+ hasIndexWrite,
+ signalIndexName,
+ } = useUserInfo();
const { ruleId } = useParams();
- const [loading, rule] = useRule(ruleId);
+ const [isLoading, rule] = useRule(ruleId);
const { aboutRuleData, defineRuleData, scheduleRuleData } = getStepsData({
rule,
detailsView: true,
});
const [lastSignals] = useSignalInfo({ ruleId });
+ const userHasNoPermissions =
+ canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false;
- const title = loading === true || rule === null ? : rule.name;
+ if (
+ isSignalIndexExists != null &&
+ isAuthenticated != null &&
+ (!isSignalIndexExists || !isAuthenticated)
+ ) {
+ return ;
+ }
+
+ const title = isLoading === true || rule === null ? : rule.name;
const subTitle = useMemo(
() =>
- loading === true || rule === null ? (
+ isLoading === true || rule === null ? (
) : (
[
@@ -118,7 +136,7 @@ const RuleDetailsComponent = memo(
),
]
),
- [loading, rule]
+ [isLoading, rule]
);
const signalDefaultFilters = useMemo(
@@ -140,6 +158,8 @@ const RuleDetailsComponent = memo(
return (
<>
+ {hasIndexWrite != null && !hasIndexWrite && }
+ {userHasNoPermissions && }
{({ indicesExist, indexPattern }) => {
return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? (
@@ -175,6 +195,7 @@ const RuleDetailsComponent = memo(
@@ -186,7 +207,7 @@ const RuleDetailsComponent = memo(
{ruleI18n.EDIT_RULE_SETTINGS}
@@ -200,7 +221,7 @@ const RuleDetailsComponent = memo(
-
+
{defineRuleData != null && (
(
-
+
{aboutRuleData != null && (
(
-
+
{scheduleRuleData != null && (
(
{ruleId != null && (
)}
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 10b7f0e832f1..e583461f5243 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,9 +48,28 @@ interface ScheduleStepRuleForm extends StepRuleForm {
}
export const EditRuleComponent = memo(() => {
+ 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 (userHasNoPermissions) {
+ return ;
+ }
+
const [initForm, setInitForm] = useState(false);
const [myAboutRuleForm, setMyAboutRuleForm] = useState({
data: null,
@@ -89,7 +109,7 @@ export const EditRuleComponent = memo(() => {
content: (
<>
-
+
{myDefineRuleForm.data != null && (
{
content: (
<>
-
+
{myAboutRuleForm.data != null && (
{
content: (
<>
-
+
{myScheduleRuleForm.data != null && (
{
],
[
loading,
+ initLoading,
isLoading,
myAboutRuleForm,
myDefineRuleForm,
@@ -310,7 +331,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 47b5c1051bcf..cc0882dd7e42 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 ef67f0a7d22c..dd46b33ca725 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,15 +18,34 @@ 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';
export const RulesComponent = React.memo(() => {
const [showImportModal, setShowImportModal] = useState(false);
const [importCompleteToggle, setImportCompleteToggle] = useState(false);
+ const {
+ loading,
+ isSignalIndexExists,
+ isAuthenticated,
+ canUserCRUD,
+ hasManageApiKey,
+ } = useUserInfo();
+ if (
+ isSignalIndexExists != null &&
+ isAuthenticated != null &&
+ (!isSignalIndexExists || !isAuthenticated)
+ ) {
+ return ;
+ }
+ const userHasNoPermissions =
+ canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false;
const lastCompletedRun = undefined;
return (
<>
+ {userHasNoPermissions && }
setShowImportModal(false)}
@@ -56,6 +76,7 @@ export const RulesComponent = React.memo(() => {
{
setShowImportModal(true);
}}
@@ -63,20 +84,23 @@ export const RulesComponent = React.memo(() => {
{i18n.IMPORT_RULE}
-
{i18n.ADD_NEW_RULE}
-
-
+
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 ec4206623bad..541b058951be 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[];
}
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 000000000000..cb358c15e5fa
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.test.ts
@@ -0,0 +1,44 @@
+/*
+ * 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 {
+ status: number = -1;
+ constructor(status: number, message: string) {
+ super(message);
+ this.status = status;
+ }
+}
+
+describe('get_index_exists', () => {
+ 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');
+ });
+ 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');
+ });
+});
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 ff65caa59a86..705f542b5012 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 },
+ {},
+ { _shards: { total: number } }
+ >,
index: string
): Promise => {
- return callWithRequest('indices.exists', {
- index,
- });
+ try {
+ const response = await callWithRequest('search', {
+ index,
+ size: 0,
+ terminate_after: 1,
+ allow_no_indices: true,
+ });
+ return response._shards.total > 0;
+ } catch (err) {
+ if (err.status === 404) {
+ return false;
+ } else {
+ throw err;
+ }
+ }
};
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 094449a5f61a..10dc14f7ed61 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);
diff --git a/x-pack/legacy/plugins/siem/server/plugin.ts b/x-pack/legacy/plugins/siem/server/plugin.ts
index 8a47aa2a2708..90ae79ef19d5 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'],