mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Security Solution] Disable ML rule's edit button link under basic license (#143260)
**Resolves:** [#139796](https://github.com/elastic/kibana/issues/139796) ## Summary It disables ML rule's edit button link under the basic license. ## Details ML rules aren't available under the basic license but installable from the prebuilt rules. Having an active edit button makes the UX inconsistent. Disabling such a button under the basic license for ML rules improves UX though doesn't block a user from opening the rule editing page from the address bar. Before: https://user-images.githubusercontent.com/3775283/195552179-525f0423-3a62-4ab5-b1ef-0f5cafe2286e.mov After: https://user-images.githubusercontent.com/3775283/195551540-b95fabeb-4e50-4a26-ae42-1a72f53573dc.mov ### Checklist - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
This commit is contained in:
parent
4349ea70ee
commit
a670c7f376
19 changed files with 213 additions and 134 deletions
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { hasUserCRUDPermission } from '.';
|
||||
|
||||
describe('privileges utils', () => {
|
||||
describe('hasUserCRUDPermission', () => {
|
||||
test("returns true when user's CRUD operations are null", () => {
|
||||
const result = hasUserCRUDPermission(null);
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
test('returns false when user cannot CRUD', () => {
|
||||
const result = hasUserCRUDPermission(false);
|
||||
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
test('returns true when user can CRUD', () => {
|
||||
const result = hasUserCRUDPermission(true);
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import type { Rule } from '../../../detections/containers/detection_engine/rules';
|
||||
import * as i18n from '../../../detections/pages/detection_engine/rules/translations';
|
||||
import * as i18nActions from '../../../detections/pages/detection_engine/rules/translations';
|
||||
import { isMlRule } from '../../../../common/machine_learning/helpers';
|
||||
import * as detectionI18n from '../../../detections/pages/detection_engine/translations';
|
||||
|
||||
|
@ -29,21 +29,28 @@ export const canEditRuleWithActions = (
|
|||
return true;
|
||||
};
|
||||
|
||||
export const getToolTipContent = (
|
||||
// typed as null not undefined as the initial state for this value is null.
|
||||
export const hasUserCRUDPermission = (canUserCRUD: boolean | null): boolean =>
|
||||
canUserCRUD != null ? canUserCRUD : true;
|
||||
|
||||
export const explainLackOfPermission = (
|
||||
rule: Rule | null | undefined,
|
||||
hasMlPermissions: boolean,
|
||||
hasReadActionsPrivileges:
|
||||
| boolean
|
||||
| Readonly<{
|
||||
[x: string]: boolean;
|
||||
}>
|
||||
}>,
|
||||
canUserCRUD: boolean | null
|
||||
): string | undefined => {
|
||||
if (rule == null) {
|
||||
return undefined;
|
||||
} else if (isMlRule(rule.type) && !hasMlPermissions) {
|
||||
return detectionI18n.ML_RULES_DISABLED_MESSAGE;
|
||||
} else if (!canEditRuleWithActions(rule, hasReadActionsPrivileges)) {
|
||||
return i18n.EDIT_RULE_SETTINGS_TOOLTIP;
|
||||
return i18nActions.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES;
|
||||
} else if (!hasUserCRUDPermission(canUserCRUD)) {
|
||||
return i18nActions.LACK_OF_KIBANA_SECURITY_PRIVILEGES;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ import { useBoolState } from '../../../../common/hooks/use_bool_state';
|
|||
import { SINGLE_RULE_ACTIONS } from '../../../../common/lib/apm/user_actions';
|
||||
import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { getToolTipContent } from '../../../../common/utils/privileges';
|
||||
import { canEditRuleWithActions } from '../../../../common/utils/privileges';
|
||||
import type { Rule } from '../../../containers/detection_engine/rules';
|
||||
import {
|
||||
executeRulesBulkAction,
|
||||
|
@ -96,7 +96,11 @@ const RuleActionsOverflowComponent = ({
|
|||
>
|
||||
<EuiToolTip
|
||||
position="left"
|
||||
content={getToolTipContent(rule, true, canDuplicateRuleWithActions)}
|
||||
content={
|
||||
!canEditRuleWithActions(rule, canDuplicateRuleWithActions)
|
||||
? i18nActions.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<>{i18nActions.DUPLICATE_RULE}</>
|
||||
</EuiToolTip>
|
||||
|
|
|
@ -332,7 +332,9 @@ export const useBulkActions = ({
|
|||
disabled:
|
||||
missingActionPrivileges || containsLoading || (!containsDisabled && !isAllSelected),
|
||||
onClick: handleEnableAction,
|
||||
toolTipContent: missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined,
|
||||
toolTipContent: missingActionPrivileges
|
||||
? i18n.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES
|
||||
: undefined,
|
||||
toolTipPosition: 'right',
|
||||
icon: undefined,
|
||||
},
|
||||
|
@ -342,7 +344,9 @@ export const useBulkActions = ({
|
|||
'data-test-subj': 'duplicateRuleBulk',
|
||||
disabled: isEditDisabled,
|
||||
onClick: handleDuplicateAction,
|
||||
toolTipContent: missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined,
|
||||
toolTipContent: missingActionPrivileges
|
||||
? i18n.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES
|
||||
: undefined,
|
||||
toolTipPosition: 'right',
|
||||
icon: undefined,
|
||||
},
|
||||
|
@ -366,7 +370,9 @@ export const useBulkActions = ({
|
|||
'data-test-subj': 'addRuleActionsBulk',
|
||||
disabled: !hasActionsPrivileges || isEditDisabled,
|
||||
onClick: handleBulkEdit(BulkActionEditType.add_rule_actions),
|
||||
toolTipContent: !hasActionsPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined,
|
||||
toolTipContent: !hasActionsPrivileges
|
||||
? i18n.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES
|
||||
: undefined,
|
||||
toolTipPosition: 'right',
|
||||
icon: undefined,
|
||||
},
|
||||
|
@ -376,7 +382,9 @@ export const useBulkActions = ({
|
|||
'data-test-subj': 'setScheduleBulk',
|
||||
disabled: isEditDisabled,
|
||||
onClick: handleBulkEdit(BulkActionEditType.set_schedule),
|
||||
toolTipContent: missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined,
|
||||
toolTipContent: missingActionPrivileges
|
||||
? i18n.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES
|
||||
: undefined,
|
||||
toolTipPosition: 'right',
|
||||
icon: undefined,
|
||||
},
|
||||
|
@ -386,7 +394,9 @@ export const useBulkActions = ({
|
|||
'data-test-subj': 'applyTimelineTemplateBulk',
|
||||
disabled: isEditDisabled,
|
||||
onClick: handleBulkEdit(BulkActionEditType.set_timeline),
|
||||
toolTipContent: missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined,
|
||||
toolTipContent: missingActionPrivileges
|
||||
? i18n.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES
|
||||
: undefined,
|
||||
toolTipPosition: 'right',
|
||||
icon: undefined,
|
||||
},
|
||||
|
@ -405,7 +415,9 @@ export const useBulkActions = ({
|
|||
disabled:
|
||||
missingActionPrivileges || containsLoading || (!containsEnabled && !isAllSelected),
|
||||
onClick: handleDisableActions,
|
||||
toolTipContent: missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined,
|
||||
toolTipContent: missingActionPrivileges
|
||||
? i18n.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES
|
||||
: undefined,
|
||||
toolTipPosition: 'right',
|
||||
icon: undefined,
|
||||
},
|
||||
|
@ -439,7 +451,9 @@ export const useBulkActions = ({
|
|||
'data-test-subj': 'addTagsBulkEditRule',
|
||||
onClick: handleBulkEdit(BulkActionEditType.add_tags),
|
||||
disabled: isEditDisabled,
|
||||
toolTipContent: missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined,
|
||||
toolTipContent: missingActionPrivileges
|
||||
? i18n.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES
|
||||
: undefined,
|
||||
toolTipPosition: 'right',
|
||||
},
|
||||
{
|
||||
|
@ -448,7 +462,9 @@ export const useBulkActions = ({
|
|||
'data-test-subj': 'deleteTagsBulkEditRule',
|
||||
onClick: handleBulkEdit(BulkActionEditType.delete_tags),
|
||||
disabled: isEditDisabled,
|
||||
toolTipContent: missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined,
|
||||
toolTipContent: missingActionPrivileges
|
||||
? i18n.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES
|
||||
: undefined,
|
||||
toolTipPosition: 'right',
|
||||
},
|
||||
],
|
||||
|
@ -463,7 +479,9 @@ export const useBulkActions = ({
|
|||
'data-test-subj': 'addIndexPatternsBulkEditRule',
|
||||
onClick: handleBulkEdit(BulkActionEditType.add_index_patterns),
|
||||
disabled: isEditDisabled,
|
||||
toolTipContent: missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined,
|
||||
toolTipContent: missingActionPrivileges
|
||||
? i18n.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES
|
||||
: undefined,
|
||||
toolTipPosition: 'right',
|
||||
},
|
||||
{
|
||||
|
@ -472,7 +490,9 @@ export const useBulkActions = ({
|
|||
'data-test-subj': 'deleteIndexPatternsBulkEditRule',
|
||||
onClick: handleBulkEdit(BulkActionEditType.delete_index_patterns),
|
||||
disabled: isEditDisabled,
|
||||
toolTipContent: missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined,
|
||||
toolTipContent: missingActionPrivileges
|
||||
? i18n.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES
|
||||
: undefined,
|
||||
toolTipPosition: 'right',
|
||||
},
|
||||
],
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
|
||||
import type { NamespaceType, ExceptionListFilter } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { useApi, useExceptionLists } from '@kbn/securitysolution-list-hooks';
|
||||
import { hasUserCRUDPermission } from '../../../../../../common/utils/privileges';
|
||||
import { useAppToasts } from '../../../../../../common/hooks/use_app_toasts';
|
||||
import { AutoDownload } from '../../../../../../common/components/auto_download/auto_download';
|
||||
import { useKibana } from '../../../../../../common/lib/kibana';
|
||||
|
@ -36,7 +37,6 @@ import { ExceptionsSearchBar } from './exceptions_search_bar';
|
|||
import { getSearchFilters } from '../helpers';
|
||||
import { SecurityPageName } from '../../../../../../../common/constants';
|
||||
import { useUserData } from '../../../../../components/user_info';
|
||||
import { userHasPermissions } from '../../helpers';
|
||||
import { useListsConfig } from '../../../../../containers/detection_engine/lists/use_lists_config';
|
||||
import type { ExceptionsTableItem } from './types';
|
||||
import { MissingPrivilegesCallOut } from '../../../../../components/callouts/missing_privileges_callout';
|
||||
|
@ -63,7 +63,7 @@ const exceptionReferenceModalInitialState: ReferenceModalState = {
|
|||
export const ExceptionListsTable = React.memo(() => {
|
||||
const { formatUrl } = useFormatUrl(SecurityPageName.rules);
|
||||
const [{ loading: userInfoLoading, canUserCRUD, canUserREAD }] = useUserData();
|
||||
const hasPermissions = userHasPermissions(canUserCRUD);
|
||||
const hasPermissions = hasUserCRUDPermission(canUserCRUD);
|
||||
|
||||
const { loading: listsConfigLoading } = useListsConfig();
|
||||
const loading = userInfoLoading || listsConfigLoading;
|
||||
|
|
|
@ -43,7 +43,7 @@ export const getRulesTableActions = ({
|
|||
'data-test-subj': 'editRuleAction',
|
||||
description: i18n.EDIT_RULE_SETTINGS,
|
||||
name: !actionsPrivileges ? (
|
||||
<EuiToolTip position="left" content={i18n.EDIT_RULE_SETTINGS_TOOLTIP}>
|
||||
<EuiToolTip position="left" content={i18n.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES}>
|
||||
<>{i18n.EDIT_RULE_SETTINGS}</>
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
|
@ -59,7 +59,7 @@ export const getRulesTableActions = ({
|
|||
description: i18n.DUPLICATE_RULE,
|
||||
icon: 'copy',
|
||||
name: !actionsPrivileges ? (
|
||||
<EuiToolTip position="left" content={i18n.EDIT_RULE_SETTINGS_TOOLTIP}>
|
||||
<EuiToolTip position="left" content={i18n.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES}>
|
||||
<>{i18n.DUPLICATE_RULE}</>
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
|
|
|
@ -179,8 +179,8 @@ export const RulesTables = React.memo<RulesTableProps>(
|
|||
[setPage, setPerPage, setSortingOptions]
|
||||
);
|
||||
|
||||
const rulesColumns = useRulesColumns({ hasPermissions });
|
||||
const monitoringColumns = useMonitoringColumns({ hasPermissions });
|
||||
const rulesColumns = useRulesColumns({ hasCRUDPermissions: hasPermissions });
|
||||
const monitoringColumns = useMonitoringColumns({ hasCRUDPermissions: hasPermissions });
|
||||
|
||||
const handleCreatePrePackagedRules = useCallback(async () => {
|
||||
if (createPrePackagedRules != null) {
|
||||
|
|
|
@ -22,7 +22,10 @@ import { FormattedRelativePreferenceDate } from '../../../../../common/component
|
|||
import { getRuleDetailsTabUrl } from '../../../../../common/components/link_to/redirect_to_detection_engine';
|
||||
import { PopoverItems } from '../../../../../common/components/popover_items';
|
||||
import { useKibana, useUiSetting$ } from '../../../../../common/lib/kibana';
|
||||
import { canEditRuleWithActions, getToolTipContent } from '../../../../../common/utils/privileges';
|
||||
import {
|
||||
canEditRuleWithActions,
|
||||
explainLackOfPermission,
|
||||
} from '../../../../../common/utils/privileges';
|
||||
import { RuleSwitch } from '../../../../components/rules/rule_switch';
|
||||
import { SeverityBadge } from '../../../../components/rules/severity_badge';
|
||||
import type { Rule } from '../../../../containers/detection_engine/rules';
|
||||
|
@ -48,10 +51,10 @@ import { RuleDetailTabs } from '../details';
|
|||
export type TableColumn = EuiBasicTableColumn<Rule> | EuiTableActionsColumnType<Rule>;
|
||||
|
||||
interface ColumnsProps {
|
||||
hasPermissions: boolean;
|
||||
hasCRUDPermissions: boolean;
|
||||
}
|
||||
|
||||
const useEnabledColumn = ({ hasPermissions }: ColumnsProps): TableColumn => {
|
||||
const useEnabledColumn = ({ hasCRUDPermissions }: ColumnsProps): TableColumn => {
|
||||
const hasMlPermissions = useHasMlPermissions();
|
||||
const hasActionsPrivileges = useHasActionsPrivileges();
|
||||
const { loadingRulesAction, loadingRuleIds } = useRulesTableContext().state;
|
||||
|
@ -68,14 +71,19 @@ const useEnabledColumn = ({ hasPermissions }: ColumnsProps): TableColumn => {
|
|||
render: (_, rule: Rule) => (
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={getToolTipContent(rule, hasMlPermissions, hasActionsPrivileges)}
|
||||
content={explainLackOfPermission(
|
||||
rule,
|
||||
hasMlPermissions,
|
||||
hasActionsPrivileges,
|
||||
hasCRUDPermissions
|
||||
)}
|
||||
>
|
||||
<RuleSwitch
|
||||
id={rule.id}
|
||||
enabled={rule.enabled}
|
||||
isDisabled={
|
||||
!canEditRuleWithActions(rule, hasActionsPrivileges) ||
|
||||
!hasPermissions ||
|
||||
!hasCRUDPermissions ||
|
||||
(isMlRule(rule.type) && !hasMlPermissions && !rule.enabled)
|
||||
}
|
||||
isLoading={loadingIds.includes(rule.id)}
|
||||
|
@ -85,7 +93,7 @@ const useEnabledColumn = ({ hasPermissions }: ColumnsProps): TableColumn => {
|
|||
width: '95px',
|
||||
sortable: true,
|
||||
}),
|
||||
[hasActionsPrivileges, hasMlPermissions, hasPermissions, loadingIds]
|
||||
[hasActionsPrivileges, hasMlPermissions, hasCRUDPermissions, loadingIds]
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -195,9 +203,9 @@ const useActionsColumn = (): EuiTableActionsColumnType<Rule> => {
|
|||
);
|
||||
};
|
||||
|
||||
export const useRulesColumns = ({ hasPermissions }: ColumnsProps): TableColumn[] => {
|
||||
export const useRulesColumns = ({ hasCRUDPermissions }: ColumnsProps): TableColumn[] => {
|
||||
const actionsColumn = useActionsColumn();
|
||||
const enabledColumn = useEnabledColumn({ hasPermissions });
|
||||
const enabledColumn = useEnabledColumn({ hasCRUDPermissions });
|
||||
const ruleNameColumn = useRuleNameColumn();
|
||||
const { isInMemorySorting } = useRulesTableContext().state;
|
||||
const [showRelatedIntegrations] = useUiSetting$<boolean>(SHOW_RELATED_INTEGRATIONS_SETTING);
|
||||
|
@ -292,12 +300,12 @@ export const useRulesColumns = ({ hasPermissions }: ColumnsProps): TableColumn[]
|
|||
width: '65px',
|
||||
},
|
||||
enabledColumn,
|
||||
...(hasPermissions ? [actionsColumn] : []),
|
||||
...(hasCRUDPermissions ? [actionsColumn] : []),
|
||||
],
|
||||
[
|
||||
actionsColumn,
|
||||
enabledColumn,
|
||||
hasPermissions,
|
||||
hasCRUDPermissions,
|
||||
isInMemorySorting,
|
||||
ruleNameColumn,
|
||||
showRelatedIntegrations,
|
||||
|
@ -305,10 +313,10 @@ export const useRulesColumns = ({ hasPermissions }: ColumnsProps): TableColumn[]
|
|||
);
|
||||
};
|
||||
|
||||
export const useMonitoringColumns = ({ hasPermissions }: ColumnsProps): TableColumn[] => {
|
||||
export const useMonitoringColumns = ({ hasCRUDPermissions }: ColumnsProps): TableColumn[] => {
|
||||
const docLinks = useKibana().services.docLinks;
|
||||
const actionsColumn = useActionsColumn();
|
||||
const enabledColumn = useEnabledColumn({ hasPermissions });
|
||||
const enabledColumn = useEnabledColumn({ hasCRUDPermissions });
|
||||
const ruleNameColumn = useRuleNameColumn();
|
||||
const { isInMemorySorting } = useRulesTableContext().state;
|
||||
const [showRelatedIntegrations] = useUiSetting$<boolean>(SHOW_RELATED_INTEGRATIONS_SETTING);
|
||||
|
@ -425,13 +433,13 @@ export const useMonitoringColumns = ({ hasPermissions }: ColumnsProps): TableCol
|
|||
width: '16%',
|
||||
},
|
||||
enabledColumn,
|
||||
...(hasPermissions ? [actionsColumn] : []),
|
||||
...(hasCRUDPermissions ? [actionsColumn] : []),
|
||||
],
|
||||
[
|
||||
actionsColumn,
|
||||
docLinks.links.siem.troubleshootGaps,
|
||||
enabledColumn,
|
||||
hasPermissions,
|
||||
hasCRUDPermissions,
|
||||
isInMemorySorting,
|
||||
ruleNameColumn,
|
||||
showRelatedIntegrations,
|
||||
|
|
|
@ -18,6 +18,7 @@ import React, { useCallback, useRef, useState, useMemo, useEffect } from 'react'
|
|||
import styled from 'styled-components';
|
||||
|
||||
import type { DataViewListItem } from '@kbn/data-views-plugin/common';
|
||||
import { hasUserCRUDPermission } from '../../../../../common/utils/privileges';
|
||||
import { isThreatMatchRule } from '../../../../../../common/detection_engine/utils';
|
||||
import { useCreateRule } from '../../../../containers/detection_engine/rules';
|
||||
import type { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request';
|
||||
|
@ -38,12 +39,7 @@ import { StepAboutRule } from '../../../../components/rules/step_about_rule';
|
|||
import { StepScheduleRule } from '../../../../components/rules/step_schedule_rule';
|
||||
import { StepRuleActions } from '../../../../components/rules/step_rule_actions';
|
||||
import * as RuleI18n from '../translations';
|
||||
import {
|
||||
redirectToDetections,
|
||||
getActionMessageParams,
|
||||
userHasPermissions,
|
||||
MaxWidthEuiFlexItem,
|
||||
} from '../helpers';
|
||||
import { redirectToDetections, getActionMessageParams, MaxWidthEuiFlexItem } from '../helpers';
|
||||
import type {
|
||||
AboutStepRule,
|
||||
DefineStepRule,
|
||||
|
@ -364,7 +360,7 @@ const CreateRulePageComponent: React.FC = () => {
|
|||
path: getDetectionEngineUrl(),
|
||||
});
|
||||
return null;
|
||||
} else if (!userHasPermissions(canUserCRUD)) {
|
||||
} else if (!hasUserCRUDPermission(canUserCRUD)) {
|
||||
navigateToApp(APP_UI_ID, {
|
||||
deepLinkId: SecurityPageName.rules,
|
||||
path: getRulesUrl(),
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { EuiToolTip } from '@elastic/eui';
|
||||
import { useKibana } from '../../../../../../common/lib/kibana';
|
||||
import { SecuritySolutionLinkButton } from '../../../../../../common/components/links';
|
||||
import { APP_UI_ID } from '../../../../../../../common/constants';
|
||||
import { SecurityPageName } from '../../../../../../app/types';
|
||||
import { getEditRuleUrl } from '../../../../../../common/components/link_to/redirect_to_detection_engine';
|
||||
import * as ruleI18n from '../../translations';
|
||||
|
||||
interface EditRuleSettingButtonLinkProps {
|
||||
ruleId: string;
|
||||
disabled: boolean;
|
||||
disabledReason?: string;
|
||||
}
|
||||
|
||||
export function EditRuleSettingButtonLink({
|
||||
ruleId,
|
||||
disabled = false,
|
||||
disabledReason,
|
||||
}: EditRuleSettingButtonLinkProps): JSX.Element {
|
||||
const {
|
||||
application: { navigateToApp },
|
||||
} = useKibana().services;
|
||||
const goToEditRule = useCallback(
|
||||
(ev) => {
|
||||
ev.preventDefault();
|
||||
navigateToApp(APP_UI_ID, {
|
||||
deepLinkId: SecurityPageName.rules,
|
||||
path: getEditRuleUrl(ruleId),
|
||||
});
|
||||
},
|
||||
[navigateToApp, ruleId]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiToolTip position="top" content={disabledReason}>
|
||||
<SecuritySolutionLinkButton
|
||||
data-test-subj="editRuleSettingsLink"
|
||||
onClick={goToEditRule}
|
||||
iconType="controlsHorizontal"
|
||||
isDisabled={disabled}
|
||||
deepLinkId={SecurityPageName.rules}
|
||||
path={getEditRuleUrl(ruleId)}
|
||||
>
|
||||
{ruleI18n.EDIT_RULE_SETTINGS}
|
||||
</SecuritySolutionLinkButton>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
|
@ -27,12 +27,12 @@ import type { ConnectedProps } from 'react-redux';
|
|||
import { connect, useDispatch } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import type { Dispatch } from 'redux';
|
||||
import { isTab } from '@kbn/timelines-plugin/public';
|
||||
import type { DataViewListItem } from '@kbn/data-views-plugin/common';
|
||||
import { tableDefaults } from '../../../../../common/store/data_table/defaults';
|
||||
import { dataTableActions, dataTableSelectors } from '../../../../../common/store/data_table';
|
||||
import { isMlRule } from '../../../../../../common/machine_learning/helpers';
|
||||
import { SecuritySolutionTabNavigation } from '../../../../../common/components/navigation';
|
||||
import { InputsModelId } from '../../../../../common/store/inputs/constants';
|
||||
import {
|
||||
|
@ -45,7 +45,6 @@ import type { UpdateDateRange } from '../../../../../common/components/charts/co
|
|||
import { FiltersGlobal } from '../../../../../common/components/filters_global';
|
||||
import { FormattedDate } from '../../../../../common/components/formatted_date';
|
||||
import {
|
||||
getEditRuleUrl,
|
||||
getRulesUrl,
|
||||
getDetectionEngineUrl,
|
||||
getRuleDetailsTabUrl,
|
||||
|
@ -69,7 +68,7 @@ import {
|
|||
} from '../../../../components/alerts_table/default_config';
|
||||
import { RuleSwitch } from '../../../../components/rules/rule_switch';
|
||||
import { StepPanel } from '../../../../components/rules/step_panel';
|
||||
import { getStepsData, redirectToDetections, userHasPermissions } from '../helpers';
|
||||
import { getStepsData, redirectToDetections } from '../helpers';
|
||||
import { useGlobalTime } from '../../../../../common/containers/use_global_time';
|
||||
import { inputsSelectors } from '../../../../../common/store/inputs';
|
||||
import { setAbsoluteRangeDatePicker } from '../../../../../common/store/inputs/actions';
|
||||
|
@ -78,8 +77,6 @@ import { useMlCapabilities } from '../../../../../common/components/ml/hooks/use
|
|||
import { hasMlAdminPermissions } from '../../../../../../common/machine_learning/has_ml_admin_permissions';
|
||||
import { hasMlLicense } from '../../../../../../common/machine_learning/has_ml_license';
|
||||
import { SecurityPageName } from '../../../../../app/types';
|
||||
import { LinkButton } from '../../../../../common/components/links';
|
||||
import { useFormatUrl } from '../../../../../common/components/link_to';
|
||||
import {
|
||||
APP_UI_ID,
|
||||
DEFAULT_INDEX_KEY,
|
||||
|
@ -97,9 +94,10 @@ import {
|
|||
import { useSourcererDataView } from '../../../../../common/containers/sourcerer';
|
||||
import { SourcererScopeName } from '../../../../../common/store/sourcerer/model';
|
||||
import {
|
||||
getToolTipContent,
|
||||
explainLackOfPermission,
|
||||
canEditRuleWithActions,
|
||||
isBoolean,
|
||||
hasUserCRUDPermission,
|
||||
} from '../../../../../common/utils/privileges';
|
||||
|
||||
import {
|
||||
|
@ -132,6 +130,7 @@ import { useSignalHelpers } from '../../../../../common/containers/sourcerer/use
|
|||
import { HeaderPage } from '../../../../../common/components/header_page';
|
||||
import { ExceptionsViewer } from '../../../../../detection_engine/rule_exceptions/components/all_exception_items_table';
|
||||
import type { NavTab } from '../../../../../common/components/navigation/types';
|
||||
import { EditRuleSettingButtonLink } from './components/edit_rule_settings_button_link';
|
||||
|
||||
/**
|
||||
* Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space.
|
||||
|
@ -299,7 +298,6 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
|
|||
const [showBuildingBlockAlerts, setShowBuildingBlockAlerts] = useState(false);
|
||||
const [showOnlyThreatIndicatorAlerts, setShowOnlyThreatIndicatorAlerts] = useState(false);
|
||||
const mlCapabilities = useMlCapabilities();
|
||||
const { formatUrl } = useFormatUrl(SecurityPageName.rules);
|
||||
const { globalFullScreen } = useGlobalFullScreen();
|
||||
const [filterGroup, setFilterGroup] = useState<Status>(FILTER_OPEN);
|
||||
const [dataViewOptions, setDataViewOptions] = useState<{ [x: string]: DataViewListItem }>({});
|
||||
|
@ -584,45 +582,6 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
|
|||
setRule((currentRule) => (currentRule ? { ...currentRule, enabled } : currentRule));
|
||||
}, []);
|
||||
|
||||
const goToEditRule = useCallback(
|
||||
(ev) => {
|
||||
ev.preventDefault();
|
||||
navigateToApp(APP_UI_ID, {
|
||||
deepLinkId: SecurityPageName.rules,
|
||||
path: getEditRuleUrl(ruleId ?? ''),
|
||||
});
|
||||
},
|
||||
[navigateToApp, ruleId]
|
||||
);
|
||||
|
||||
const editRule = useMemo(() => {
|
||||
if (!hasActionsPrivileges) {
|
||||
return (
|
||||
<EuiToolTip position="top" content={ruleI18n.EDIT_RULE_SETTINGS_TOOLTIP}>
|
||||
<LinkButton
|
||||
onClick={goToEditRule}
|
||||
iconType="controlsHorizontal"
|
||||
isDisabled={true}
|
||||
href={formatUrl(getEditRuleUrl(ruleId ?? ''))}
|
||||
>
|
||||
{ruleI18n.EDIT_RULE_SETTINGS}
|
||||
</LinkButton>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<LinkButton
|
||||
data-test-subj="editRuleSettingsLink"
|
||||
onClick={goToEditRule}
|
||||
iconType="controlsHorizontal"
|
||||
isDisabled={!isExistingRule || !userHasPermissions(canUserCRUD)}
|
||||
href={formatUrl(getEditRuleUrl(ruleId ?? ''))}
|
||||
>
|
||||
{ruleI18n.EDIT_RULE_SETTINGS}
|
||||
</LinkButton>
|
||||
);
|
||||
}, [isExistingRule, canUserCRUD, formatUrl, goToEditRule, hasActionsPrivileges, ruleId]);
|
||||
|
||||
const onShowBuildingBlockAlertsChangedCallback = useCallback(
|
||||
(newShowBuildingBlockAlerts: boolean) => {
|
||||
setShowBuildingBlockAlerts(newShowBuildingBlockAlerts);
|
||||
|
@ -719,7 +678,12 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={getToolTipContent(rule, hasMlPermissions, hasActionsPrivileges)}
|
||||
content={explainLackOfPermission(
|
||||
rule,
|
||||
hasMlPermissions,
|
||||
hasActionsPrivileges,
|
||||
canUserCRUD
|
||||
)}
|
||||
>
|
||||
<EuiFlexGroup>
|
||||
<RuleSwitch
|
||||
|
@ -727,7 +691,7 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
|
|||
isDisabled={
|
||||
!isExistingRule ||
|
||||
!canEditRuleWithActions(rule, hasActionsPrivileges) ||
|
||||
!userHasPermissions(canUserCRUD) ||
|
||||
!hasUserCRUDPermission(canUserCRUD) ||
|
||||
(!hasMlPermissions && !rule?.enabled)
|
||||
}
|
||||
enabled={isExistingRule && (rule?.enabled ?? false)}
|
||||
|
@ -740,11 +704,26 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
|
|||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem grow={false}>{editRule}</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EditRuleSettingButtonLink
|
||||
ruleId={ruleId}
|
||||
disabled={
|
||||
!isExistingRule ||
|
||||
!hasUserCRUDPermission(canUserCRUD) ||
|
||||
(isMlRule(rule?.type) && !hasMlPermissions)
|
||||
}
|
||||
disabledReason={explainLackOfPermission(
|
||||
rule,
|
||||
hasMlPermissions,
|
||||
hasActionsPrivileges,
|
||||
canUserCRUD
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<RuleActionsOverflow
|
||||
rule={rule}
|
||||
userHasPermissions={isExistingRule && userHasPermissions(canUserCRUD)}
|
||||
userHasPermissions={isExistingRule && hasUserCRUDPermission(canUserCRUD)}
|
||||
canDuplicateRuleWithActions={canEditRuleWithActions(
|
||||
rule,
|
||||
hasActionsPrivileges
|
||||
|
|
|
@ -21,6 +21,7 @@ import { useParams } from 'react-router-dom';
|
|||
import { noop } from 'lodash';
|
||||
|
||||
import type { DataViewListItem } from '@kbn/data-views-plugin/common';
|
||||
import { hasUserCRUDPermission } from '../../../../../common/utils/privileges';
|
||||
import type { UpdateRulesSchema } from '../../../../../../common/detection_engine/schemas/request';
|
||||
import { useRule, useUpdateRule } from '../../../../containers/detection_engine/rules';
|
||||
import { useListsConfig } from '../../../../containers/detection_engine/lists/use_lists_config';
|
||||
|
@ -49,7 +50,6 @@ import {
|
|||
getStepsData,
|
||||
redirectToDetections,
|
||||
getActionMessageParams,
|
||||
userHasPermissions,
|
||||
MaxWidthEuiFlexItem,
|
||||
} from '../helpers';
|
||||
import * as ruleI18n from '../translations';
|
||||
|
@ -454,7 +454,7 @@ const EditRulePageComponent: FC = () => {
|
|||
path: getDetectionEngineUrl(),
|
||||
});
|
||||
return null;
|
||||
} else if (!userHasPermissions(canUserCRUD)) {
|
||||
} else if (!hasUserCRUDPermission(canUserCRUD)) {
|
||||
navigateToApp(APP_UI_ID, {
|
||||
deepLinkId: SecurityPageName.rules,
|
||||
path: getRuleDetailsUrl(ruleId ?? ''),
|
||||
|
|
|
@ -18,7 +18,6 @@ import {
|
|||
getPrePackagedRuleStatus,
|
||||
getPrePackagedTimelineStatus,
|
||||
determineDetailsValue,
|
||||
userHasPermissions,
|
||||
fillEmptySeverityMappings,
|
||||
} from './helpers';
|
||||
import { mockRuleWithEverything, mockRule } from './all/__mocks__/mock';
|
||||
|
@ -448,29 +447,6 @@ describe('rule helpers', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('userHasPermissions', () => {
|
||||
test("returns true when user's CRUD operations are null", () => {
|
||||
const result: boolean = userHasPermissions(null);
|
||||
const userHasPermissionsExpectedResult = true;
|
||||
|
||||
expect(result).toEqual(userHasPermissionsExpectedResult);
|
||||
});
|
||||
|
||||
test('returns false when user cannot CRUD', () => {
|
||||
const result: boolean = userHasPermissions(false);
|
||||
const userHasPermissionsExpectedResult = false;
|
||||
|
||||
expect(result).toEqual(userHasPermissionsExpectedResult);
|
||||
});
|
||||
|
||||
test('returns true when user can CRUD', () => {
|
||||
const result: boolean = userHasPermissions(true);
|
||||
const userHasPermissionsExpectedResult = true;
|
||||
|
||||
expect(result).toEqual(userHasPermissionsExpectedResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPrePackagedRuleStatus', () => {
|
||||
test('ruleNotInstalled', () => {
|
||||
const rulesInstalled = 0;
|
||||
|
|
|
@ -461,10 +461,6 @@ export const getActionMessageParams = memoizeOne((ruleType: Type | undefined): A
|
|||
export const getAllActionMessageParams = () =>
|
||||
transformRuleKeysToActionVariables(getAllRuleParamsKeys());
|
||||
|
||||
// typed as null not undefined as the initial state for this value is null.
|
||||
export const userHasPermissions = (canUserCRUD: boolean | null): boolean =>
|
||||
canUserCRUD != null ? canUserCRUD : true;
|
||||
|
||||
export const MaxWidthEuiFlexItem = styled(EuiFlexItem)`
|
||||
max-width: 1000px;
|
||||
overflow: hidden;
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { hasUserCRUDPermission } from '../../../../common/utils/privileges';
|
||||
import { MlJobUpgradeModal } from '../../../components/modals/ml_job_upgrade_modal';
|
||||
import { affectedJobIds } from '../../../components/callouts/ml_job_compatibility_callout/affected_job_ids';
|
||||
import { useInstalledSecurityJobs } from '../../../../common/components/ml/hooks/use_installed_security_jobs';
|
||||
|
@ -26,7 +27,6 @@ import {
|
|||
getPrePackagedRuleStatus,
|
||||
getPrePackagedTimelineStatus,
|
||||
redirectToDetections,
|
||||
userHasPermissions,
|
||||
} from './helpers';
|
||||
import * as i18n from './translations';
|
||||
import { SecurityPageName } from '../../../../app/types';
|
||||
|
@ -126,7 +126,7 @@ const RulesPageComponent: React.FC = () => {
|
|||
const loadPrebuiltRulesAndTemplatesButton = useMemo(
|
||||
() =>
|
||||
getLoadPrebuiltRulesAndTemplatesButton({
|
||||
isDisabled: !userHasPermissions(canUserCRUD) || loading || loadingJobs,
|
||||
isDisabled: !hasUserCRUDPermission(canUserCRUD) || loading || loadingJobs,
|
||||
onClick: showMlJobUpgradeModal,
|
||||
}),
|
||||
[
|
||||
|
@ -141,7 +141,7 @@ const RulesPageComponent: React.FC = () => {
|
|||
const reloadPrebuiltRulesAndTemplatesButton = useMemo(
|
||||
() =>
|
||||
getReloadPrebuiltRulesAndTemplatesButton({
|
||||
isDisabled: !userHasPermissions(canUserCRUD) || loading || loadingJobs,
|
||||
isDisabled: !hasUserCRUDPermission(canUserCRUD) || loading || loadingJobs,
|
||||
onClick: showMlJobUpgradeModal,
|
||||
}),
|
||||
[
|
||||
|
@ -224,7 +224,7 @@ const RulesPageComponent: React.FC = () => {
|
|||
<EuiButton
|
||||
data-test-subj="rules-import-modal-button"
|
||||
iconType="importAction"
|
||||
isDisabled={!userHasPermissions(canUserCRUD) || loading}
|
||||
isDisabled={!hasUserCRUDPermission(canUserCRUD) || loading}
|
||||
onClick={showImportModal}
|
||||
>
|
||||
{i18n.IMPORT_RULE}
|
||||
|
@ -235,7 +235,7 @@ const RulesPageComponent: React.FC = () => {
|
|||
data-test-subj="create-new-rule"
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
isDisabled={!userHasPermissions(canUserCRUD) || loading}
|
||||
isDisabled={!hasUserCRUDPermission(canUserCRUD) || loading}
|
||||
deepLinkId={SecurityPageName.rulesCreate}
|
||||
>
|
||||
{i18n.ADD_NEW_RULE}
|
||||
|
@ -257,7 +257,7 @@ const RulesPageComponent: React.FC = () => {
|
|||
createPrePackagedRules={createPrePackagedRules}
|
||||
data-test-subj="all-rules"
|
||||
loadingCreatePrePackagedRules={loadingCreatePrePackagedRules}
|
||||
hasPermissions={userHasPermissions(canUserCRUD)}
|
||||
hasPermissions={hasUserCRUDPermission(canUserCRUD)}
|
||||
rulesCustomInstalled={rulesCustomInstalled}
|
||||
rulesInstalled={rulesInstalled}
|
||||
rulesNotInstalled={rulesNotInstalled}
|
||||
|
|
|
@ -484,13 +484,20 @@ export const EDIT_RULE_SETTINGS = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const EDIT_RULE_SETTINGS_TOOLTIP = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.allRules.actions.editRuleSettingsToolTip',
|
||||
export const LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.allRules.actions.lackOfKibanaActionsFeaturePrivileges',
|
||||
{
|
||||
defaultMessage: 'You do not have Kibana Actions privileges',
|
||||
}
|
||||
);
|
||||
|
||||
export const LACK_OF_KIBANA_SECURITY_PRIVILEGES = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.allRules.actions.lackOfKibanaSecurityPrivileges',
|
||||
{
|
||||
defaultMessage: 'You do not have Kibana Security privileges',
|
||||
}
|
||||
);
|
||||
|
||||
export const DUPLICATE_RULE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateRuleDescription',
|
||||
{
|
||||
|
|
|
@ -27333,7 +27333,7 @@
|
|||
"xpack.securitySolution.detectionEngine.rules.allRules.actions.deleteRuleDescription": "Supprimer la règle",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateRuleDescription": "Dupliquer la règle",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.actions.editRuleSettingsDescription": "Modifier les paramètres de règles",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.actions.editRuleSettingsToolTip": "Vous ne disposez pas des privilèges d'actions Kibana",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.actions.lackOfKibanaActionsFeaturePrivileges": "Vous ne disposez pas des privilèges d'actions Kibana",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.actions.exportRuleDescription": "Exporter la règle",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedImmutableTitle": "La sélection contient des règles immuables qui ne peuvent pas être supprimées",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.batchActionsTitle": "Actions groupées",
|
||||
|
|
|
@ -27308,7 +27308,7 @@
|
|||
"xpack.securitySolution.detectionEngine.rules.allRules.actions.deleteRuleDescription": "ルールの削除",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateRuleDescription": "ルールの複製",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.actions.editRuleSettingsDescription": "ルール設定の編集",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.actions.editRuleSettingsToolTip": "Kibana アクション特権がありません",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.actions.lackOfKibanaActionsFeaturePrivileges": "Kibana アクション特権がありません",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.actions.exportRuleDescription": "ルールのエクスポート",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedImmutableTitle": "選択には削除できないイミュータブルルールがあります",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.batchActionsTitle": "一斉アクション",
|
||||
|
|
|
@ -27342,7 +27342,7 @@
|
|||
"xpack.securitySolution.detectionEngine.rules.allRules.actions.deleteRuleDescription": "删除规则",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateRuleDescription": "复制规则",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.actions.editRuleSettingsDescription": "编辑规则设置",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.actions.editRuleSettingsToolTip": "您没有 Kibana 操作权限",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.actions.lackOfKibanaActionsFeaturePrivileges": "您没有 Kibana 操作权限",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.actions.exportRuleDescription": "导出规则",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedImmutableTitle": "选择内容包含无法删除的不可变规则",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.batchActionsTitle": "批处理操作",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue