diff --git a/x-pack/legacy/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap index 9b6bfb1752a2..87409c5fdebe 100644 --- a/x-pack/legacy/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap @@ -3,8 +3,12 @@ exports[`renders correctly 1`] = ` - + + ( title={

{title}

} body={message &&

{message}

} actions={ - - + + ( {actionSecondaryLabel && actionSecondaryUrl && ( - + => { - try { - const text = await response.text(); - return JSON.parse(text); - } catch (error) { - return null; - } -}; - export const tryParseResponse = (response: string): string => { try { return JSON.stringify(JSON.parse(response), null, 2); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts index f6274db4a9da..e69bbfe1925f 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts @@ -10,14 +10,27 @@ import { throwIfNotOk } from '../../../hooks/api/api'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL, DETECTION_ENGINE_SIGNALS_STATUS_URL, + DETECTION_ENGINE_INDEX_URL, + DETECTION_ENGINE_PRIVILEGES_URL, } from '../../../../common/constants'; -import { QuerySignals, SignalSearchResponse, UpdateSignalStatusProps } from './types'; +import { + QuerySignals, + SignalSearchResponse, + UpdateSignalStatusProps, + SignalsIndex, + SignalIndexError, + Privilege, + PostSignalError, + BasicSignals, +} from './types'; +import { parseJsonFromBody } from '../../../utils/api'; /** * Fetch Signals by providing a query * * @param query String to match a dsl * @param kbnVersion current Kibana Version to use for headers + * @param signal AbortSignal for cancelling request */ export const fetchQuerySignals = async ({ query, @@ -46,7 +59,7 @@ export const fetchQuerySignals = async ({ * @param query of signals to update * @param status to update to('open' / 'closed') * @param kbnVersion current Kibana Version to use for headers - * @param signal to cancel request + * @param signal AbortSignal for cancelling request */ export const updateSignalStatus = async ({ query, @@ -69,3 +82,90 @@ export const updateSignalStatus = async ({ await throwIfNotOk(response); return response.json(); }; + +/** + * Fetch Signal Index + * + * @param kbnVersion current Kibana Version to use for headers + * @param signal AbortSignal for cancelling request + */ +export const getSignalIndex = async ({ + kbnVersion, + signal, +}: BasicSignals): Promise => { + const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_INDEX_URL}`, { + method: 'GET', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-version': kbnVersion, + 'kbn-xsrf': kbnVersion, + }, + signal, + }); + if (response.ok) { + const signalIndex = await response.json(); + return signalIndex; + } + const error = await parseJsonFromBody(response); + if (error != null) { + throw new SignalIndexError(error); + } + return null; +}; + +/** + * Get User Privileges + * + * @param kbnVersion current Kibana Version to use for headers + * @param signal AbortSignal for cancelling request + */ +export const getUserPrivilege = async ({ + kbnVersion, + signal, +}: BasicSignals): Promise => { + const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_PRIVILEGES_URL}`, { + method: 'GET', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-version': kbnVersion, + 'kbn-xsrf': kbnVersion, + }, + signal, + }); + + await throwIfNotOk(response); + return response.json(); +}; + +/** + * Create Signal Index if needed it + * + * @param kbnVersion current Kibana Version to use for headers + * @param signal AbortSignal for cancelling request + */ +export const createSignalIndex = async ({ + kbnVersion, + signal, +}: BasicSignals): Promise => { + const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_INDEX_URL}`, { + method: 'POST', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-version': kbnVersion, + 'kbn-xsrf': kbnVersion, + }, + signal, + }); + if (response.ok) { + const signalIndex = await response.json(); + return signalIndex; + } + const error = await parseJsonFromBody(response); + if (error != null) { + throw new PostSignalError(error); + } + return null; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/errors_types/get_index_error.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/errors_types/get_index_error.ts new file mode 100644 index 000000000000..e4f2a658b736 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/errors_types/get_index_error.ts @@ -0,0 +1,24 @@ +/* + * 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 { MessageBody } from '../../../../components/ml/api/throw_if_not_ok'; + +export class SignalIndexError extends Error { + message: string = ''; + statusCode: number = -1; + error: string = ''; + + constructor(errObj: MessageBody) { + super(errObj.message); + this.message = errObj.message ?? ''; + this.statusCode = errObj.statusCode ?? -1; + this.error = errObj.error ?? ''; + this.name = 'SignalIndexError'; + + // Set the prototype explicitly. + Object.setPrototypeOf(this, SignalIndexError.prototype); + } +} diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/errors_types/index.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/errors_types/index.ts new file mode 100644 index 000000000000..4ce8e6ba8918 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/errors_types/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export * from './get_index_error'; +export * from './post_index_error'; +export * from './privilege_user_error'; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/errors_types/post_index_error.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/errors_types/post_index_error.ts new file mode 100644 index 000000000000..d6d8cccfb454 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/errors_types/post_index_error.ts @@ -0,0 +1,24 @@ +/* + * 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 { MessageBody } from '../../../../components/ml/api/throw_if_not_ok'; + +export class PostSignalError extends Error { + message: string = ''; + statusCode: number = -1; + error: string = ''; + + constructor(errObj: MessageBody) { + super(errObj.message); + this.message = errObj.message ?? ''; + this.statusCode = errObj.statusCode ?? -1; + this.error = errObj.error ?? ''; + this.name = 'PostSignalError'; + + // Set the prototype explicitly. + Object.setPrototypeOf(this, PostSignalError.prototype); + } +} diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/errors_types/privilege_user_error.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/errors_types/privilege_user_error.ts new file mode 100644 index 000000000000..5cd458a7fe9a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/errors_types/privilege_user_error.ts @@ -0,0 +1,24 @@ +/* + * 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 { MessageBody } from '../../../../components/ml/api/throw_if_not_ok'; + +export class PrivilegeUserError extends Error { + message: string = ''; + statusCode: number = -1; + error: string = ''; + + constructor(errObj: MessageBody) { + super(errObj.message); + this.message = errObj.message ?? ''; + this.statusCode = errObj.statusCode ?? -1; + this.error = errObj.error ?? ''; + this.name = 'PrivilegeUserError'; + + // Set the prototype explicitly. + Object.setPrototypeOf(this, PrivilegeUserError.prototype); + } +} diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts index 5846f3275c0f..118c2b367ca5 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts @@ -4,11 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface QuerySignals { - query: string; +export * from './errors_types'; + +export interface BasicSignals { kbnVersion: string; signal: AbortSignal; } +export interface QuerySignals extends BasicSignals { + query: string; +} export interface SignalsResponse { took: number; @@ -38,3 +42,60 @@ export interface UpdateSignalStatusProps { kbnVersion: string; signal?: AbortSignal; // TODO: implement cancelling } + +export interface SignalsIndex { + name: string; +} + +export interface Privilege { + username: string; + has_all_requested: boolean; + cluster: { + monitor_ml: boolean; + manage_ccr: boolean; + manage_index_templates: boolean; + monitor_watcher: boolean; + monitor_transform: boolean; + read_ilm: boolean; + manage_api_key: boolean; + manage_security: boolean; + manage_own_api_key: boolean; + manage_saml: boolean; + all: boolean; + manage_ilm: boolean; + manage_ingest_pipelines: boolean; + read_ccr: boolean; + manage_rollup: boolean; + monitor: boolean; + manage_watcher: boolean; + manage: boolean; + manage_transform: boolean; + manage_token: boolean; + manage_ml: boolean; + manage_pipeline: boolean; + monitor_rollup: boolean; + transport_client: boolean; + create_snapshot: boolean; + }; + index: { + [indexName: string]: { + all: boolean; + manage_ilm: boolean; + read: boolean; + create_index: boolean; + read_cross_cluster: boolean; + index: boolean; + monitor: boolean; + delete: boolean; + manage: boolean; + delete_index: boolean; + create_doc: boolean; + view_index_metadata: boolean; + create: boolean; + manage_follow_index: boolean; + manage_leader_index: boolean; + write: boolean; + }; + }; + isAuthenticated: boolean; +} 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 new file mode 100644 index 000000000000..6f897703059f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx @@ -0,0 +1,63 @@ +/* + * 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 { useEffect, useState } from 'react'; + +import { DEFAULT_KBN_VERSION } from '../../../../common/constants'; +import { useUiSetting$ } from '../../../lib/kibana'; +import { getUserPrivilege } from './api'; + +type Return = [boolean, boolean | null, boolean | null]; + +/** + * Hook to get user privilege from + * + */ +export const usePrivilegeUser = (): Return => { + const [loading, setLoading] = useState(true); + const [isAuthenticated, setAuthenticated] = useState(null); + const [hasWrite, setHasWrite] = useState(null); + const [kbnVersion] = useUiSetting$(DEFAULT_KBN_VERSION); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + setLoading(true); + + async function fetchData() { + try { + const privilege = await getUserPrivilege({ + kbnVersion, + signal: abortCtrl.signal, + }); + + if (isSubscribed && privilege != null) { + 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); + } + } + } catch (error) { + if (isSubscribed) { + setAuthenticated(false); + setHasWrite(false); + } + } + if (isSubscribed) { + setLoading(false); + } + } + + fetchData(); + return () => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, []); + + return [loading, isAuthenticated, hasWrite]; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.tsx index d86562528855..9501f1189a48 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.tsx @@ -8,11 +8,8 @@ import { useEffect, useState } from 'react'; import { useUiSetting$ } from '../../../lib/kibana'; import { DEFAULT_KBN_VERSION } from '../../../../common/constants'; -import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; -import { useStateToaster } from '../../../components/toasters'; import { fetchQuerySignals } from './api'; -import * as i18n from './translations'; import { SignalSearchResponse } from './types'; type Return = [boolean, SignalSearchResponse | null]; @@ -27,7 +24,6 @@ export const useQuerySignals = (query: string): Return => const [signals, setSignals] = useState | null>(null); const [loading, setLoading] = useState(true); const [kbnVersion] = useUiSetting$(DEFAULT_KBN_VERSION); - const [, dispatchToaster] = useStateToaster(); useEffect(() => { let isSubscribed = true; @@ -48,7 +44,6 @@ export const useQuerySignals = (query: string): Return => } catch (error) { if (isSubscribed) { setSignals(null); - errorToToaster({ title: i18n.SIGNAL_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 new file mode 100644 index 000000000000..347c90fa3b41 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx @@ -0,0 +1,99 @@ +/* + * 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 { useEffect, useState, useRef } from 'react'; + +import { DEFAULT_KBN_VERSION } from '../../../../common/constants'; +import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; +import { useStateToaster } from '../../../components/toasters'; +import { useUiSetting$ } from '../../../lib/kibana'; +import { createSignalIndex, getSignalIndex } from './api'; +import * as i18n from './translations'; +import { PostSignalError } from './types'; + +type Func = () => void; + +type Return = [boolean, boolean | null, string | null, Func | null]; + +/** + * Hook for managing signal index + * + * + */ +export const useSignalIndex = (): Return => { + const [loading, setLoading] = useState(true); + const [signalIndexName, setSignalIndexName] = useState(null); + const [signalIndexExists, setSignalIndexExists] = useState(null); + const createDeSignalIndex = useRef(null); + const [kbnVersion] = useUiSetting$(DEFAULT_KBN_VERSION); + const [, dispatchToaster] = useStateToaster(); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + + const fetchData = async () => { + try { + setLoading(true); + const signal = await getSignalIndex({ + kbnVersion, + signal: abortCtrl.signal, + }); + + if (isSubscribed && signal != null) { + setSignalIndexName(signal.name); + setSignalIndexExists(true); + } + } catch (error) { + if (isSubscribed) { + setSignalIndexName(null); + setSignalIndexExists(false); + } + } + if (isSubscribed) { + setLoading(false); + } + }; + + const createIndex = async () => { + let isFetchingData = false; + try { + setLoading(true); + await createSignalIndex({ + kbnVersion, + signal: abortCtrl.signal, + }); + + if (isSubscribed) { + isFetchingData = true; + fetchData(); + } + } catch (error) { + if (isSubscribed) { + if (error instanceof PostSignalError && error.statusCode === 409) { + fetchData(); + } else { + setSignalIndexName(null); + setSignalIndexExists(false); + errorToToaster({ title: i18n.SIGNAL_FETCH_FAILURE, error, dispatchToaster }); + } + } + } + if (isSubscribed && !isFetchingData) { + setLoading(false); + } + }; + + fetchData(); + createDeSignalIndex.current = createIndex; + return () => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, []); + + return [loading, signalIndexExists, signalIndexName, createDeSignalIndex.current]; +}; 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 aeb5e677374f..f9e80334a888 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 @@ -26,7 +26,7 @@ import { SignalsTableFilterGroup, } from './signals_filter_group'; import { useKibana, useUiSetting$ } from '../../../../lib/kibana'; -import { DEFAULT_KBN_VERSION, DEFAULT_SIGNALS_INDEX } from '../../../../../common/constants'; +import { DEFAULT_KBN_VERSION } from '../../../../../common/constants'; import { defaultHeaders } from '../../../../components/timeline/body/column_headers/default_headers'; import { ColumnHeader } from '../../../../components/timeline/body/column_headers/column_header'; import { esFilters, esQuery } from '../../../../../../../../../src/plugins/data/common/es_query'; @@ -91,6 +91,7 @@ interface DispatchProps { interface OwnProps { defaultFilters?: esFilters.Filter[]; from: number; + signalsIndex: string; to: number; } @@ -112,15 +113,14 @@ export const SignalsTableComponent = React.memo( selectedEventIds, setEventsDeleted, setEventsLoading, + signalsIndex, to, }) => { const [selectAll, setSelectAll] = useState(false); const [showClearSelectionAction, setShowClearSelectionAction] = useState(false); const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); - const [{ browserFields, indexPatterns }] = useFetchIndexPatterns([ - `${DEFAULT_SIGNALS_INDEX}-default`, - ]); // TODO Get from new FrankInspired XavierHook + const [{ browserFields, indexPatterns }] = useFetchIndexPatterns([signalsIndex]); const [kbnVersion] = useUiSetting$(DEFAULT_KBN_VERSION); const kibana = useKibana(); @@ -266,9 +266,7 @@ export const SignalsTableComponent = React.memo( [createTimelineCallback, filterGroup, kbnVersion] ); - const defaultIndices = useMemo(() => [`${DEFAULT_SIGNALS_INDEX}-default`], [ - `${DEFAULT_SIGNALS_INDEX}-default`, - ]); + const defaultIndices = useMemo(() => [signalsIndex], [signalsIndex]); const defaultFiltersMemo = useMemo( () => [ ...defaultFilters, @@ -292,7 +290,7 @@ export const SignalsTableComponent = React.memo( return ( { const [lastSignals, setLastSignals] = useState( ); - const [totalSignals, setTotalSignals] = useState( + const [totalSignals, setTotalSignals] = useState( ); @@ -33,7 +33,7 @@ export const useSignalInfo = ({ ruleId = null }: SignalInfo): Return => { query = ''; } - const [, signals] = useQuerySignals(query); + const [loading, signals] = useQuerySignals(query); useEffect(() => { if (signals != null) { @@ -46,8 +46,11 @@ export const useSignalInfo = ({ ruleId = null }: SignalInfo): Return => { ) : null ); setTotalSignals(<>{mySignals.hits.total.value}); + } else { + setLastSignals(null); + setTotalSignals(null); } - }, [signals]); + }, [loading, signals]); return [lastSignals, totalSignals]; }; 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 6e6b71729b07..8e5c3e9f1311 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 } from '@elastic/eui'; +import { EuiButton, EuiSpacer, EuiPanel, EuiLoadingContent } from '@elastic/eui'; import React from 'react'; import { StickyContainer } from 'react-sticky'; @@ -17,59 +17,101 @@ 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'; -export const DetectionEngineComponent = React.memo(() => { - const [lastSignals] = useSignalInfo({}); - return ( - <> - - {({ indicesExist, indexPattern }) => { - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - - +interface DetectionEngineComponentProps { + loading: boolean; + isSignalIndexExists: boolean | null; + isUserAuthenticated: boolean | null; + signalsIndex: string | null; +} - - - {i18n.LAST_SIGNAL} - {': '} - {lastSignals} - - ) - } - title={i18n.PAGE_TITLE} - > - - {i18n.BUTTON_MANAGE_RULES} - - +export const DetectionEngineComponent = React.memo( + ({ loading, isSignalIndexExists, isUserAuthenticated, signalsIndex }) => { + const [lastSignals] = useSignalInfo({}); + if (isUserAuthenticated != null && !isUserAuthenticated && !loading) { + return ( + + + + + ); + } + if (isSignalIndexExists != null && !isSignalIndexExists && !loading) { + return ( + + + + + ); + } + return ( + <> + + {({ indicesExist, indexPattern }) => { + return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + + + + + + + + {i18n.LAST_SIGNAL} + {': '} + {lastSignals} + + ) + } + title={i18n.PAGE_TITLE} + > + + {i18n.BUTTON_MANAGE_RULES} + + - + - - {({ to, from }) => } + + + {({ to, from }) => + !loading ? ( + isSignalIndexExists && ( + + ) + ) : ( + + + + + ) + } + + + + ) : ( + + + - - ) : ( - - - - - ); - }} - + ); + }} + - - - ); -}); + + + ); + } +); DetectionEngineComponent.displayName = 'DetectionEngineComponent'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.tsx new file mode 100644 index 000000000000..713bd6239d80 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.tsx @@ -0,0 +1,25 @@ +/* + * 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 React from 'react'; +import { documentationLinks } from 'ui/documentation_links'; + +import { EmptyPage } from '../../components/empty_page'; +import * as i18n from './translations'; + +export const DetectionEngineNoIndex = React.memo(() => ( + +)); + +DetectionEngineNoIndex.displayName = 'DetectionEngineNoIndex'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.tsx new file mode 100644 index 000000000000..bd3876b810a7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.tsx @@ -0,0 +1,25 @@ +/* + * 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 React from 'react'; +import { documentationLinks } from 'ui/documentation_links'; + +import { EmptyPage } from '../../components/empty_page'; +import * as i18n from './translations'; + +export const DetectionEngineUserUnauthenticated = React.memo(() => ( + +)); + +DetectionEngineUserUnauthenticated.displayName = 'DetectionEngineUserUnauthenticated'; 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 21ebac2b4d33..e8a2c98a94a5 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,9 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useEffect } 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 { DetectionEngineComponent } from './detection_engine'; import { EditRuleComponent } from './rules/edit'; @@ -17,29 +20,61 @@ const detectionEnginePath = `/:pageName(detection-engine)`; type Props = Partial> & { url: string }; -export const DetectionEngineContainer = React.memo(() => ( - - - - - - - - - - - - - - - - - ( - +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 ( + + + + + {isSignalIndexExists && isAuthenticated && ( + <> + + + + + + + + + + + + + )} - /> - -)); + + ( + + )} + /> + + ); +}); DetectionEngineContainer.displayName = 'DetectionEngineContainer'; 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 b0cf183949dd..1bc2bc24517e 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 @@ -40,7 +40,11 @@ import * as ruleI18n from '../translations'; import * as i18n from './translations'; import { GlobalTime } from '../../../../containers/global_time'; -export const RuleDetailsComponent = memo(() => { +interface RuleDetailsComponentProps { + signalsIndex: string | null; +} + +export const RuleDetailsComponent = memo(({ signalsIndex }) => { const { ruleId } = useParams(); const [loading, rule] = useRule(ruleId); const { aboutRuleData, defineRuleData, scheduleRuleData } = getStepsData({ @@ -200,7 +204,12 @@ export const RuleDetailsComponent = memo(() => { {ruleId != null && ( - + )} @@ -220,4 +229,5 @@ export const RuleDetailsComponent = memo(() => { ); }); + RuleDetailsComponent.displayName = 'RuleDetailsComponent'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts index c7a71bf48d7d..e5f830d3a49b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts @@ -51,3 +51,34 @@ export const EMPTY_ACTION_SECONDARY = i18n.translate( defaultMessage: 'Go to documentation', } ); + +export const NO_INDEX_TITLE = i18n.translate('xpack.siem.detectionEngine.noIndexTitle', { + defaultMessage: 'Let’s set up your detection engine', +}); + +export const NO_INDEX_MSG_BODY = i18n.translate('xpack.siem.detectionEngine.noIndexMsgBody', { + defaultMessage: + 'To use the detection engine, a user with the required cluster and index privileges must first access this page. For more help, contact your administrator.', +}); + +export const GO_TO_DOCUMENTATION = i18n.translate( + 'xpack.siem.detectionEngine.goToDocumentationButton', + { + defaultMessage: 'View documentation', + } +); + +export const USER_UNAUTHENTICATED_TITLE = i18n.translate( + 'xpack.siem.detectionEngine.userUnauthenticatedTitle', + { + defaultMessage: 'Detection engine permissions required', + } +); + +export const USER_UNAUTHENTICATED_MSG_BODY = i18n.translate( + 'xpack.siem.detectionEngine.userUnauthenticatedMsgBody', + { + defaultMessage: + 'You do not have the required permissions for viewing the detection engine. For more help, contact your administrator.', + } +); diff --git a/x-pack/legacy/plugins/siem/public/utils/api/index.ts b/x-pack/legacy/plugins/siem/public/utils/api/index.ts new file mode 100644 index 000000000000..1dc14413b04d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/utils/api/index.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export interface MessageBody { + error?: string; + message?: string; + statusCode?: number; +} + +export const parseJsonFromBody = async (response: Response): Promise => { + try { + const text = await response.text(); + return JSON.parse(text); + } catch (error) { + return null; + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 66e1d29b8085..2e16f209acfb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -381,4 +381,5 @@ export const getMockPrivileges = () => ({ }, }, application: {}, + isAuthenticated: false, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts index 457de05674f6..240200af8b58 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts @@ -5,6 +5,7 @@ */ import Hapi from 'hapi'; +import { merge } from 'lodash/fp'; import { DETECTION_ENGINE_PRIVILEGES_URL } from '../../../../../common/constants'; import { RulesRequest } from '../../rules/types'; import { ServerFacade } from '../../../../types'; @@ -28,7 +29,9 @@ export const createReadPrivilegesRulesRoute = (server: ServerFacade): Hapi.Serve const callWithRequest = callWithRequestFactory(request, server); const index = getIndex(request, server); const permissions = await readPrivileges(callWithRequest, index); - return permissions; + return merge(permissions, { + isAuthenticated: request?.auth?.isAuthenticated ?? false, + }); } catch (err) { return transformError(err); }