mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
* 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:
parent
b4f15c6346
commit
bfe9b45a55
22 changed files with 865 additions and 359 deletions
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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];
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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];
|
||||
};
|
||||
|
|
|
@ -59,6 +59,7 @@ export const usePrivilegeUser = (): Return => {
|
|||
setAuthenticated(false);
|
||||
setHasIndexManage(false);
|
||||
setHasIndexWrite(false);
|
||||
setHasManageApiKey(false);
|
||||
errorToToaster({ title: i18n.PRIVILEGE_FETCH_FAILURE, error, dispatchToaster });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 />}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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': {
|
||||
|
|
|
@ -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);
|
|
@ -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}} ',
|
||||
});
|
|
@ -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);
|
|
@ -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}
|
||||
|
|
|
@ -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 />}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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}} ',
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue