Skip to content
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
1 change: 1 addition & 0 deletions .buildkite/ftr_security_serverless_configs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ disabled:
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/upgrade_prebuilt_rules/diffable_rule_fields/common_fields/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/upgrade_prebuilt_rules/diffable_rule_fields/type_specific_fields/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/configs/serverless_essentials_tier.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_delete/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_delete/basic_license_essentials_tier/configs/serverless.config.ts
Expand Down
3 changes: 2 additions & 1 deletion .buildkite/ftr_security_stateful_configs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ enabled:
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/upgrade_prebuilt_rules/diffable_rule_fields/type_specific_fields/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/configs/ess_air_gapped_large_package.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/configs/ess_air_gapped.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/configs/ess_basic_license.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_delete/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_delete/basic_license_essentials_tier/configs/ess.config.ts
Expand Down Expand Up @@ -104,4 +105,4 @@ enabled:
- x-pack/test/cloud_security_posture_functional/config.agentless.ts
- x-pack/test/cloud_security_posture_functional/data_views/config.ts
- x-pack/test/automatic_import_api_integration/apis/config_basic.ts
- x-pack/test/automatic_import_api_integration/apis/config_graphs.ts
- x-pack/test/automatic_import_api_integration/apis/config_graphs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ describe('Perform Rule Upgrade Route Schemas', () => {
);
});

