[SIEM] Detections create prepackage rules (#55403) (#55503)

* update extra action on rule detail to match design

* remove experimental label

* allow pre-package to be deleted + do not allow wrong user to create pre-packages rules

* Additional look back minimum value to 1

* fix flow with edit rule

* add success toaster when rule is created or updated

* Fix Timeline selector loading

* review ben doc + change detectin engine to detection even in url

* Succeeded text size consistency in rule details page

* fix description of threats

* fix test

* fix type

* fix internatinalization

* adding pre-packaged rules

* fix bug + enhance ux

* unified icon

* fix i18n

* fix bugs

* review I

* review II

* add border back
This commit is contained in:
Xavier Mouligneau 2020-01-21 21:54:55 -05:00 committed by GitHub
parent b4f15c6346
commit bfe9b45a55
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 865 additions and 359 deletions

View file

@ -60,7 +60,8 @@ export const DETECTION_ENGINE_PREPACKAGED_URL = `${DETECTION_ENGINE_RULES_URL}/p
export const DETECTION_ENGINE_PRIVILEGES_URL = `${DETECTION_ENGINE_URL}/privileges`;
export const DETECTION_ENGINE_INDEX_URL = `${DETECTION_ENGINE_URL}/index`;
export const DETECTION_ENGINE_TAGS_URL = `${DETECTION_ENGINE_URL}/tags`;
export const DETECTION_ENGINE_RULES_STATUS = `${DETECTION_ENGINE_URL}/rules/_find_statuses`;
export const DETECTION_ENGINE_RULES_STATUS_URL = `${DETECTION_ENGINE_RULES_URL}/_find_statuses`;
export const DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL = `${DETECTION_ENGINE_RULES_URL}/prepackaged/_status`;
/**
* Default signals index key for kibana.dev.yml

View file

@ -26,7 +26,8 @@ import { throwIfNotOk } from '../../../hooks/api/api';
import {
DETECTION_ENGINE_RULES_URL,
DETECTION_ENGINE_PREPACKAGED_URL,
DETECTION_ENGINE_RULES_STATUS,
DETECTION_ENGINE_RULES_STATUS_URL,
DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL,
} from '../../../../common/constants';
import * as i18n from '../../../pages/detection_engine/rules/translations';
@ -63,7 +64,7 @@ export const addRule = async ({ rule, signal }: AddRulesProps): Promise<NewRule>
export const fetchRules = async ({
filterOptions = {
filter: '',
sortField: 'enabled',
sortField: 'name',
sortOrder: 'desc',
},
pagination = {
@ -313,6 +314,7 @@ export const exportRules = async ({
* Get Rule Status provided Rule ID
*
* @param id string of Rule ID's (not rule_id)
* @param signal AbortSignal for cancelling request
*
* @throws An error if response is not OK
*/
@ -324,7 +326,7 @@ export const getRuleStatusById = async ({
signal: AbortSignal;
}): Promise<Record<string, RuleStatus>> => {
const response = await fetch(
`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_STATUS}?ids=${encodeURIComponent(
`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_STATUS_URL}?ids=${encodeURIComponent(
JSON.stringify([id])
)}`,
{
@ -341,3 +343,36 @@ export const getRuleStatusById = async ({
await throwIfNotOk(response);
return response.json();
};
/**
* Get pre packaged rules Status
*
* @param signal AbortSignal for cancelling request
*
* @throws An error if response is not OK
*/
export const getPrePackagedRulesStatus = async ({
signal,
}: {
signal: AbortSignal;
}): Promise<{
rules_installed: number;
rules_not_installed: number;
rules_not_updated: number;
}> => {
const response = await fetch(
`${chrome.getBasePath()}${DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL}`,
{
method: 'GET',
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
'kbn-xsrf': 'true',
},
signal,
}
);
await throwIfNotOk(response);
return response.json();
};

View file

@ -10,4 +10,5 @@ export * from './persist_rule';
export * from './types';
export * from './use_rule';
export * from './use_rules';
export * from './use_pre_packaged_rules';
export * from './use_rule_status';

View file

@ -16,3 +16,17 @@ export const RULE_ADD_FAILURE = i18n.translate(
defaultMessage: 'Failed to add Rule',
}
);
export const RULE_PREPACKAGED_FAILURE = i18n.translate(
'xpack.siem.containers.detectionEngine.createPrePackagedRuleFailDescription',
{
defaultMessage: 'Failed to installed pre-packaged rules from elastic',
}
);
export const RULE_PREPACKAGED_SUCCESS = i18n.translate(
'xpack.siem.containers.detectionEngine.createPrePackagedRuleSuccesDescription',
{
defaultMessage: 'Installed pre-packaged rules from elastic',
}
);

View file

@ -1,81 +0,0 @@
/*
* 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 { createPrepackagedRules } from './api';
type Return = [boolean, boolean | null];
interface UseCreatePackagedRules {
canUserCRUD: boolean | null;
hasIndexManage: boolean | null;
hasManageApiKey: boolean | null;
isAuthenticated: boolean | null;
isSignalIndexExists: boolean | null;
}
/**
* Hook for creating the packages rules
*
* @param canUserCRUD boolean
* @param hasIndexManage boolean
* @param hasManageApiKey boolean
* @param isAuthenticated boolean
* @param isSignalIndexExists boolean
*
* @returns [loading, hasCreatedPackageRules]
*/
export const useCreatePackagedRules = ({
canUserCRUD,
hasIndexManage,
hasManageApiKey,
isAuthenticated,
isSignalIndexExists,
}: UseCreatePackagedRules): Return => {
const [hasCreatedPackageRules, setHasCreatedPackageRules] = useState<boolean | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let isSubscribed = true;
const abortCtrl = new AbortController();
setLoading(true);
async function createRules() {
try {
await createPrepackagedRules({
signal: abortCtrl.signal,
});
if (isSubscribed) {
setHasCreatedPackageRules(true);
}
} catch (error) {
if (isSubscribed) {
setHasCreatedPackageRules(false);
}
}
if (isSubscribed) {
setLoading(false);
}
}
if (
canUserCRUD &&
hasIndexManage &&
hasManageApiKey &&
isAuthenticated &&
isSignalIndexExists
) {
createRules();
}
return () => {
isSubscribed = false;
abortCtrl.abort();
};
}, [canUserCRUD, hasIndexManage, hasManageApiKey, isAuthenticated, isSignalIndexExists]);
return [loading, hasCreatedPackageRules];
};

