Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution] Allow prebuilt rules import and export #212509

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .buildkite/ftr_security_serverless_configs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ enabled:
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_delete/basic_license_essentials_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/import_non_customized_prebuilt_rules/feature_enabled/configs/serverless_essentials_tier.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/import_non_customized_prebuilt_rules/feature_disabled/configs/serverless_feature_flag_disabled.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/import_customized_prebuilt_rules/feature_enabled/configs/serverless_complete_tier.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/import_customized_prebuilt_rules/feature_disabled/configs/serverless_essentials_tier.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/import_customized_prebuilt_rules/feature_disabled/configs/serverless_feature_flag_disabled.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/export_prebuilt_rules/feature_enabled/configs/serverless_essentials_tier.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/export_prebuilt_rules/feature_disabled/configs/serverless_feature_flag_disabled.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_management/basic_license_essentials_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_management/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_read/trial_license_complete_tier/configs/serverless.config.ts
Expand Down
7 changes: 7 additions & 0 deletions .buildkite/ftr_security_stateful_configs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ enabled:
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_management/basic_license_essentials_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/import_non_customized_prebuilt_rules/feature_enabled/configs/ess_basic_license.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/import_non_customized_prebuilt_rules/feature_disabled/configs/ess_feature_flag_disabled.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/import_customized_prebuilt_rules/feature_enabled/configs/ess_enterprise_license.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/import_customized_prebuilt_rules/feature_disabled/configs/ess_basic_license.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/import_customized_prebuilt_rules/feature_disabled/configs/ess_feature_flag_disabled.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/export_prebuilt_rules/feature_enabled/configs/ess_basic_license.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/export_prebuilt_rules/feature_disabled/configs/ess_feature_flag_disabled.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_management/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_read/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_read/basic_license_essentials_tier/configs/ess.config.ts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ import { useDownloadExportedRules } from '../../../rule_management/logic/bulk_ac
import { useHasActionsPrivileges } from './use_has_actions_privileges';
import type { TimeRange } from '../../../rule_gaps/types';
import { useScheduleRuleRun } from '../../../rule_gaps/logic/use_schedule_rule_run';
import { usePrebuiltRulesCustomizationStatus } from '../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_customization_status';
import { ManualRuleRunEventTypes } from '../../../../common/lib/telemetry';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';