test('rejects paylaod with missing rules array', () => {
test('rejects payload with missing rules array', () => {
const invalid = { ...validRequest, rules: undefined };
const result = UpgradeSpecificRulesRequest.safeParse(invalid);
expectParseError(result);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useMemo, useState } from 'react';
import useResizeObserver from 'use-resize-observer/polyfilled';
import { niceTimeFormatByDay, timeFormatter } from '@elastic/charts';
import moment from 'moment-timezone';
import type { IToasts } from '@kbn/core/public';

export const getDaysDiff = (minDate: moment.Moment, maxDate: moment.Moment) => {
const diff = maxDate.diff(minDate, 'days');
Expand All @@ -35,3 +36,32 @@ export const useThrottledResizeObserver = (wait = 100) => {

return { ref, ...size };
};

/**
* Displays an error toast with a specified title and a short message.
* Also allows to set a detailed message that will appear in the modal when user clicks the "See full error" button.
*
* @param title The title of the toast notification. Appears in both the toast header and the modal header.
* @param shortMessage An optional short message. Appears under toast header.
* @param fullMessage The full error message. Appears in the modal when user clicks the "See full error" button.
* @param toasts The toasts service instance.
*/
export function showErrorToast({
title,
shortMessage,
fullMessage,
toasts,
}: {
title: string;
shortMessage?: string;
fullMessage: string;
toasts: IToasts;
}) {
const error = new Error('Error details');
error.stack = fullMessage;
toasts.addError(error, {
title,
// Fall back to a space to ensure that the toast component does not render its default message
toastMessage: shortMessage ?? ' ',
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const RULE_INSTALLATION_FAILED = i18n.translate(

export const INSTALL_RULE_SUCCESS = (succeeded: number) =>
i18n.translate('xpack.securitySolution.detectionEngine.prebuiltRules.toast.installRuleSuccess', {
defaultMessage: '{succeeded, plural, one {# rule} other {# rules}} installed successfully.',
defaultMessage: '{succeeded, plural, one {# rule} other {# rules}} installed successfully',
values: { succeeded },
});

Expand All @@ -29,7 +29,7 @@ export const INSTALL_RULE_SKIPPED = (skipped: number) =>

export const INSTALL_RULE_FAILED = (failed: number) =>
i18n.translate('xpack.securitySolution.detectionEngine.prebuiltRules.toast.installRuleFailed', {
defaultMessage: '{failed, plural, one {# rule} other {# rules}} failed to install.',
defaultMessage: '{failed, plural, one {# rule} other {# rules}} failed to install',
values: { failed },
});

Expand All @@ -42,7 +42,7 @@ export const RULE_UPGRADE_FAILED = i18n.translate(

export const UPGRADE_RULE_SUCCESS = (succeeded: number) =>
i18n.translate('xpack.securitySolution.detectionEngine.prebuiltRules.toast.upgradeRuleSuccess', {
defaultMessage: '{succeeded, plural, one {# rule} other {# rules}} updated successfully.',
defaultMessage: '{succeeded, plural, one {# rule} other {# rules}} updated successfully',
values: { succeeded },
});

Expand All @@ -55,6 +55,6 @@ export const UPGRADE_RULE_SKIPPED = (skipped: number) =>

export const UPGRADE_RULE_FAILED = (failed: number) =>
i18n.translate('xpack.securitySolution.detectionEngine.prebuiltRules.toast.upgradeRuleFailed', {
defaultMessage: '{failed, plural, one {# rule} other {# rules}} failed to update.',
defaultMessage: '{failed, plural, one {# rule} other {# rules}} failed to update',
values: { failed },
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,58 +4,81 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import type { IToasts } from '@kbn/core/public';
import type { PerformRuleInstallationResponseBody } from '../../../../../common/api/detection_engine';
import { useToasts } from '../../../../common/lib/kibana';
import { usePerformAllRulesInstallMutation } from '../../api/hooks/prebuilt_rules/use_perform_all_rules_install_mutation';
import { usePerformSpecificRulesInstallMutation } from '../../api/hooks/prebuilt_rules/use_perform_specific_rules_install_mutation';

import * as i18n from './translations';
import { showErrorToast } from '../../../../common/components/utils';

export const usePerformInstallAllRules = () => {
const { addError, addSuccess } = useAppToasts();
const toasts = useToasts();

return usePerformAllRulesInstallMutation({
onError: (err) => {
addError(err, { title: i18n.RULE_INSTALLATION_FAILED });
onError: (error) => {
handleErrorResponse(error, toasts);
},
onSuccess: (result) => {
addSuccess(getSuccessToastMessage(result));
handleSuccessResponse(result, toasts);
},
});
};

export const usePerformInstallSpecificRules = () => {
const { addError, addSuccess } = useAppToasts();
const toasts = useToasts();

return usePerformSpecificRulesInstallMutation({
onError: (err) => {
addError(err, { title: i18n.RULE_INSTALLATION_FAILED });
onError: (error) => {
handleErrorResponse(error, toasts);
},
onSuccess: (result) => {
addSuccess(getSuccessToastMessage(result));
handleSuccessResponse(result, toasts);
},
});
};

const getSuccessToastMessage = (result: {
function handleErrorResponse(error: unknown, toasts: IToasts) {
showErrorToast({
title: i18n.RULE_INSTALLATION_FAILED,
fullMessage: JSON.stringify(error, null, 2),
toasts,
});
}

function handleSuccessResponse(result: PerformRuleInstallationResponseBody, toasts: IToasts) {
const successToastMessage = getSuccessToastMessage(result);
if (successToastMessage) {
toasts.addSuccess(successToastMessage);
}

if (result.summary.failed > 0) {
showErrorToast({
title: i18n.INSTALL_RULE_FAILED(result.summary.failed),
fullMessage: JSON.stringify(result.errors, null, 2),
toasts,
});
}
}

function getSuccessToastMessage(result: {
summary: {
total: number;
succeeded: number;
skipped: number;
failed: number;
};
}) => {
}): string {
const toastMessages: string[] = [];
const {
summary: { succeeded, skipped, failed },
summary: { succeeded, skipped },
} = result;
if (succeeded > 0) {
toastMessages.push(i18n.INSTALL_RULE_SUCCESS(succeeded));
}
if (skipped > 0) {
toastMessages.push(i18n.INSTALL_RULE_SKIPPED(skipped));
}
if (failed > 0) {
toastMessages.push(i18n.INSTALL_RULE_FAILED(failed));
}
return toastMessages.join(' ');
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,42 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';

import { showErrorToast } from '../../../../common/components/utils';
import { useToasts } from '../../../../common/lib/kibana';
import { usePerformRulesUpgradeMutation } from '../../api/hooks/prebuilt_rules/use_perform_rules_upgrade_mutation';

import * as i18n from './translations';

export const usePerformUpgradeRules = () => {
const { addError, addSuccess } = useAppToasts();
const toasts = useToasts();

return usePerformRulesUpgradeMutation({
onError: (err) => {
addError(err, { title: i18n.RULE_UPGRADE_FAILED });
onError: (error) => {
showErrorToast({
title: i18n.RULE_UPGRADE_FAILED,
fullMessage: JSON.stringify(error, null, 2),
toasts,
});
},
onSuccess: (result, vars) => {
if (vars.dry_run) {
// This is a preflight check, no need to show toast
return;
}
addSuccess(getSuccessToastMessage(result));

const successToastMessage = getSuccessToastMessage(result);
if (successToastMessage) {
toasts.addSuccess(getSuccessToastMessage(result));
}

if (result.summary.failed > 0) {
showErrorToast({
title: i18n.UPGRADE_RULE_FAILED(result.summary.failed),
fullMessage: JSON.stringify(result.errors, null, 2),
toasts,
});
}
},
});
};
Expand All @@ -36,16 +54,13 @@ const getSuccessToastMessage = (result: {
}) => {
const toastMessage: string[] = [];
const {
summary: { succeeded, skipped, failed },
summary: { succeeded, skipped },
} = result;
if (succeeded > 0) {
toastMessage.push(i18n.UPGRADE_RULE_SUCCESS(succeeded));
}
if (skipped > 0) {
toastMessage.push(i18n.UPGRADE_RULE_SKIPPED(skipped));
}
if (failed > 0) {
toastMessage.push(i18n.UPGRADE_RULE_FAILED(failed));
}
return toastMessage.join(' ');
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { IToasts } from '@kbn/core/public';
import * as i18n from './translations';

import type { ErrorSchema, ImportRulesResponse } from '../../../../../common/api/detection_engine';
import { showErrorToast } from '../../../../common/components/utils';

export function getFailedConnectorsCount(actionConnectorsErrors: ErrorSchema[]) {
const connectorIds = new Set(
Expand Down Expand Up @@ -46,25 +47,6 @@ function getUserFriendlyConnectorMessages(actionConnectorsErrors: ErrorSchema[])
return mappedErrors;
}

function showErrorToast({
title,
shortMessage,
fullMessage,
toasts,
}: {
title: string;
shortMessage: string;
fullMessage: string;
toasts: IToasts;
}) {
const error = new Error('Error details');
error.stack = fullMessage;
toasts.addError(error, {
title,
toastMessage: shortMessage,
});
}

export function showToast({
importResponse,
toasts,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { SecuritySolutionPluginRouter } from '../../../../../types';
import { buildSiemResponse } from '../../../routes/utils';
import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client';
import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client';
import { excludeLicenseRestrictedRules, getPossibleUpgrades } from '../../logic/utils';

export const getPrebuiltRulesStatusRoute = (router: SecuritySolutionPluginRouter) => {
router.versioned
Expand All @@ -33,11 +34,12 @@ export const getPrebuiltRulesStatusRoute = (router: SecuritySolutionPluginRouter
const siemResponse = buildSiemResponse(response);

try {
const ctx = await context.resolve(['core', 'alerting']);
const ctx = await context.resolve(['core', 'alerting', 'securitySolution']);
const soClient = ctx.core.savedObjects.client;
const rulesClient = await ctx.alerting.getRulesClient();
const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient);
const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient);
const mlAuthz = ctx.securitySolution.getMlAuthz();

const currentRuleVersions = await ruleObjectsClient.fetchInstalledRuleVersions();
const latestRuleVersions = await ruleAssetsClient.fetchLatestVersions();
Expand All @@ -47,24 +49,39 @@ export const getPrebuiltRulesStatusRoute = (router: SecuritySolutionPluginRouter
const latestRuleVersionsMap = new Map(
latestRuleVersions.map((rule) => [rule.rule_id, rule])
);
const installableRules = latestRuleVersions.filter(
const allInstallableRules = latestRuleVersions.filter(
(rule) => !currentRuleVersionsMap.has(rule.rule_id)
);
const upgradeableRules = currentRuleVersions.filter((rule) => {
const latestVersion = latestRuleVersionsMap.get(rule.rule_id);
return latestVersion != null && rule.version < latestVersion.version;
});

const installableRuleAssets = await excludeLicenseRestrictedRules(
allInstallableRules,
mlAuthz
);

const upgradableRules = await getPossibleUpgrades(
currentRuleVersions,
latestRuleVersionsMap,
mlAuthz
);

const upgradeableRulesTags = upgradableRules.reduce<string[]>((tags, rule) => {
const ruleTags = currentRuleVersionsMap.get(rule.rule_id)?.tags;
if (ruleTags) {
tags.push(...ruleTags);
}
return tags;
}, []);

const body: GetPrebuiltRulesStatusResponseBody = {
stats: {
num_prebuilt_rules_installed: currentRuleVersions.length,
num_prebuilt_rules_to_install: installableRules.length,
num_prebuilt_rules_to_upgrade: upgradeableRules.length,
num_prebuilt_rules_to_install: installableRuleAssets.length,
num_prebuilt_rules_to_upgrade: upgradableRules.length,
num_prebuilt_rules_total_in_package: latestRuleVersions.length,
},
aggregated_fields: {
upgradeable_rules: {
tags: [...new Set(upgradeableRules.flatMap((rule) => rule.tags))],
tags: [...new Set(upgradeableRulesTags)],
},
},
};
Expand Down
Loading