View file

@ -0,0 +1,166 @@
/*
* 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 { useStateToaster, displaySuccessToast } from '../../../components/toasters';
import { errorToToaster } from '../../../components/ml/api/error_to_toaster';
import { getPrePackagedRulesStatus, createPrepackagedRules } from './api';
import * as i18n from './translations';
type Func = () => void;
export type CreatePreBuiltRules = () => Promise<boolean>;
interface Return {
createPrePackagedRules: null | CreatePreBuiltRules;
loading: boolean;
loadingCreatePrePackagedRules: boolean;
refetchPrePackagedRulesStatus: Func | null;
rulesInstalled: number | null;
rulesNotInstalled: number | null;
rulesNotUpdated: number | null;
}
interface UsePrePackagedRuleProps {
canUserCRUD: boolean | null;
hasIndexManage: boolean | null;
hasManageApiKey: boolean | null;
isAuthenticated: boolean | null;
isSignalIndexExists: boolean | null;
}
/**
* Hook for using to get status about pre-packaged Rules from the Detection Engine API
*
* @param hasIndexManage boolean
* @param hasManageApiKey boolean
* @param isAuthenticated boolean
* @param isSignalIndexExists boolean
*
*/
export const usePrePackagedRules = ({
canUserCRUD,
hasIndexManage,
hasManageApiKey,
isAuthenticated,
isSignalIndexExists,
}: UsePrePackagedRuleProps): Return => {
const [rulesInstalled, setRulesInstalled] = useState<number | null>(null);
const [rulesNotInstalled, setRulesNotInstalled] = useState<number | null>(null);
const [rulesNotUpdated, setRulesNotUpdated] = useState<number | null>(null);
const [loadingCreatePrePackagedRules, setLoadingCreatePrePackagedRules] = useState(false);
const [loading, setLoading] = useState(true);
const createPrePackagedRules = useRef<null | CreatePreBuiltRules>(null);
const refetchPrePackagedRules = useRef<Func | null>(null);
const [, dispatchToaster] = useStateToaster();
useEffect(() => {
let isSubscribed = true;
const abortCtrl = new AbortController();
const fetchPrePackagedRules = async () => {
try {
setLoading(true);
const prePackagedRuleStatusResponse = await getPrePackagedRulesStatus({
signal: abortCtrl.signal,
});
if (isSubscribed) {
setRulesInstalled(prePackagedRuleStatusResponse.rules_installed);
setRulesNotInstalled(prePackagedRuleStatusResponse.rules_not_installed);
setRulesNotUpdated(prePackagedRuleStatusResponse.rules_not_updated);
}
} catch (error) {
if (isSubscribed) {
setRulesInstalled(null);
setRulesNotInstalled(null);
setRulesNotUpdated(null);
errorToToaster({ title: i18n.RULE_FETCH_FAILURE, error, dispatchToaster });
}
}
if (isSubscribed) {
setLoading(false);
}
};
const createElasticRules = async (): Promise<boolean> => {
return new Promise(async resolve => {
try {
if (
canUserCRUD &&
hasIndexManage &&
hasManageApiKey &&
isAuthenticated &&
isSignalIndexExists
) {
setLoadingCreatePrePackagedRules(true);
await createPrepackagedRules({
signal: abortCtrl.signal,
});
if (isSubscribed) {
let iterationTryOfFetchingPrePackagedCount = 0;
let timeoutId = -1;
const stopTimeOut = () => {
if (timeoutId !== -1) {
window.clearTimeout(timeoutId);
}
};
const reFetch = () =>
window.setTimeout(async () => {
iterationTryOfFetchingPrePackagedCount =
iterationTryOfFetchingPrePackagedCount + 1;
const prePackagedRuleStatusResponse = await getPrePackagedRulesStatus({
signal: abortCtrl.signal,
});
if (
isSubscribed &&
((prePackagedRuleStatusResponse.rules_not_installed === 0 &&
prePackagedRuleStatusResponse.rules_not_updated === 0) ||
iterationTryOfFetchingPrePackagedCount > 100)
) {
setLoadingCreatePrePackagedRules(false);
setRulesInstalled(prePackagedRuleStatusResponse.rules_installed);
setRulesNotInstalled(prePackagedRuleStatusResponse.rules_not_installed);
setRulesNotUpdated(prePackagedRuleStatusResponse.rules_not_updated);
displaySuccessToast(i18n.RULE_PREPACKAGED_SUCCESS, dispatchToaster);
stopTimeOut();
resolve(true);
} else {
timeoutId = reFetch();
}
}, 300);
timeoutId = reFetch();
}
}
} catch (error) {
if (isSubscribed) {
setLoadingCreatePrePackagedRules(false);
errorToToaster({ title: i18n.RULE_PREPACKAGED_FAILURE, error, dispatchToaster });
resolve(false);
}
}
});
};
fetchPrePackagedRules();
createPrePackagedRules.current = createElasticRules;
refetchPrePackagedRules.current = fetchPrePackagedRules;
return () => {
isSubscribed = false;
abortCtrl.abort();
};
}, [canUserCRUD, hasIndexManage, hasManageApiKey, isAuthenticated, isSignalIndexExists]);
return {
loading,
loadingCreatePrePackagedRules,
refetchPrePackagedRulesStatus: refetchPrePackagedRules.current,
rulesInstalled,
rulesNotInstalled,
rulesNotUpdated,
createPrePackagedRules: createPrePackagedRules.current,
};
};

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { useEffect, useState } from 'react';
import { useEffect, useState, useRef } from 'react';
import { FetchRulesResponse, FilterOptions, PaginationOptions } from './types';
import { useStateToaster } from '../../../components/toasters';
@ -12,36 +12,33 @@ import { fetchRules } from './api';
import { errorToToaster } from '../../../components/ml/api/error_to_toaster';
import * as i18n from './translations';
type Return = [boolean, FetchRulesResponse];
type Func = () => void;
type Return = [boolean, FetchRulesResponse, Func | null];
/**
* Hook for using the list of Rules from the Detection Engine API
*
* @param pagination desired pagination options (e.g. page/perPage)
* @param filterOptions desired filters (e.g. filter/sortField/sortOrder)
* @param refetchToggle toggle for refetching data
*/
export const useRules = (
pagination: PaginationOptions,
filterOptions: FilterOptions,
refetchToggle: boolean
): Return => {
export const useRules = (pagination: PaginationOptions, filterOptions: FilterOptions): Return => {
const [rules, setRules] = useState<FetchRulesResponse>({
page: 1,
perPage: 20,
total: 0,
data: [],
});
const reFetchRules = useRef<Func | null>(null);
const [loading, setLoading] = useState(true);
const [, dispatchToaster] = useStateToaster();
useEffect(() => {
let isSubscribed = true;
const abortCtrl = new AbortController();
setLoading(true);
async function fetchData() {
try {
setLoading(true);
const fetchRulesResult = await fetchRules({
filterOptions,
pagination,
@ -62,12 +59,12 @@ export const useRules = (
}
fetchData();
reFetchRules.current = fetchData;
return () => {
isSubscribed = false;
abortCtrl.abort();
};
}, [
refetchToggle,
pagination.page,
pagination.perPage,
filterOptions.filter,
@ -75,5 +72,5 @@ export const useRules = (
filterOptions.sortOrder,
]);
return [loading, rules];
return [loading, rules, reFetchRules.current];
};

View file

@ -59,6 +59,7 @@ export const usePrivilegeUser = (): Return => {
setAuthenticated(false);
setHasIndexManage(false);
setHasIndexWrite(false);
setHasManageApiKey(false);
errorToToaster({ title: i18n.PRIVILEGE_FETCH_FAILURE, error, dispatchToaster });
}
}

View file

@ -10,7 +10,6 @@ import React, { useEffect, useReducer, Dispatch, createContext, useContext } fro
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';
import { useCreatePackagedRules } from '../../../../containers/detection_engine/rules/use_create_packaged_rules';
export interface State {
canUserCRUD: boolean | null;
@ -162,14 +161,6 @@ export const useUserInfo = (): State => {
createSignalIndex,
] = useSignalIndex();
useCreatePackagedRules({
canUserCRUD,
hasIndexManage,
hasManageApiKey,
isAuthenticated,
isSignalIndexExists,
});
const uiCapabilities = useKibana().services.application.capabilities;
const capabilitiesCanUserCRUD: boolean =
typeof uiCapabilities.siem.crud === 'boolean' ? uiCapabilities.siem.crud : false;

View file

@ -90,23 +90,6 @@ const DetectionEngineComponent = React.memo<DetectionEngineComponentProps>(
[setAbsoluteRangeDatePicker]
);
if (isUserAuthenticated != null && !isUserAuthenticated && !loading) {
return (
<WrapperPage>
<HeaderPage border title={i18n.PAGE_TITLE} />
<DetectionEngineUserUnauthenticated />
</WrapperPage>
);
}
if (isSignalIndexExists != null && !isSignalIndexExists && !loading) {
return (
<WrapperPage>
<HeaderPage border title={i18n.PAGE_TITLE} />
<DetectionEngineNoIndex />
</WrapperPage>
);
}
const tabs = useMemo(
() => (
<EuiTabs>
@ -125,6 +108,23 @@ const DetectionEngineComponent = React.memo<DetectionEngineComponentProps>(
[detectionsTabs, tabName]
);
if (isUserAuthenticated != null && !isUserAuthenticated && !loading) {
return (
<WrapperPage>
<HeaderPage border title={i18n.PAGE_TITLE} />
<DetectionEngineUserUnauthenticated />
</WrapperPage>
);
}
if (isSignalIndexExists != null && !isSignalIndexExists && !loading) {
return (
<WrapperPage>
<HeaderPage border title={i18n.PAGE_TITLE} />
<DetectionEngineNoIndex />
</WrapperPage>
);
}
return (
<>
{hasIndexWrite != null && !hasIndexWrite && <NoWriteSignalsCallOut />}

View file

@ -64,13 +64,12 @@ export const deleteRulesAction = async (
onRuleDeleted?: () => void
) => {
try {
dispatch({ type: 'updateLoading', ids, isLoading: true });
dispatch({ type: 'loading', isLoading: true });
const response = await deleteRules({ ids });
const { rules, errors } = bucketRulesResponse(response);
dispatch({ type: 'deleteRules', rules });
const { errors } = bucketRulesResponse(response);
dispatch({ type: 'refresh' });
if (errors.length > 0) {
displayErrorToast(
i18n.BATCH_ACTION_DELETE_SELECTED_ERROR(ids.length),

View file

@ -11,10 +11,12 @@ import {
EuiLoadingContent,
EuiSpacer,
} from '@elastic/eui';
import { isEmpty } from 'lodash/fp';
import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react';
import { useHistory } from 'react-router-dom';
import uuid from 'uuid';
import { useRules, CreatePreBuiltRules } from '../../../../containers/detection_engine/rules';
import { HeaderSection } from '../../../../components/header_section';
import {
UtilityBar,
@ -23,16 +25,17 @@ import {
UtilityBarSection,
UtilityBarText,
} from '../../../../components/detection_engine/utility_bar';
import { getColumns } from './columns';
import { useRules } from '../../../../containers/detection_engine/rules';
import { useStateToaster } from '../../../../components/toasters';
import { Loader } from '../../../../components/loader';
import { Panel } from '../../../../components/panel';
import { getBatchItems } from './batch_actions';
import { EuiBasicTableOnChange, TableData } from '../types';
import { allRulesReducer, State } from './reducer';
import * as i18n from '../translations';
import { PrePackagedRulesPrompt } from '../components/pre_packaged_rules/load_empty_prompt';
import { RuleDownloader } from '../components/rule_downloader';
import { useStateToaster } from '../../../../components/toasters';
import { getPrePackagedRuleStatus } from '../helpers';
import * as i18n from '../translations';
import { EuiBasicTableOnChange, TableData } from '../types';
import { getBatchItems } from './batch_actions';
import { getColumns } from './columns';
import { allRulesReducer, State } from './reducer';
const initialState: State = {
isLoading: true,
@ -52,6 +55,19 @@ const initialState: State = {
},
};
interface AllRulesProps {
createPrePackagedRules: CreatePreBuiltRules | null;
hasNoPermissions: boolean;
importCompleteToggle: boolean;
loading: boolean;
loadingCreatePrePackagedRules: boolean;
refetchPrePackagedRulesStatus: () => void;
rulesInstalled: number | null;
rulesNotInstalled: number | null;
rulesNotUpdated: number | null;
setRefreshRulesData: (refreshRule: () => void) => void;
}
/**
* Table Component for displaying all Rules for a given cluster. Provides the ability to filter
* by name, sort by enabled, and perform the following actions:
@ -60,191 +76,248 @@ const initialState: State = {
* * Delete
* * Import/Export
*/
export const AllRules = React.memo<{
hasNoPermissions: boolean;
importCompleteToggle: boolean;
loading: boolean;
}>(({ hasNoPermissions, importCompleteToggle, loading }) => {
const [
{
exportPayload,
filterOptions,
isLoading,
refreshToggle,
selectedItems,
tableData,
pagination,
},
dispatch,
] = useReducer(allRulesReducer, initialState);
const history = useHistory();
const [isInitialLoad, setIsInitialLoad] = useState(true);
const [isLoadingRules, rulesData] = useRules(pagination, filterOptions, refreshToggle);
const [, dispatchToaster] = useStateToaster();
export const AllRules = React.memo<AllRulesProps>(
({
createPrePackagedRules,
hasNoPermissions,
importCompleteToggle,
loading,
loadingCreatePrePackagedRules,
refetchPrePackagedRulesStatus,
rulesInstalled,
rulesNotInstalled,
rulesNotUpdated,
setRefreshRulesData,
}) => {
const [
{
exportPayload,
filterOptions,
isLoading,
refreshToggle,
selectedItems,
tableData,
pagination,
},
dispatch,
] = useReducer(allRulesReducer, initialState);
const history = useHistory();
const [isInitialLoad, setIsInitialLoad] = useState(true);
const [isGlobalLoading, setIsGlobalLoad] = useState(false);
const [, dispatchToaster] = useStateToaster();
const [isLoadingRules, rulesData, reFetchRulesData] = useRules(pagination, filterOptions);
const getBatchItemsPopoverContent = useCallback(
(closePopover: () => void) => (
<EuiContextMenuPanel
items={getBatchItems(selectedItems, dispatch, dispatchToaster, history, closePopover)}
/>
),
[selectedItems, dispatch, dispatchToaster, history]
);
const prePackagedRuleStatus = getPrePackagedRuleStatus(
rulesInstalled,
rulesNotInstalled,
rulesNotUpdated
);
const tableOnChangeCallback = useCallback(
({ page, sort }: EuiBasicTableOnChange) => {
const getBatchItemsPopoverContent = useCallback(
(closePopover: () => void) => (
<EuiContextMenuPanel
items={getBatchItems(selectedItems, dispatch, dispatchToaster, history, closePopover)}
/>
),
[selectedItems, dispatch, dispatchToaster, history]
);
const tableOnChangeCallback = useCallback(
({ page, sort }: EuiBasicTableOnChange) => {
dispatch({
type: 'updatePagination',
pagination: { ...pagination, page: page.index + 1, perPage: page.size },
});
dispatch({
type: 'updateFilterOptions',
filterOptions: {
...filterOptions,
sortField: 'enabled', // Only enabled is supported for sorting currently
sortOrder: sort?.direction ?? 'desc',
},
});
},
[dispatch, filterOptions, pagination]
);
const columns = useMemo(() => {
return getColumns(dispatch, dispatchToaster, history, hasNoPermissions);
}, [dispatch, dispatchToaster, history]);
useEffect(() => {
dispatch({ type: 'loading', isLoading: isLoadingRules });
}, [isLoadingRules]);
useEffect(() => {
if (!isLoadingRules && !loading && isInitialLoad) {
setIsInitialLoad(false);
}
}, [isInitialLoad, isLoadingRules, loading]);
useEffect(() => {
if (!isGlobalLoading && (isLoadingRules || isLoading)) {
setIsGlobalLoad(true);
} else if (isGlobalLoading && !isLoadingRules && !isLoading) {
setIsGlobalLoad(false);
}
}, [setIsGlobalLoad, isGlobalLoading, isLoadingRules, isLoading]);
useEffect(() => {
if (!isInitialLoad) {
dispatch({ type: 'refresh' });
}
}, [importCompleteToggle]);
useEffect(() => {
if (reFetchRulesData != null) {
reFetchRulesData();
}
refetchPrePackagedRulesStatus();
}, [refreshToggle, reFetchRulesData, refetchPrePackagedRulesStatus]);
useEffect(() => {
if (reFetchRulesData != null) {
setRefreshRulesData(reFetchRulesData);
}
}, [reFetchRulesData, setRefreshRulesData]);
useEffect(() => {
dispatch({
type: 'updatePagination',
pagination: { ...pagination, page: page.index + 1, perPage: page.size },
});
dispatch({
type: 'updateFilterOptions',
filterOptions: {
...filterOptions,
sortField: 'enabled', // Only enabled is supported for sorting currently
sortOrder: sort?.direction ?? 'desc',
type: 'updateRules',
rules: rulesData.data,
pagination: {
page: rulesData.page,
perPage: rulesData.perPage,
total: rulesData.total,
},
});
},
[dispatch, filterOptions, pagination]
);
}, [rulesData]);
const columns = useMemo(() => {
return getColumns(dispatch, dispatchToaster, history, hasNoPermissions);
}, [dispatch, dispatchToaster, history]);
const handleCreatePrePackagedRules = useCallback(async () => {
if (createPrePackagedRules != null) {
await createPrePackagedRules();
dispatch({ type: 'refresh' });
}
}, [createPrePackagedRules]);
useEffect(() => {
dispatch({ type: 'loading', isLoading: isLoadingRules });
const euiBasicTableSelectionProps = useMemo(
() => ({
selectable: (item: TableData) => !item.isLoading,
onSelectionChange: (selected: TableData[]) =>
dispatch({ type: 'setSelected', selectedItems: selected }),
}),
[]
);
if (!isLoadingRules) {
setIsInitialLoad(false);
}
}, [isLoadingRules]);
return (
<>
<RuleDownloader
filename={`${i18n.EXPORT_FILENAME}.ndjson`}
rules={exportPayload}
onExportComplete={exportCount => {
dispatchToaster({
type: 'addToaster',
toast: {
id: uuid.v4(),
title: i18n.SUCCESSFULLY_EXPORTED_RULES(exportCount),
color: 'success',
iconType: 'check',
},
});
}}
/>
<EuiSpacer />
useEffect(() => {
if (!isInitialLoad) {
dispatch({ type: 'refresh' });
}
}, [importCompleteToggle]);
useEffect(() => {
dispatch({
type: 'updateRules',
rules: rulesData.data,
pagination: {
page: rulesData.page,
perPage: rulesData.perPage,
total: rulesData.total,
},
});
}, [rulesData]);
const euiBasicTableSelectionProps = useMemo(
() => ({
selectable: (item: TableData) => !item.isLoading,
onSelectionChange: (selected: TableData[]) =>
dispatch({ type: 'setSelected', selectedItems: selected }),
}),
[]
);
return (
<>
<RuleDownloader
filename={`${i18n.EXPORT_FILENAME}.ndjson`}
rules={exportPayload}
onExportComplete={exportCount => {
dispatchToaster({
type: 'addToaster',
toast: {
id: uuid.v4(),
title: i18n.SUCCESSFULLY_EXPORTED_RULES(exportCount),
color: 'success',
iconType: 'check',
},
});
}}
/>
<EuiSpacer />
<Panel loading={isLoading}>
{isInitialLoad ? (
<EuiLoadingContent data-test-subj="initialLoadingPanelAllRulesTable" lines={10} />
) : (
<Panel loading={isGlobalLoading}>
<>
<HeaderSection split title={i18n.ALL_RULES}>
<EuiFieldSearch
aria-label={i18n.SEARCH_RULES}
fullWidth
incremental={false}
placeholder={i18n.SEARCH_PLACEHOLDER}
onSearch={filterString => {
dispatch({
type: 'updateFilterOptions',
filterOptions: {
...filterOptions,
filter: filterString,
},
});
dispatch({
type: 'updatePagination',
pagination: { ...pagination, page: 1 },
});
}}
/>
</HeaderSection>
<UtilityBar border>
<UtilityBarSection>
<UtilityBarGroup>
<UtilityBarText>{i18n.SHOWING_RULES(pagination.total ?? 0)}</UtilityBarText>
</UtilityBarGroup>
<UtilityBarGroup>
<UtilityBarText>{i18n.SELECTED_RULES(selectedItems.length)}</UtilityBarText>
{!hasNoPermissions && (
<UtilityBarAction
iconSide="right"
iconType="arrowDown"
popoverContent={getBatchItemsPopoverContent}
>
{i18n.BATCH_ACTIONS}
</UtilityBarAction>
)}
<UtilityBarAction
iconSide="right"
iconType="refresh"
onClick={() => dispatch({ type: 'refresh' })}
>
{i18n.REFRESH}
</UtilityBarAction>
</UtilityBarGroup>
</UtilityBarSection>
</UtilityBar>
<EuiBasicTable
columns={columns}
isSelectable={!hasNoPermissions ?? false}
itemId="id"
items={tableData}
onChange={tableOnChangeCallback}
pagination={{
pageIndex: pagination.page - 1,
pageSize: pagination.perPage,
totalItemCount: pagination.total,
pageSizeOptions: [5, 10, 20, 50, 100, 200, 300],
}}
sorting={{ sort: { field: 'activate', direction: filterOptions.sortOrder } }}
selection={hasNoPermissions ? undefined : euiBasicTableSelectionProps}
/>
{(isLoading || loading) && (
{rulesInstalled != null && rulesInstalled > 0 && (
<HeaderSection split title={i18n.ALL_RULES} border={true}>
<EuiFieldSearch
aria-label={i18n.SEARCH_RULES}
fullWidth
incremental={false}
placeholder={i18n.SEARCH_PLACEHOLDER}
onSearch={filterString => {
dispatch({
type: 'updateFilterOptions',
filterOptions: {
...filterOptions,
filter: filterString,
},
});
dispatch({
type: 'updatePagination',
pagination: { ...pagination, page: 1 },
});
}}
/>
</HeaderSection>
)}
{isInitialLoad && isEmpty(tableData) && (
<EuiLoadingContent data-test-subj="initialLoadingPanelAllRulesTable" lines={10} />
)}
{isGlobalLoading && !isEmpty(tableData) && (
<Loader data-test-subj="loadingPanelAllRulesTable" overlay size="xl" />
)}
{isEmpty(tableData) && prePackagedRuleStatus === 'ruleNotInstalled' && (
<PrePackagedRulesPrompt
createPrePackagedRules={handleCreatePrePackagedRules}
loading={loadingCreatePrePackagedRules}
userHasNoPermissions={hasNoPermissions}
/>
)}
{!isEmpty(tableData) && (
<>
<UtilityBar border>
<UtilityBarSection>
<UtilityBarGroup>
<UtilityBarText>{i18n.SHOWING_RULES(pagination.total ?? 0)}</UtilityBarText>
</UtilityBarGroup>
<UtilityBarGroup>
<UtilityBarText>{i18n.SELECTED_RULES(selectedItems.length)}</UtilityBarText>
{!hasNoPermissions && (
<UtilityBarAction
iconSide="right"
iconType="arrowDown"
popoverContent={getBatchItemsPopoverContent}
>
{i18n.BATCH_ACTIONS}
</UtilityBarAction>
)}
<UtilityBarAction
iconSide="right"
iconType="refresh"
onClick={() => dispatch({ type: 'refresh' })}
>
{i18n.REFRESH}
</UtilityBarAction>
</UtilityBarGroup>
</UtilityBarSection>
</UtilityBar>
<EuiBasicTable
columns={columns}
isSelectable={!hasNoPermissions ?? false}
itemId="id"
items={tableData}
onChange={tableOnChangeCallback}
pagination={{
pageIndex: pagination.page - 1,
pageSize: pagination.perPage,
totalItemCount: pagination.total,
pageSizeOptions: [5, 10, 20, 50, 100, 200, 300],
}}
sorting={{ sort: { field: 'activate', direction: filterOptions.sortOrder } }}
selection={hasNoPermissions ? undefined : euiBasicTableSelectionProps}
/>
</>
)}
</>
)}
</Panel>
</>
);
});
</Panel>
</>
);
}
);
AllRules.displayName = 'AllRules';

View file

@ -58,19 +58,21 @@ export const allRulesReducer = (state: State, action: Action): State => {
const ruleIds = state.rules.map(r => r.rule_id);
const appendIdx =
action.appendRuleId != null ? state.rules.findIndex(r => r.id === action.appendRuleId) : -1;
const updatedRules = action.rules.reduce(
(rules, updatedRule) =>
ruleIds.includes(updatedRule.rule_id)
? rules.map(r => (updatedRule.rule_id === r.rule_id ? updatedRule : r))
: appendIdx !== -1
? [
...rules.slice(0, appendIdx + 1),
updatedRule,
...rules.slice(appendIdx + 1, rules.length - 1),
]
: [...rules, updatedRule],
[...state.rules]
);
const updatedRules = action.rules.reverse().reduce((rules, updatedRule) => {
let newRules = rules;
if (ruleIds.includes(updatedRule.rule_id)) {
newRules = newRules.map(r => (updatedRule.rule_id === r.rule_id ? updatedRule : r));
} else if (appendIdx !== -1) {
newRules = [
...newRules.slice(0, appendIdx + 1),
updatedRule,
...newRules.slice(appendIdx + 1, newRules.length),
];
} else {
newRules = [...newRules, updatedRule];
}
return newRules;
}, state.rules);
// Update enabled on selectedItems so that batch actions show correct available actions
const updatedRuleIdToState = action.rules.reduce<Record<string, boolean>>(
@ -88,6 +90,13 @@ export const allRulesReducer = (state: State, action: Action): State => {
rules: updatedRules,
tableData: formatRules(updatedRules),
selectedItems: updatedSelectedItems,
pagination: {
...state.pagination,
total:
action.appendRuleId != null
? state.pagination.total + action.rules.length
: state.pagination.total,
},
};
}
case 'updatePagination': {
@ -112,6 +121,7 @@ export const allRulesReducer = (state: State, action: Action): State => {
...state,
rules: updatedRules,
tableData: formatRules(updatedRules),
refreshToggle: !state.refreshToggle,
};
}
case 'setSelected': {

View file

@ -0,0 +1,65 @@
/*
* 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 { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
import React, { memo, useCallback } from 'react';
import styled from 'styled-components';
import { DETECTION_ENGINE_PAGE_NAME } from '../../../../../components/link_to/redirect_to_detection_engine';
import * as i18n from './translations';
const EmptyPrompt = styled(EuiEmptyPrompt)`
align-self: center; /* Corrects horizontal centering in IE11 */
`;
interface PrePackagedRulesPromptProps {
createPrePackagedRules: () => void;
loading: boolean;
userHasNoPermissions: boolean;
}
const PrePackagedRulesPromptComponent: React.FC<PrePackagedRulesPromptProps> = ({
createPrePackagedRules,
loading = false,
userHasNoPermissions = true,
}) => {
const handlePreBuiltCreation = useCallback(() => {
createPrePackagedRules();
}, [createPrePackagedRules]);
return (
<EmptyPrompt
iconType="securityAnalyticsApp"
title={<h2>{i18n.PRE_BUILT_TITLE}</h2>}
body={<p>{i18n.PRE_BUILT_MSG}</p>}
actions={
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<EuiButton
fill
iconType="indexOpen"
isDisabled={userHasNoPermissions}
isLoading={loading}
onClick={handlePreBuiltCreation}
>
{i18n.PRE_BUILT_ACTION}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
isDisabled={userHasNoPermissions}
href={`#${DETECTION_ENGINE_PAGE_NAME}/rules/create`}
iconType="plusInCircle"
>
{i18n.CREATE_RULE_ACTION}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
}
/>
);
};
export const PrePackagedRulesPrompt = memo(PrePackagedRulesPromptComponent);

View file

@ -0,0 +1,57 @@
/*
* 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 PRE_BUILT_TITLE = i18n.translate(
'xpack.siem.detectionEngine.rules.prePackagedRules.emptyPromptTitle',
{
defaultMessage: 'Load Elastic prebuilt detection rules',
}
);
export const PRE_BUILT_MSG = i18n.translate(
'xpack.siem.detectionEngine.rules.prePackagedRules.emptyPromptMessage',
{
defaultMessage:
'Elastic SIEM comes with prebuilt detection rules that run in the background and create signals when their conditions are met.By default, all prebuilt rules are disabled and you select which rules you want to activate',
}
);
export const PRE_BUILT_ACTION = i18n.translate(
'xpack.siem.detectionEngine.rules.prePackagedRules.loadPreBuiltButton',
{
defaultMessage: 'Load prebuilt detection rules',
}
);
export const CREATE_RULE_ACTION = i18n.translate(
'xpack.siem.detectionEngine.rules.prePackagedRules.createOwnRuletButton',
{
defaultMessage: 'Create your own rules',
}
);
export const UPDATE_PREPACKAGED_RULES_TITLE = i18n.translate(
'xpack.siem.detectionEngine.rules.updatePrePackagedRulesTitle',
{
defaultMessage: 'Update available for Elastic prebuilt rules',
}
);
export const UPDATE_PREPACKAGED_RULES_MSG = (updateRules: number) =>
i18n.translate('xpack.siem.detectionEngine.rules.updatePrePackagedRulesMsg', {
values: { updateRules },
defaultMessage:
'You can update {updateRules} Elastic prebuilt {updateRules, plural, =1 {rule} other {rules}}. Note that this will reload deleted Elastic prebuilt rules.',
});
export const UPDATE_PREPACKAGED_RULES = (updateRules: number) =>
i18n.translate('xpack.siem.detectionEngine.rules.updatePrePackagedRulesButton', {
values: { updateRules },
defaultMessage:
'Update {updateRules} Elastic prebuilt {updateRules, plural, =1 {rule} other {rules}} ',
});

View file

@ -0,0 +1,31 @@
/*
* 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, { memo } from 'react';
import { EuiCallOut, EuiButton } from '@elastic/eui';
import * as i18n from './translations';
interface UpdatePrePackagedRulesCallOutProps {
loading: boolean;
numberOfUpdatedRules: number;
updateRules: () => void;
}
const UpdatePrePackagedRulesCallOutComponent: React.FC<UpdatePrePackagedRulesCallOutProps> = ({
loading,
numberOfUpdatedRules,
updateRules,
}) => (
<EuiCallOut title={i18n.UPDATE_PREPACKAGED_RULES_TITLE}>
<p>{i18n.UPDATE_PREPACKAGED_RULES_MSG(numberOfUpdatedRules)}</p>
<EuiButton onClick={updateRules} size="s" isLoading={loading}>
{i18n.UPDATE_PREPACKAGED_RULES(numberOfUpdatedRules)}
</EuiButton>
</EuiCallOut>
);
export const UpdatePrePackagedRulesCallOut = memo(UpdatePrePackagedRulesCallOutComponent);

View file

@ -29,10 +29,10 @@ import * as i18n from './translations';
const stepsRuleOrder = [RuleStep.defineRule, RuleStep.aboutRule, RuleStep.scheduleRule];
const MyEuiPanel = styled(EuiPanel)<{
zIndex?: number;
zindex?: number;
}>`
position: relative;
z-index: ${props => props.zIndex}; /* ugly fix to allow searchBar to overflow the EuiPanel */
z-index: ${props => props.zindex}; /* ugly fix to allow searchBar to overflow the EuiPanel */
.euiAccordion__iconWrapper {
display: none;
@ -80,16 +80,6 @@ export const CreateRuleComponent = React.memo(() => {
const userHasNoPermissions =
canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false;
if (
isSignalIndexExists != null &&
isAuthenticated != null &&
(!isSignalIndexExists || !isAuthenticated)
) {
return <Redirect to={`/${DETECTION_ENGINE_PAGE_NAME}`} />;
} else if (userHasNoPermissions) {
return <Redirect to={`/${DETECTION_ENGINE_PAGE_NAME}/rules`} />;
}
const setStepData = useCallback(
(step: RuleStep, data: unknown, isValid: boolean) => {
stepsData.current[step] = { ...stepsData.current[step], data, isValid };
@ -228,6 +218,16 @@ export const CreateRuleComponent = React.memo(() => {
return <Redirect to={`/${DETECTION_ENGINE_PAGE_NAME}/rules`} />;
}
if (
isSignalIndexExists != null &&
isAuthenticated != null &&
(!isSignalIndexExists || !isAuthenticated)
) {
return <Redirect to={`/${DETECTION_ENGINE_PAGE_NAME}`} />;
} else if (userHasNoPermissions) {
return <Redirect to={`/${DETECTION_ENGINE_PAGE_NAME}/rules`} />;
}
return (
<>
<WrapperPage restrictWidth>
@ -237,7 +237,7 @@ export const CreateRuleComponent = React.memo(() => {
isLoading={isLoading || loading}
title={i18n.PAGE_TITLE}
/>
<MyEuiPanel zIndex={3}>
<MyEuiPanel zindex={3}>
<EuiAccordion
initialIsOpen={true}
id={RuleStep.defineRule}
@ -272,7 +272,7 @@ export const CreateRuleComponent = React.memo(() => {
</EuiAccordion>
</MyEuiPanel>
<EuiSpacer size="l" />
<MyEuiPanel zIndex={2}>
<MyEuiPanel zindex={2}>
<EuiAccordion
initialIsOpen={false}
id={RuleStep.aboutRule}
@ -305,7 +305,7 @@ export const CreateRuleComponent = React.memo(() => {
</EuiAccordion>
</MyEuiPanel>
<EuiSpacer size="l" />
<MyEuiPanel zIndex={1}>
<MyEuiPanel zindex={1}>
<EuiAccordion
initialIsOpen={false}
id={RuleStep.scheduleRule}

View file

@ -122,14 +122,6 @@ const RuleDetailsComponent = memo<RuleDetailsComponentProps>(
const userHasNoPermissions =
canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false;
if (
isSignalIndexExists != null &&
isAuthenticated != null &&
(!isSignalIndexExists || !isAuthenticated)
) {
return <Redirect to={`/${DETECTION_ENGINE_PAGE_NAME}`} />;
}
const title = isLoading === true || rule === null ? <EuiLoadingSpinner size="m" /> : rule.name;
const subTitle = useMemo(
() =>
@ -228,6 +220,14 @@ const RuleDetailsComponent = memo<RuleDetailsComponentProps>(
[ruleEnabled, setRuleEnabled]
);
if (
isSignalIndexExists != null &&
isAuthenticated != null &&
(!isSignalIndexExists || !isAuthenticated)
) {
return <Redirect to={`/${DETECTION_ENGINE_PAGE_NAME}`} />;
}
return (
<>
{hasIndexWrite != null && !hasIndexWrite && <NoWriteSignalsCallOut />}

View file

@ -62,15 +62,6 @@ export const EditRuleComponent = memo(() => {
const userHasNoPermissions =
canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false;
if (
isSignalIndexExists != null &&
isAuthenticated != null &&
(!isSignalIndexExists || !isAuthenticated)
) {
return <Redirect to={`/${DETECTION_ENGINE_PAGE_NAME}`} />;
} else if (userHasNoPermissions) {
return <Redirect to={`/${DETECTION_ENGINE_PAGE_NAME}/rules/id/${ruleId}`} />;
}
const [initForm, setInitForm] = useState(false);
const [myAboutRuleForm, setMyAboutRuleForm] = useState<AboutStepRuleForm>({
@ -277,6 +268,16 @@ export const EditRuleComponent = memo(() => {
return <Redirect to={`/${DETECTION_ENGINE_PAGE_NAME}/rules/id/${ruleId}`} />;
}
if (
isSignalIndexExists != null &&
isAuthenticated != null &&
(!isSignalIndexExists || !isAuthenticated)
) {
return <Redirect to={`/${DETECTION_ENGINE_PAGE_NAME}`} />;
} else if (userHasNoPermissions) {
return <Redirect to={`/${DETECTION_ENGINE_PAGE_NAME}/rules/id/${ruleId}`} />;
}
return (
<>
<WrapperPage restrictWidth>

View file

@ -69,6 +69,52 @@ export const getStepsData = ({
export const useQuery = () => new URLSearchParams(useLocation().search);
export type PrePackagedRuleStatus =
| 'ruleInstalled'
| 'ruleNotInstalled'
| 'ruleNeedUpdate'
| 'someRuleUninstall'
| 'unknown';
export const getPrePackagedRuleStatus = (
rulesInstalled: number | null,
rulesNotInstalled: number | null,
rulesNotUpdated: number | null
): PrePackagedRuleStatus => {
if (
rulesNotInstalled != null &&
rulesInstalled === 0 &&
rulesNotInstalled > 0 &&
rulesNotUpdated === 0
) {
return 'ruleNotInstalled';
} else if (
rulesInstalled != null &&
rulesInstalled > 0 &&
rulesNotInstalled === 0 &&
rulesNotUpdated === 0
) {
return 'ruleInstalled';
} else if (
rulesInstalled != null &&
rulesNotInstalled != null &&
rulesInstalled > 0 &&
rulesNotInstalled > 0 &&
rulesNotUpdated === 0
) {
return 'someRuleUninstall';
} else if (
rulesInstalled != null &&
rulesNotInstalled != null &&
rulesNotUpdated != null &&
rulesInstalled > 0 &&
rulesNotInstalled >= 0 &&
rulesNotUpdated > 0
) {
return 'ruleNeedUpdate';
}
return 'unknown';
};
export const setFieldValue = (
form: FormHook<FormData>,
schema: FormSchema<FormData>,

View file

@ -6,32 +6,81 @@
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useState } from 'react';
import React, { useCallback, useRef, useState } from 'react';
import { Redirect } from 'react-router-dom';
import { usePrePackagedRules } from '../../../containers/detection_engine/rules';
import { DETECTION_ENGINE_PAGE_NAME } from '../../../components/link_to/redirect_to_detection_engine';
import { FormattedRelativePreferenceDate } from '../../../components/formatted_date';
import { getEmptyTagValue } from '../../../components/empty_value';
import { HeaderPage } from '../../../components/header_page';
import { WrapperPage } from '../../../components/wrapper_page';
import { SpyRoute } from '../../../utils/route/spy_routes';
import { useUserInfo } from '../components/user_info';
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 { UpdatePrePackagedRulesCallOut } from './components/pre_packaged_rules/update_callout';
import { getPrePackagedRuleStatus } from './helpers';
import * as i18n from './translations';
type Func = () => void;
export const RulesComponent = React.memo(() => {
const [showImportModal, setShowImportModal] = useState(false);
const [importCompleteToggle, setImportCompleteToggle] = useState(false);
const refreshRulesData = useRef<null | Func>(null);
const {
loading,
isSignalIndexExists,
isAuthenticated,
canUserCRUD,
hasIndexManage,
hasManageApiKey,
} = useUserInfo();
const {
createPrePackagedRules,
loading: prePackagedRuleLoading,
loadingCreatePrePackagedRules,
refetchPrePackagedRulesStatus,
rulesInstalled,
rulesNotInstalled,
rulesNotUpdated,
} = usePrePackagedRules({
canUserCRUD,
hasIndexManage,
hasManageApiKey,
isSignalIndexExists,
isAuthenticated,
});
const prePackagedRuleStatus = getPrePackagedRuleStatus(
rulesInstalled,
rulesNotInstalled,
rulesNotUpdated
);
const userHasNoPermissions =
canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false;
const lastCompletedRun = undefined;
const handleCreatePrePackagedRules = useCallback(async () => {
if (createPrePackagedRules != null) {
await createPrePackagedRules();
if (refreshRulesData.current != null) {
refreshRulesData.current();
}
}
}, [createPrePackagedRules, refreshRulesData]);
const handleRefetchPrePackagedRulesStatus = useCallback(() => {
if (refetchPrePackagedRulesStatus != null) {
refetchPrePackagedRulesStatus();
}
}, [refetchPrePackagedRulesStatus]);
const handleSetRefreshRulesData = useCallback((refreshRule: Func) => {
refreshRulesData.current = refreshRule;
}, []);
if (
isSignalIndexExists != null &&
@ -40,9 +89,7 @@ export const RulesComponent = React.memo(() => {
) {
return <Redirect to={`/${DETECTION_ENGINE_PAGE_NAME}`} />;
}
const userHasNoPermissions =
canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false;
const lastCompletedRun = undefined;
return (
<>
{userHasNoPermissions && <ReadOnlyCallOut />}
@ -73,6 +120,30 @@ export const RulesComponent = React.memo(() => {
title={i18n.PAGE_TITLE}
>
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false} wrap={true}>
{prePackagedRuleStatus === 'ruleNotInstalled' && (
<EuiFlexItem grow={false}>
<EuiButton
iconType="indexOpen"
isLoading={loadingCreatePrePackagedRules}
isDisabled={userHasNoPermissions || loading}
onClick={handleCreatePrePackagedRules}
>
{i18n.LOAD_PREPACKAGED_RULES}
</EuiButton>
</EuiFlexItem>
)}
{prePackagedRuleStatus === 'someRuleUninstall' && (
<EuiFlexItem grow={false}>
<EuiButton
iconType="plusInCircle"
isLoading={loadingCreatePrePackagedRules}
isDisabled={userHasNoPermissions || loading}
onClick={handleCreatePrePackagedRules}
>
{i18n.RELOAD_MISSING_PREPACKAGED_RULES(rulesNotInstalled ?? 0)}
</EuiButton>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiButton
iconType="importAction"
@ -96,10 +167,24 @@ export const RulesComponent = React.memo(() => {
</EuiFlexItem>
</EuiFlexGroup>
</HeaderPage>
{prePackagedRuleStatus === 'ruleNeedUpdate' && (
<UpdatePrePackagedRulesCallOut
loading={loadingCreatePrePackagedRules}
numberOfUpdatedRules={rulesNotUpdated ?? 0}
updateRules={handleCreatePrePackagedRules}
/>
)}
<AllRules
loading={loading}
createPrePackagedRules={createPrePackagedRules}
loading={loading || prePackagedRuleLoading}
loadingCreatePrePackagedRules={loadingCreatePrePackagedRules}
hasNoPermissions={userHasNoPermissions}
importCompleteToggle={importCompleteToggle}
refetchPrePackagedRulesStatus={handleRefetchPrePackagedRulesStatus}
rulesInstalled={rulesInstalled}
rulesNotInstalled={rulesNotInstalled}
rulesNotUpdated={rulesNotUpdated}
setRefreshRulesData={handleSetRefreshRulesData}
/>
</WrapperPage>

View file

@ -310,3 +310,17 @@ export const UPDATE = i18n.translate('xpack.siem.detectionEngine.rules.updateBut
export const DELETE = i18n.translate('xpack.siem.detectionEngine.rules.deleteDescription', {
defaultMessage: 'Delete',
});
export const LOAD_PREPACKAGED_RULES = i18n.translate(
'xpack.siem.detectionEngine.rules.loadPrePackagedRulesButton',
{
defaultMessage: 'Load Elastic prebuilt rules',
}
);
export const RELOAD_MISSING_PREPACKAGED_RULES = (missingRules: number) =>
i18n.translate('xpack.siem.detectionEngine.rules.reloadMissingPrePackagedRulesButton', {
values: { missingRules },
defaultMessage:
'Reload {missingRules} deleted Elastic prebuilt {missingRules, plural, =1 {rule} other {rules}} ',
});