export const useRulesTableActions = ({
showExceptionsDuplicateConfirmation,
Expand All @@ -47,7 +47,9 @@ export const useRulesTableActions = ({
const { bulkExport } = useBulkExport();
const downloadExportedRules = useDownloadExportedRules();
const { scheduleRuleRun } = useScheduleRuleRun();
const { isRulesCustomizationEnabled } = usePrebuiltRulesCustomizationStatus();
const isPrebuiltRulesCustomizationFeatureFlagEnabled = useIsExperimentalFeatureEnabled(
'prebuiltRulesCustomizationEnabled'
);

return [
{
Expand Down Expand Up @@ -118,7 +120,7 @@ export const useRulesTableActions = ({
await downloadExportedRules(response);
}
},
enabled: (rule: Rule) => isRulesCustomizationEnabled || !rule.immutable,
enabled: (rule: Rule) => isPrebuiltRulesCustomizationFeatureFlagEnabled || !rule.immutable,
},
{
type: 'icon',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
} from '@elastic/eui';
import React, { useCallback, useMemo } from 'react';
import styled from 'styled-components';
import { usePrebuiltRulesCustomizationStatus } from '../../../../detection_engine/rule_management/logic/prebuilt_rules/use_prebuilt_rules_customization_status';
import { useScheduleRuleRun } from '../../../../detection_engine/rule_gaps/logic/use_schedule_rule_run';
import type { TimeRange } from '../../../../detection_engine/rule_gaps/types';
import { APP_UI_ID, SecurityPageName } from '../../../../../common/constants';
Expand All @@ -36,6 +35,7 @@ import { useDownloadExportedRules } from '../../../../detection_engine/rule_mana
import * as i18nActions from '../../../pages/detection_engine/rules/translations';
import * as i18n from './translations';
import { ManualRuleRunEventTypes } from '../../../../common/lib/telemetry';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';

const MyEuiButtonIcon = styled(EuiButtonIcon)`
&.euiButtonIcon {
Expand Down Expand Up @@ -73,7 +73,9 @@ const RuleActionsOverflowComponent = ({
application: { navigateToApp },
telemetry,
} = useKibana().services;
const { isRulesCustomizationEnabled } = usePrebuiltRulesCustomizationStatus();
const isPrebuiltRulesCustomizationFeatureFlagEnabled = useIsExperimentalFeatureEnabled(
'prebuiltRulesCustomizationEnabled'
);
const { startTransaction } = useStartTransaction();
const { executeBulkAction } = useExecuteBulkAction({ suppressSuccessToast: true });
const { bulkExport } = useBulkExport();
Expand Down Expand Up @@ -140,7 +142,8 @@ const RuleActionsOverflowComponent = ({
key={i18nActions.EXPORT_RULE}
icon="exportAction"
disabled={
!userHasPermissions || (isRulesCustomizationEnabled === false && rule.immutable)
!userHasPermissions ||
(isPrebuiltRulesCustomizationFeatureFlagEnabled === false && rule.immutable)
}
data-test-subj="rules-details-export-rule"
onClick={async () => {
Expand Down Expand Up @@ -210,7 +213,7 @@ const RuleActionsOverflowComponent = ({
rule,
canDuplicateRuleWithActions,
userHasPermissions,
isRulesCustomizationEnabled,
isPrebuiltRulesCustomizationFeatureFlagEnabled,
startTransaction,
closePopover,
showBulkDuplicateExceptionsConfirmation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { bulkEnableDisableRules } from './bulk_enable_disable_rules';
import { fetchRulesByQueryOrIds } from './fetch_rules_by_query_or_ids';
import { bulkScheduleBackfill } from './bulk_schedule_rule_run';
import { createPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client';
import { PrebuiltRulesCustomizationDisabledReason } from '../../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';

const MAX_RULES_TO_PROCESS_TOTAL = 10000;
// Set a lower limit for bulk edit as the rules client might fail with a "Query
Expand Down Expand Up @@ -277,14 +278,22 @@ export const performBulkActionRoute = (
break;
}
case BulkActionTypeEnum.export: {
const prebuiltRulesCustomizationStatus =
detectionRulesClient.getRuleCustomizationStatus();

const isPrebuiltRulesExportAllowed =
prebuiltRulesCustomizationStatus.isRulesCustomizationEnabled ||
prebuiltRulesCustomizationStatus.customizationDisabledReason ===
PrebuiltRulesCustomizationDisabledReason.License;

const exported = await getExportByObjectIds(
rulesClient,
exceptionsClient,
rules.map(({ params }) => params.ruleId),
exporter,
request,
actionsClient,
detectionRulesClient.getRuleCustomizationStatus().isRulesCustomizationEnabled
isPrebuiltRulesExportAllowed
);

const responseBody = `${exported.rulesNdjson}${exported.exceptionLists}${exported.actionConnectors}${exported.exportDetails}`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { getExportByObjectIds } from '../../../logic/export/get_export_by_object
import { getExportAll } from '../../../logic/export/get_export_all';
import { buildSiemResponse } from '../../../../routes/utils';
import { RULE_MANAGEMENT_IMPORT_EXPORT_SOCKET_TIMEOUT_MS } from '../../timeouts';
import { PrebuiltRulesCustomizationDisabledReason } from '../../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';

export const exportRulesRoute = (
router: SecuritySolutionPluginRouter,
Expand Down Expand Up @@ -73,7 +74,12 @@ export const exportRulesRoute = (

const client = getClient({ includedHiddenTypes: ['action'] });
const actionsExporter = getExporter(client);
const { isRulesCustomizationEnabled } = detectionRulesClient.getRuleCustomizationStatus();
const prebuiltRulesCustomizationStatus = detectionRulesClient.getRuleCustomizationStatus();

const isPrebuiltRulesExportAllowed =
prebuiltRulesCustomizationStatus.isRulesCustomizationEnabled ||
prebuiltRulesCustomizationStatus.customizationDisabledReason ===
PrebuiltRulesCustomizationDisabledReason.License;

try {
const exportSizeLimit = config.maxRuleImportExportSize;
Expand All @@ -85,7 +91,7 @@ export const exportRulesRoute = (
} else {
let rulesCount = 0;

if (isRulesCustomizationEnabled) {
if (isPrebuiltRulesExportAllowed) {
rulesCount = await getRulesCount({
rulesClient,
filter: '',
Expand All @@ -95,6 +101,7 @@ export const exportRulesRoute = (
rulesClient,
});
}

if (rulesCount > exportSizeLimit) {
return siemResponse.error({
statusCode: 400,
Expand All @@ -112,15 +119,15 @@ export const exportRulesRoute = (
actionsExporter,
request,
actionsClient,
isRulesCustomizationEnabled
isPrebuiltRulesExportAllowed
)
: await getExportAll(
rulesClient,
exceptionsClient,
actionsExporter,
request,
actionsClient,
isRulesCustomizationEnabled
isPrebuiltRulesExportAllowed
);

const responseBody = request.query.exclude_export_details
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { getQueryRuleParams } from '../../../../rule_schema/mocks';
import { importRulesRoute } from './route';
import { HttpAuthzError } from '../../../../../machine_learning/validation';
import { createPrebuiltRuleAssetsClient as createPrebuiltRuleAssetsClientMock } from '../../../../prebuilt_rules/logic/rule_assets/__mocks__/prebuilt_rule_assets_client';
import { PrebuiltRulesCustomizationDisabledReason } from '../../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';

jest.mock('../../../../../machine_learning/authz');

Expand Down Expand Up @@ -59,6 +60,7 @@ describe('Import rules route', () => {
clients.detectionRulesClient.importRule.mockResolvedValue(getRulesSchemaMock());
clients.detectionRulesClient.getRuleCustomizationStatus.mockReturnValue({
isRulesCustomizationEnabled: false,
customizationDisabledReason: PrebuiltRulesCustomizationDisabledReason.FeatureFlag,
});
clients.actionsClient.getAll.mockResolvedValue([]);
context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValue(
Expand All @@ -71,7 +73,6 @@ describe('Import rules route', () => {
describe('status codes', () => {
test('returns 200 when importing a single rule with a valid actionClient and alertClient', async () => {
const response = await server.inject(request, requestContextMock.convertContext(context));

expect(response.status).toEqual(200);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
migrateLegacyActionsIds,
} from '../../../utils/utils';
import { RULE_MANAGEMENT_IMPORT_EXPORT_SOCKET_TIMEOUT_MS } from '../../timeouts';
import { PrebuiltRulesCustomizationDisabledReason } from '../../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';

const CHUNK_PARSED_OBJECT_SIZE = 50;

Expand Down Expand Up @@ -86,7 +87,7 @@ export const importRulesRoute = (router: SecuritySolutionPluginRouter, config: C
]);

const detectionRulesClient = ctx.securitySolution.getDetectionRulesClient();
const { isRulesCustomizationEnabled } = detectionRulesClient.getRuleCustomizationStatus();
const ruleCustomizationStatus = detectionRulesClient.getRuleCustomizationStatus();
const actionsClient = ctx.actions.getActionsClient();
const actionSOClient = ctx.core.savedObjects.getClient({
includedHiddenTypes: ['action'],
Expand Down Expand Up @@ -166,21 +167,24 @@ export const importRulesRoute = (router: SecuritySolutionPluginRouter, config: C

let importRuleResponse: ImportRuleResponse[] = [];

if (isRulesCustomizationEnabled) {
importRuleResponse = await importRules({
if (
ruleCustomizationStatus.customizationDisabledReason ===
PrebuiltRulesCustomizationDisabledReason.FeatureFlag
) {
importRuleResponse = await importRulesLegacy({
ruleChunks,
overwriteRules: request.query.overwrite,
allowMissingConnectorSecrets: !!actionConnectors.length,
ruleSourceImporter,
detectionRulesClient,
savedObjectsClient,
});
} else {
importRuleResponse = await importRulesLegacy({
importRuleResponse = await importRules({
ruleChunks,
overwriteRules: request.query.overwrite,
allowMissingConnectorSecrets: !!actionConnectors.length,
ruleSourceImporter,
detectionRulesClient,
savedObjectsClient,
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export const validateBulkEditRule = async ({
}

// Rule customization is disabled; only certain actions can be applied to immutable rules
const canRuleBeEdited = istEditApplicableToImmutableRule(edit);
const canRuleBeEdited = isEditApplicableToImmutableRule(edit);
if (!canRuleBeEdited) {
await throwDryRunError(
() => invariant(canRuleBeEdited, "Elastic rule can't be edited"),
Expand All @@ -120,7 +120,7 @@ export const validateBulkEditRule = async ({
/**
* add_rule_actions, set_rule_actions can be applied to prebuilt/immutable rules
*/
const istEditApplicableToImmutableRule = (edit: BulkActionEditPayload[]): boolean => {
const isEditApplicableToImmutableRule = (edit: BulkActionEditPayload[]): boolean => {
const applicableActions: BulkActionEditType[] = [
BulkActionEditTypeEnum.set_rule_actions,
BulkActionEditTypeEnum.add_rule_actions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import { checkRuleExceptionReferences } from '../../import/check_rule_exception_references';
import { getReferencedExceptionLists } from '../../import/gather_referenced_exceptions';
import type { IDetectionRulesClient } from '../detection_rules_client_interface';
import { PrebuiltRulesCustomizationDisabledReason } from '../../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';

/**
* Imports rules
Expand All @@ -39,6 +40,8 @@ export const importRules = async ({
rules: RuleToImport[];
savedObjectsClient: SavedObjectsClientContract;
}): Promise<Array<RuleResponse | RuleImportErrorObject>> => {
const ruleCustomizationStatus = detectionRulesClient.getRuleCustomizationStatus();

const existingLists = await getReferencedExceptionLists({
rules,
savedObjectsClient,
Expand Down Expand Up @@ -69,6 +72,25 @@ export const importRules = async ({
}

const { immutable, ruleSource } = ruleSourceImporter.calculateRuleSource(rule);
const isCustomized = (ruleSource.type === 'external' && ruleSource.is_customized) ?? false;

if (
isCustomized &&
ruleCustomizationStatus.customizationDisabledReason ===
PrebuiltRulesCustomizationDisabledReason.License
) {
return createRuleImportErrorObject({
message: i18n.translate(
'xpack.securitySolution.detectionEngine.rules.licenseInsufficientToImportCustomizedPrebuiltRule',
{
defaultMessage:
'Importing prebuilt rules is not supported if the they were modified. Upgrade your license to import modified prebuilt rules [rule_id: {ruleId}].',
values: { ruleId: rule.rule_id },
}
),
ruleId: rule.rule_id,
});
}

const [exceptionErrors, exceptions] = checkRuleExceptionReferences({
rule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,14 @@ export const getExportAll = async (
actionsExporter: ISavedObjectsExporter,
request: KibanaRequest,
actionsClient: ActionsClient,
prebuiltRulesCustomizationEnabled?: boolean
isPrebuiltRulesExportAllowed?: boolean
): Promise<{
rulesNdjson: string;
exportDetails: string;
exceptionLists: string | null;
actionConnectors: string;
prebuiltRulesCustomizationEnabled?: boolean;
}> => {
const ruleAlertTypes = prebuiltRulesCustomizationEnabled
const ruleAlertTypes = isPrebuiltRulesExportAllowed
? await getRules({ rulesClient, filter: '' })
: await getNonPackagedRules({ rulesClient });
const rules = transformAlertsToRules(ruleAlertTypes);
Expand Down
Loading