diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 34694ea652caf..ec3c86097117c 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -126,6 +126,7 @@ export enum SecurityPageName { policies = 'policy', responseActionsHistory = 'response_actions_history', rules = 'rules', + rulesAdd = 'rules-add', rulesCreate = 'rules-create', sessions = 'sessions', /* @@ -158,6 +159,7 @@ export const DETECTIONS_PATH = '/detections' as const; export const ALERTS_PATH = '/alerts' as const; export const ALERT_DETAILS_REDIRECT_PATH = `${ALERTS_PATH}/redirect` as const; export const RULES_PATH = '/rules' as const; +export const RULES_ADD_PATH = `${RULES_PATH}/add_rules` as const; export const RULES_CREATE_PATH = `${RULES_PATH}/create` as const; export const EXCEPTIONS_PATH = '/exceptions' as const; export const EXCEPTION_LIST_DETAIL_PATH = `${EXCEPTIONS_PATH}/details/:detailName` as const; diff --git a/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_request_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_request_schema.ts index 0fede224254e6..3dc0c3619b2ee 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_request_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_request_schema.ts @@ -14,6 +14,8 @@ export const RuleVersionSpecifier = t.exact( ); export type RuleVersionSpecifier = t.TypeOf; +export type InstallSpecificRulesRequest = t.TypeOf; + export const InstallSpecificRulesRequest = t.exact( t.type({ mode: t.literal(`SPECIFIC_RULES`), @@ -21,6 +23,8 @@ export const InstallSpecificRulesRequest = t.exact( }) ); +export type InstallAllRulesRequest = t.TypeOf; + export const InstallAllRulesRequest = t.exact( t.type({ mode: t.literal(`ALL_RULES`), diff --git a/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_request_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_request_schema.ts index e0cfe9427dc97..eb92782f4d916 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_request_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_request_schema.ts @@ -22,11 +22,11 @@ export const RuleUpgradeSpecifier = t.exact( rule_id: t.string, /** * This parameter is needed for handling race conditions with Optimistic Concurrency Control. - * Two or more users can call installation/_review and installation/_perform endpoints concurrently. + * Two or more users can call upgrade/_review and upgrade/_perform endpoints concurrently. * Also, in general the time between these two calls can be anything. * The idea is to only allow the user to install a rule if the user has reviewed the exact version * of it that had been returned from the _review endpoint. If the version changed on the BE, - * installation/_perform endpoint will return a version mismatch error for this rule. + * upgrade/_perform endpoint will return a version mismatch error for this rule. */ revision: t.number, /** @@ -41,6 +41,7 @@ export const RuleUpgradeSpecifier = t.exact( ); export type RuleUpgradeSpecifier = t.TypeOf; +export type UpgradeSpecificRulesRequest = t.TypeOf; export const UpgradeSpecificRulesRequest = t.exact( t.intersection([ t.type({ diff --git a/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/review_rule_upgrade/response_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/review_rule_upgrade/response_schema.ts index 06c45008caf6e..52e12bc49fac6 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/review_rule_upgrade/response_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/review_rule_upgrade/response_schema.ts @@ -30,4 +30,5 @@ export interface RuleUpgradeInfoForReview { rule_id: RuleSignatureId; rule: DiffableRule; diff: PartialRuleDiff; + revision: number; } diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 68f4308384649..131d9a52cc5bb 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -46,12 +46,6 @@ export const allowedExperimentalValues = Object.freeze({ */ extendedRuleExecutionLoggingEnabled: false, - /** - * Enables the new API and UI for https://github.com/elastic/security-team/issues/1974. - * It's a temporary feature flag that will be removed once the feature gets a basic production-ready implementation. - */ - prebuiltRulesNewUpgradeAndInstallationWorkflowsEnabled: false, - /** * Enables the SOC trends timerange and stats on D&R page */ diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules.cy.ts index 549b46a0b886e..b07555156b351 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules.cy.ts @@ -34,7 +34,6 @@ import { testAllTagsBadges, testTagsBadge, testMultipleSelectedRulesLabel, - loadPrebuiltDetectionRulesFromHeaderBtn, filterByElasticRules, clickErrorToastBtn, unselectRuleByName, @@ -90,7 +89,10 @@ import { } from '../../objects/rule'; import { esArchiverResetKibana } from '../../tasks/es_archiver'; -import { getAvailablePrebuiltRulesCount } from '../../tasks/api_calls/prebuilt_rules'; +import { + getAvailablePrebuiltRulesCount, + excessivelyInstallAllPrebuiltRules, +} from '../../tasks/api_calls/prebuilt_rules'; import { setRowsPerPageTo } from '../../tasks/table_pagination'; const RULE_NAME = 'Custom rule for bulk actions'; @@ -151,7 +153,7 @@ describe('Detection rules, bulk edit', () => { it('Only prebuilt rules selected', () => { const expectedNumberOfSelectedRules = 10; - loadPrebuiltDetectionRulesFromHeaderBtn(); + excessivelyInstallAllPrebuiltRules(); // select Elastic(prebuilt) rules, check if we can't proceed further, as Elastic rules are not editable filterByElasticRules(); @@ -167,7 +169,7 @@ describe('Detection rules, bulk edit', () => { }); it('Prebuilt and custom rules selected: user proceeds with custom rules editing', () => { - loadPrebuiltDetectionRulesFromHeaderBtn(); + excessivelyInstallAllPrebuiltRules(); // modal window should show how many rules can be edit, how many not selectAllRules(); @@ -190,7 +192,7 @@ describe('Detection rules, bulk edit', () => { }); it('Prebuilt and custom rules selected: user cancels action', () => { - loadPrebuiltDetectionRulesFromHeaderBtn(); + excessivelyInstallAllPrebuiltRules(); // modal window should show how many rules can be edit, how many not selectAllRules(); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_actions.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_actions.cy.ts index 624dec7f1c955..2845c3b2b7adf 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_actions.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_actions.cy.ts @@ -32,7 +32,6 @@ import { import { waitForRulesTableToBeLoaded, selectNumberOfRules, - loadPrebuiltDetectionRulesFromHeaderBtn, goToEditRuleActionsSettingsOf, } from '../../tasks/alerts_detection_rules'; import { @@ -58,6 +57,7 @@ import { getMachineLearningRule, getNewTermsRule, } from '../../objects/rule'; +import { excessivelyInstallAllPrebuiltRules } from '../../tasks/api_calls/prebuilt_rules'; const ruleNameToAssert = 'Custom rule name with actions'; const expectedNumberOfCustomRulesToBeEdited = 7; @@ -136,7 +136,7 @@ describe.skip('Detection rules, bulk edit of rule actions', () => { throttleUnit: 'd', }; - loadPrebuiltDetectionRulesFromHeaderBtn(); + excessivelyInstallAllPrebuiltRules(); // select both custom and prebuilt rules selectNumberOfRules(expectedNumberOfRulesToBeEdited); @@ -164,7 +164,7 @@ describe.skip('Detection rules, bulk edit of rule actions', () => { }); it('Overwrite rule actions in rules', () => { - loadPrebuiltDetectionRulesFromHeaderBtn(); + excessivelyInstallAllPrebuiltRules(); // select both custom and prebuilt rules selectNumberOfRules(expectedNumberOfRulesToBeEdited); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/export_rule.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/export_rule.cy.ts index 2065b914b8168..ee0dc9379c613 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/export_rule.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/export_rule.cy.ts @@ -15,7 +15,6 @@ import { TOASTER, } from '../../screens/alerts_detection_rules'; import { - loadPrebuiltDetectionRulesFromHeaderBtn, filterByElasticRules, selectNumberOfRules, bulkExportRules, @@ -32,7 +31,10 @@ import { cleanKibana, resetRulesTableState, deleteAlertsAndRules } from '../../t import { login, visitWithoutDateRange } from '../../tasks/login'; import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation'; -import { getAvailablePrebuiltRulesCount } from '../../tasks/api_calls/prebuilt_rules'; +import { + excessivelyInstallAllPrebuiltRules, + getAvailablePrebuiltRulesCount, +} from '../../tasks/api_calls/prebuilt_rules'; const EXPORTED_RULES_FILENAME = 'rules_export.ndjson'; const exceptionList = getExceptionList(); @@ -83,7 +85,7 @@ describe('Export rules', () => { it('shows a modal saying that no rules can be exported if all the selected rules are prebuilt', function () { const expectedElasticRulesCount = 7; - loadPrebuiltDetectionRulesFromHeaderBtn(); + excessivelyInstallAllPrebuiltRules(); filterByElasticRules(); selectNumberOfRules(expectedElasticRulesCount); @@ -97,7 +99,7 @@ describe('Export rules', () => { it('exports only custom rules', function () { const expectedNumberCustomRulesToBeExported = 1; - loadPrebuiltDetectionRulesFromHeaderBtn(); + excessivelyInstallAllPrebuiltRules(); selectAllRules(); bulkExportRules(); @@ -149,7 +151,7 @@ describe('Export rules', () => { // one rule with exception, one without it const expectedNumberCustomRulesToBeExported = 2; - loadPrebuiltDetectionRulesFromHeaderBtn(); + excessivelyInstallAllPrebuiltRules(); selectAllRules(); bulkExportRules(); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/prebuilt_rules.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/prebuilt_rules.cy.ts index 532bf5bf0bf24..024797b7fde78 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/prebuilt_rules.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/prebuilt_rules.cy.ts @@ -15,6 +15,7 @@ import { RULES_MANAGEMENT_TABLE, RULE_SWITCH, SELECT_ALL_RULES_ON_PAGE_CHECKBOX, + INSTALL_ALL_RULES_BUTTON, } from '../../screens/alerts_detection_rules'; import { confirmRulesDelete, @@ -22,14 +23,15 @@ import { deleteSelectedRules, disableSelectedRules, enableSelectedRules, - loadPrebuiltDetectionRules, - reloadDeletedRules, selectAllRules, selectNumberOfRules, waitForPrebuiltDetectionRulesToBeLoaded, waitForRuleToUpdate, } from '../../tasks/alerts_detection_rules'; -import { getAvailablePrebuiltRulesCount } from '../../tasks/api_calls/prebuilt_rules'; +import { + excessivelyInstallAllPrebuiltRules, + getAvailablePrebuiltRulesCount, +} from '../../tasks/api_calls/prebuilt_rules'; import { cleanKibana, deleteAlertsAndRules } from '../../tasks/common'; import { login, visitWithoutDateRange } from '../../tasks/login'; import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation'; @@ -43,7 +45,8 @@ describe('Prebuilt rules', () => { login(); deleteAlertsAndRules(); visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); - loadPrebuiltDetectionRules(); + excessivelyInstallAllPrebuiltRules(); + cy.reload(); waitForPrebuiltDetectionRulesToBeLoaded(); }); @@ -111,15 +114,19 @@ describe('Prebuilt rules', () => { 'have.text', `Elastic rules (${expectedNumberOfRulesAfterDeletion})` ); - cy.get(LOAD_PREBUILT_RULES_ON_PAGE_HEADER_BTN).should('exist'); - cy.get(LOAD_PREBUILT_RULES_ON_PAGE_HEADER_BTN).should( - 'have.text', - 'Install 1 Elastic prebuilt rule ' - ); + cy.get(LOAD_PREBUILT_RULES_ON_PAGE_HEADER_BTN).should('have.text', `Add Elastic rules1`); - reloadDeletedRules(); + // Navigate to the prebuilt rule installation page + cy.get(LOAD_PREBUILT_RULES_ON_PAGE_HEADER_BTN).click(); - cy.get(LOAD_PREBUILT_RULES_ON_PAGE_HEADER_BTN).should('not.exist'); + // Click the "Install all rules" button + cy.get(INSTALL_ALL_RULES_BUTTON).click(); + + // Wait for the rules to be installed + cy.get(INSTALL_ALL_RULES_BUTTON).should('be.disabled'); + + // Navigate back to the rules page + cy.go('back'); cy.get(ELASTIC_RULES_BTN).should( 'have.text', @@ -137,20 +144,28 @@ describe('Prebuilt rules', () => { selectNumberOfRules(numberOfRulesToBeSelected); deleteSelectedRules(); - cy.get(LOAD_PREBUILT_RULES_ON_PAGE_HEADER_BTN).should('exist'); cy.get(LOAD_PREBUILT_RULES_ON_PAGE_HEADER_BTN).should( 'have.text', - `Install ${numberOfRulesToBeSelected} Elastic prebuilt rules ` + `Add Elastic rules${numberOfRulesToBeSelected}` ); cy.get(ELASTIC_RULES_BTN).should( 'have.text', `Elastic rules (${expectedNumberOfRulesAfterDeletion})` ); - reloadDeletedRules(); + // Navigate to the prebuilt rule installation page + cy.get(LOAD_PREBUILT_RULES_ON_PAGE_HEADER_BTN).click(); + + // Click the "Install all rules" button + cy.get(INSTALL_ALL_RULES_BUTTON).click(); + + // Wait for the rules to be installed + cy.get(INSTALL_ALL_RULES_BUTTON).should('be.disabled'); - cy.get(LOAD_PREBUILT_RULES_ON_PAGE_HEADER_BTN).should('not.exist'); + // Navigate back to the rules page + cy.go('back'); + // Check that the rules table contains all rules cy.get(ELASTIC_RULES_BTN).should( 'have.text', `Elastic rules (${expectedNumberOfRulesAfterRecovering})` diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/rules_selection.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/rules_selection.cy.ts index 37ac8ddadfe7a..43d2445b318b0 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/rules_selection.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/rules_selection.cy.ts @@ -10,12 +10,14 @@ import { SELECT_ALL_RULES_ON_PAGE_CHECKBOX, } from '../../screens/alerts_detection_rules'; import { - loadPrebuiltDetectionRules, selectNumberOfRules, unselectNumberOfRules, waitForPrebuiltDetectionRulesToBeLoaded, } from '../../tasks/alerts_detection_rules'; -import { getAvailablePrebuiltRulesCount } from '../../tasks/api_calls/prebuilt_rules'; +import { + excessivelyInstallAllPrebuiltRules, + getAvailablePrebuiltRulesCount, +} from '../../tasks/api_calls/prebuilt_rules'; import { cleanKibana } from '../../tasks/common'; import { login, visitWithoutDateRange } from '../../tasks/login'; import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation'; @@ -29,7 +31,7 @@ describe.skip('Rules selection', () => { }); it('should correctly update the selection label when rules are individually selected and unselected', () => { - loadPrebuiltDetectionRules(); + excessivelyInstallAllPrebuiltRules(); waitForPrebuiltDetectionRulesToBeLoaded(); selectNumberOfRules(2); @@ -42,7 +44,7 @@ describe.skip('Rules selection', () => { }); it('should correctly update the selection label when rules are bulk selected and then bulk un-selected', () => { - loadPrebuiltDetectionRules(); + excessivelyInstallAllPrebuiltRules(); waitForPrebuiltDetectionRulesToBeLoaded(); cy.get(SELECT_ALL_RULES_BTN).click(); @@ -63,7 +65,7 @@ describe.skip('Rules selection', () => { }); it('should correctly update the selection label when rules are bulk selected and then unselected via the table select all checkbox', () => { - loadPrebuiltDetectionRules(); + excessivelyInstallAllPrebuiltRules(); waitForPrebuiltDetectionRulesToBeLoaded(); cy.get(SELECT_ALL_RULES_BTN).click(); diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts index 8d8ac48a06782..a4383b48d2dd0 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts @@ -63,6 +63,8 @@ export const LOAD_PREBUILT_RULES_BTN = '[data-test-subj="load-prebuilt-rules"]'; export const LOAD_PREBUILT_RULES_ON_PAGE_HEADER_BTN = '[data-test-subj="loadPrebuiltRulesBtn"]'; +export const INSTALL_ALL_RULES_BUTTON = '[data-test-subj="installAllRulesButton"]'; + export const RULES_TABLE_INITIAL_LOADING_INDICATOR = '[data-test-subj="initialLoadingPanelAllRulesTable"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts index 27d6ee054c0d5..7f4a8fcacd0b1 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts @@ -14,7 +14,6 @@ import { DELETE_RULE_BULK_BTN, RULES_SELECTED_TAG, LOAD_PREBUILT_RULES_BTN, - LOAD_PREBUILT_RULES_ON_PAGE_HEADER_BTN, RULES_TABLE_INITIAL_LOADING_INDICATOR, RULES_TABLE_AUTOREFRESH_INDICATOR, RULE_CHECKBOX, @@ -72,7 +71,6 @@ import { EUI_CHECKBOX } from '../screens/common/controls'; import { ALL_ACTIONS } from '../screens/rule_details'; import { EDIT_SUBMIT_BUTTON } from '../screens/edit_rule'; import { LOADING_INDICATOR } from '../screens/security_header'; -import { waitTillPrebuiltRulesReadyToInstall } from './api_calls/prebuilt_rules'; import { goToRuleEditSettings } from './rule_details'; import { goToActionsStepTab } from './create_new_rule'; @@ -243,32 +241,10 @@ export const goToTheRuleDetailsOf = (ruleName: string) => { cy.contains(RULE_NAME, ruleName).click(); }; -export const loadPrebuiltDetectionRules = () => { - cy.log('load prebuilt detection rules'); - waitTillPrebuiltRulesReadyToInstall(); - cy.get(LOAD_PREBUILT_RULES_BTN, { timeout: 300000 }).should('be.enabled'); - cy.get(LOAD_PREBUILT_RULES_BTN).click(); - cy.get(LOAD_PREBUILT_RULES_BTN).should('be.disabled'); -}; - -/** - * load prebuilt rules by clicking button on page header - */ -export const loadPrebuiltDetectionRulesFromHeaderBtn = () => { - cy.log('load prebuilt detection rules from header'); - waitTillPrebuiltRulesReadyToInstall(); - cy.get(LOAD_PREBUILT_RULES_ON_PAGE_HEADER_BTN, { timeout: 300000 }).click(); - cy.get(LOAD_PREBUILT_RULES_ON_PAGE_HEADER_BTN, { timeout: 300000 }).should('not.exist'); -}; - export const openIntegrationsPopover = () => { cy.get(INTEGRATIONS_POPOVER).click(); }; -export const reloadDeletedRules = () => { - cy.get(LOAD_PREBUILT_RULES_ON_PAGE_HEADER_BTN).click(); -}; - /** * Selects the number of rules. Since there can be missing click handlers * when the page loads at first, we use a pipe and a trigger of click diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/prebuilt_rules.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/prebuilt_rules.ts index 2d8d8b7bf49e0..424426e91d793 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/prebuilt_rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/prebuilt_rules.ts @@ -15,6 +15,17 @@ export const getPrebuiltRulesStatus = () => { }); }; +export const installAllPrebuiltRulesRequest = () => { + return cy.request({ + method: 'POST', + url: 'internal/detection_engine/prebuilt_rules/installation/_perform', + headers: { 'kbn-xsrf': 'cypress-creds' }, + body: { + mode: 'ALL_RULES', + }, + }); +}; + export const getAvailablePrebuiltRulesCount = () => { cy.log('Get prebuilt rules count'); return getPrebuiltRulesStatus().then(({ body }) => { @@ -34,3 +45,16 @@ export const waitTillPrebuiltRulesReadyToInstall = () => { { interval: 2000, timeout: 60000 } ); }; + +/** + * Install all prebuilt rules. + * + * This is a heavy request and should be used with caution. Most likely you + * don't need all prebuilt rules to be installed, crating just a few prebuilt + * rules should be enough for most cases. + */ +export const excessivelyInstallAllPrebuiltRules = () => { + cy.log('Install prebuilt rules (heavy request)'); + waitTillPrebuiltRulesReadyToInstall(); + installAllPrebuiltRulesRequest(); +}; diff --git a/x-pack/plugins/security_solution/public/app/translations.ts b/x-pack/plugins/security_solution/public/app/translations.ts index 1c75cb7bba595..50c3fae8f8ad2 100644 --- a/x-pack/plugins/security_solution/public/app/translations.ts +++ b/x-pack/plugins/security_solution/public/app/translations.ts @@ -64,6 +64,10 @@ export const RULES = i18n.translate('xpack.securitySolution.navigation.rules', { defaultMessage: 'Rules', }); +export const ADD_RULES = i18n.translate('xpack.securitySolution.navigation.addRules', { + defaultMessage: 'Add Rules', +}); + export const EXCEPTIONS = i18n.translate('xpack.securitySolution.navigation.exceptions', { defaultMessage: 'Shared Exception Lists', }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/api.mock.ts b/x-pack/plugins/security_solution/public/common/components/ml_popover/api.mock.ts index 6915c5e4305d9..2000db1807cbf 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/api.mock.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/api.mock.ts @@ -418,4 +418,28 @@ export const mockSecurityJobs: SecurityJob[] = [ security_app_display_name: 'Unusually Windows Processes', }, }, + { + datafeedId: 'datafeed-siem-api-rare_process_linux_ecs', + datafeedIndices: ['auditbeat-*'], + datafeedState: 'failed', + description: 'SIEM Auditbeat: Detect unusually rare processes on Linux (beta)', + earliestTimestampMs: 1561651364098, + groups: ['siem'], + hasDatafeed: true, + id: 'siem-api-rare_process_linux_ecs', + isSingleMetricViewerJob: true, + jobState: 'closed', + latestTimestampMs: 1562870521264, + memory_status: 'hard_limit', + nodeName: 'siem-es', + processed_record_count: 3425264, + awaitingNodeAssignment: false, + jobTags: {}, + bucketSpanSeconds: 900, + moduleId: 'security_linux_v3', + defaultIndexPattern: 'auditbeat-*', + isCompatible: true, + isInstalled: true, + isElasticJob: true, + }, ]; diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/helpers.test.tsx index 7811710cfd6da..3af90ea528eec 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/helpers.test.tsx @@ -10,7 +10,7 @@ import { filterJobs, getStablePatternTitles, searchFilter } from './helpers'; describe('helpers', () => { describe('filterJobs', () => { - test('returns all jobs when no filter is suplied', () => { + test('returns all jobs when no filter is supplied', () => { const filteredJobs = filterJobs({ jobs: mockSecurityJobs, selectedGroups: [], @@ -18,24 +18,24 @@ describe('helpers', () => { showElasticJobs: false, filterQuery: '', }); - expect(filteredJobs.length).toEqual(3); + expect(filteredJobs.length).toEqual(mockSecurityJobs.length); }); }); describe('searchFilter', () => { - test('returns all jobs when nullfilterQuery is provided', () => { + test('returns all jobs when null filterQuery is provided', () => { const jobsToDisplay = searchFilter(mockSecurityJobs); expect(jobsToDisplay.length).toEqual(mockSecurityJobs.length); }); test('returns correct DisplayJobs when filterQuery matches job.id', () => { const jobsToDisplay = searchFilter(mockSecurityJobs, 'rare_process'); - expect(jobsToDisplay.length).toEqual(2); + expect(jobsToDisplay.length).toEqual(3); }); test('returns correct DisplayJobs when filterQuery matches job.description', () => { const jobsToDisplay = searchFilter(mockSecurityJobs, 'Detect unusually'); - expect(jobsToDisplay.length).toEqual(2); + expect(jobsToDisplay.length).toEqual(3); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/__snapshots__/jobs_table.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/__snapshots__/jobs_table.test.tsx.snap deleted file mode 100644 index 76ac5ef287004..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/__snapshots__/jobs_table.test.tsx.snap +++ /dev/null @@ -1,138 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`JobsTableComponent renders correctly against snapshot 1`] = ` - - } - onChange={[Function]} - pagination={ - Object { - "pageIndex": 0, - "pageSize": 5, - "showPerPageOptions": false, - "totalItemCount": 3, - } - } - responsive={false} - tableLayout="fixed" -/> -`; diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap deleted file mode 100644 index d6fb52aa3acac..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap +++ /dev/null @@ -1,148 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`JobsTableFilters renders correctly against snapshot 1`] = ` - - - - - - - - - - - - - Elastic jobs - - - Custom jobs - - - - -`; diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx index 16879a0c22cc3..6156a01c70485 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { mount, shallow } from 'enzyme'; +import { mount } from 'enzyme'; import React from 'react'; import { JobsTableFiltersComponent } from './jobs_table_filters'; import type { SecurityJob } from '../../types'; @@ -19,13 +19,6 @@ describe('JobsTableFilters', () => { securityJobs = cloneDeep(mockSecurityJobs); }); - test('renders correctly against snapshot', () => { - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - }); - test('when you click Elastic Jobs filter, state is updated and it is selected', () => { const onFilterChanged = jest.fn(); const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx index 63a0fbb55d973..48308202c1a26 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { shallow, mount } from 'enzyme'; +import { mount } from 'enzyme'; import React from 'react'; import { render, waitFor } from '@testing-library/react'; import { JobsTableComponent } from './jobs_table'; @@ -33,18 +33,6 @@ describe('JobsTableComponent', () => { onJobStateChangeMock = jest.fn(); }); - test('renders correctly against snapshot', () => { - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - }); - test('should render the hyperlink which points specifically to the job id', async () => { const href = await getRenderedHref( () => ( diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts index fdaa1e04a00e1..cfbffae6ad8de 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts @@ -103,6 +103,7 @@ const getTrailingBreadcrumbsForRoutes = ( case SecurityPageName.users: return getUsersBreadcrumbs(spyState, getSecuritySolutionUrl); case SecurityPageName.rules: + case SecurityPageName.rulesAdd: case SecurityPageName.rulesCreate: return getDetectionRulesBreadcrumbs(spyState, getSecuritySolutionUrl); case SecurityPageName.exceptions: diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/tab_navigation.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/tab_navigation.tsx index f903611a3bf96..24f50df4a5c67 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/tab_navigation.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/tab_navigation.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiTab, EuiTabs, EuiBetaBadge } from '@elastic/eui'; +import { EuiTab, EuiTabs, EuiBadge } from '@elastic/eui'; import { getOr } from 'lodash/fp'; import React, { useEffect, useState, useCallback, useMemo } from 'react'; import { useLocation } from 'react-router-dom'; @@ -49,7 +49,7 @@ const TabNavigationItemComponent = ({ isSelected={isSelected} href={appHref} onClick={handleClick} - append={isBeta && } + append={isBeta && {betaOptions?.text ?? BETA}} > {name} diff --git a/x-pack/plugins/security_solution/public/common/components/utility_bar/styles.tsx b/x-pack/plugins/security_solution/public/common/components/utility_bar/styles.tsx index 11886504eaa20..52740c0b3213d 100644 --- a/x-pack/plugins/security_solution/public/common/components/utility_bar/styles.tsx +++ b/x-pack/plugins/security_solution/public/common/components/utility_bar/styles.tsx @@ -30,7 +30,8 @@ export const Bar = styled.aside.attrs({ ${border && css` border-bottom: ${theme.eui.euiBorderThin}; - padding-bottom: ${theme.eui.euiSizeS}; + padding-bottom: ${theme.eui.euiSizeXS}; + align-items: center; `} @media only screen and (min-width: ${theme.eui.euiBreakpoints.l}) { @@ -47,6 +48,7 @@ export const BarSection = styled.div.attrs({ ${({ grow, theme }) => css` & + & { margin-top: ${theme.eui.euiSizeS}; + align-items: center; } @media only screen and (min-width: ${theme.eui.euiBreakpoints.m}) { diff --git a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar.test.tsx b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar.test.tsx index e753b4ff40560..9f21ccbb9c0b4 100644 --- a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import { euiDarkVars } from '@kbn/ui-theme'; import { mount, shallow } from 'enzyme'; import React from 'react'; @@ -85,8 +84,8 @@ describe('UtilityBar', () => { ); const siemUtilityBar = wrapper.find('.siemUtilityBar').first(); - expect(siemUtilityBar).toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin); - expect(siemUtilityBar).toHaveStyleRule('padding-bottom', euiDarkVars.euiSizeS); + expect(siemUtilityBar).toHaveStyleRule('border-bottom', expect.any(String)); + expect(siemUtilityBar).toHaveStyleRule('padding-bottom', expect.any(String)); }); test('it DOES NOT apply border styles when border is false', () => { @@ -121,7 +120,7 @@ describe('UtilityBar', () => { ); const siemUtilityBar = wrapper.find('.siemUtilityBar').first(); - expect(siemUtilityBar).not.toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin); - expect(siemUtilityBar).not.toHaveStyleRule('padding-bottom', euiDarkVars.euiSizeS); + expect(siemUtilityBar).not.toHaveStyleRule('border-bottom', expect.any(String)); + expect(siemUtilityBar).not.toHaveStyleRule('padding-bottom', expect.any(String)); }); }); diff --git a/x-pack/plugins/security_solution/public/common/utils/global_query_string/helpers.ts b/x-pack/plugins/security_solution/public/common/utils/global_query_string/helpers.ts index a8120cd0b99cc..a5b3bb1c8d31f 100644 --- a/x-pack/plugins/security_solution/public/common/utils/global_query_string/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/utils/global_query_string/helpers.ts @@ -17,6 +17,7 @@ import { SecurityPageName } from '../../../app/types'; export const isDetectionsPages = (pageName: string) => pageName === SecurityPageName.alerts || pageName === SecurityPageName.rules || + pageName === SecurityPageName.rulesAdd || pageName === SecurityPageName.rulesCreate || pageName === SecurityPageName.exceptions; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.test.ts index d1e67923d7b56..8ec7f77531653 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.test.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.test.ts @@ -28,7 +28,6 @@ import { patchRule, fetchRules, fetchRuleById, - createPrepackagedRules, importRules, exportRules, getPrePackagedRulesStatus, @@ -435,34 +434,6 @@ describe('Detections Rules API', () => { }); }); - describe('createPrepackagedRules', () => { - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue({ - rules_installed: 0, - rules_updated: 0, - timelines_installed: 0, - timelines_updated: 0, - }); - }); - - test('check parameter url when creating pre-packaged rules', async () => { - await createPrepackagedRules(); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/prepackaged', { - method: 'PUT', - }); - }); - test('happy path', async () => { - const resp = await createPrepackagedRules(); - expect(resp).toEqual({ - rules_installed: 0, - rules_updated: 0, - timelines_installed: 0, - timelines_updated: 0, - }); - }); - }); - describe('importRules', () => { const fileToImport: File = { lastModified: 33, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts index 74eb06bb373e9..aff10f8a817aa 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts @@ -13,6 +13,11 @@ import { INTERNAL_ALERTING_API_FIND_RULES_PATH } from '@kbn/alerting-plugin/comm import type { BulkInstallPackagesResponse } from '@kbn/fleet-plugin/common'; import { epmRouteService } from '@kbn/fleet-plugin/common'; import type { InstallPackageResponse } from '@kbn/fleet-plugin/common/types'; +import type { UpgradeSpecificRulesRequest } from '../../../../common/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_request_schema'; +import type { PerformRuleUpgradeResponseBody } from '../../../../common/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_response_schema'; +import type { InstallSpecificRulesRequest } from '../../../../common/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_request_schema'; +import type { PerformRuleInstallationResponseBody } from '../../../../common/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_response_schema'; +import type { GetPrebuiltRulesStatusResponseBody } from '../../../../common/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/response_schema'; import type { RuleManagementFiltersResponse } from '../../../../common/detection_engine/rule_management/api/rules/filters/response_schema'; import { RULE_MANAGEMENT_FILTERS_URL } from '../../../../common/detection_engine/rule_management/api/urls'; import type { BulkActionsDryRunErrCode } from '../../../../common/constants'; @@ -24,21 +29,25 @@ import { } from '../../../../common/constants'; import { + GET_PREBUILT_RULES_STATUS_URL, + PERFORM_RULE_INSTALLATION_URL, + PERFORM_RULE_UPGRADE_URL, PREBUILT_RULES_STATUS_URL, - PREBUILT_RULES_URL, + REVIEW_RULE_INSTALLATION_URL, + REVIEW_RULE_UPGRADE_URL, } from '../../../../common/detection_engine/prebuilt_rules'; import type { RulesReferencedByExceptionListsSchema } from '../../../../common/detection_engine/rule_exceptions'; import { DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL } from '../../../../common/detection_engine/rule_exceptions'; import type { - BulkActionEditPayload, BulkActionDuplicatePayload, + BulkActionEditPayload, } from '../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema'; import { BulkActionType } from '../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema'; import type { - RuleResponse, PreviewResponse, + RuleResponse, } from '../../../../common/detection_engine/rule_schema'; import { KibanaServices } from '../../../common/lib/kibana'; @@ -62,6 +71,8 @@ import type { UpdateRulesProps, } from '../logic/types'; import { convertRulesFilterToKQL } from '../logic/utils'; +import type { ReviewRuleUpgradeResponseBody } from '../../../../common/detection_engine/prebuilt_rules/api/review_rule_upgrade/response_schema'; +import type { ReviewRuleInstallationResponseBody } from '../../../../common/detection_engine/prebuilt_rules/api/review_rule_installation/response_schema'; /** * Create provided Rule @@ -225,7 +236,6 @@ export const fetchRulesSnoozeSettings = async ({ return result; }, {} as RulesSnoozeSettingsMap); }; - export interface BulkActionSummary { failed: number; skipped: number; @@ -347,26 +357,6 @@ export interface CreatePrepackagedRulesResponse { timelines_updated: number; } -/** - * Create Prepackaged Rules - * - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const createPrepackagedRules = async (): Promise => { - const result = await KibanaServices.get().http.fetch<{ - rules_installed: number; - rules_updated: number; - timelines_installed: number; - timelines_updated: number; - }>(PREBUILT_RULES_URL, { - method: 'PUT', - }); - - return result; -}; - /** * Imports rules in the same format as exported via the _export API * @@ -584,3 +574,102 @@ export const bulkInstallFleetPackages = ({ } ); }; + +/** + * NEW PREBUILT RULES ROUTES START HERE! 👋 + * USE THESE ONES! THEY'RE THE NICE ONES, PROMISE! + */ + +/** + * Get prebuilt rules status + * + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const getPrebuiltRulesStatus = async ({ + signal, +}: { + signal: AbortSignal | undefined; +}): Promise => + KibanaServices.get().http.fetch( + GET_PREBUILT_RULES_STATUS_URL, + { + method: 'GET', + signal, + } + ); + +/** + * Review prebuilt rules upgrade + * + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const reviewRuleUpgrade = async ({ + signal, +}: { + signal: AbortSignal | undefined; +}): Promise => + KibanaServices.get().http.fetch(REVIEW_RULE_UPGRADE_URL, { + method: 'POST', + signal, + }); + +/** + * Review prebuilt rules install (new rules) + * + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const reviewRuleInstall = async ({ + signal, +}: { + signal: AbortSignal | undefined; +}): Promise => + KibanaServices.get().http.fetch(REVIEW_RULE_INSTALLATION_URL, { + method: 'POST', + signal, + }); + +export const performInstallAllRules = async (): Promise => + KibanaServices.get().http.fetch(PERFORM_RULE_INSTALLATION_URL, { + method: 'POST', + body: JSON.stringify({ + mode: 'ALL_RULES', + }), + }); + +export const performInstallSpecificRules = async ( + rules: InstallSpecificRulesRequest['rules'] +): Promise => + KibanaServices.get().http.fetch(PERFORM_RULE_INSTALLATION_URL, { + method: 'POST', + body: JSON.stringify({ + mode: 'SPECIFIC_RULES', + rules, + }), + }); + +export const performUpgradeAllRules = async (): Promise => + KibanaServices.get().http.fetch(PERFORM_RULE_UPGRADE_URL, { + method: 'POST', + body: JSON.stringify({ + mode: 'ALL_RULES', + pick_version: 'TARGET', + }), + }); + +export const performUpgradeSpecificRules = async ( + rules: UpgradeSpecificRulesRequest['rules'] +): Promise => + KibanaServices.get().http.fetch(PERFORM_RULE_UPGRADE_URL, { + method: 'POST', + body: JSON.stringify({ + mode: 'SPECIFIC_RULES', + rules, + pick_version: 'TARGET', // Setting fixed 'TARGET' temporarily for Milestone 2 + }), + }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_install_review_query.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_install_review_query.ts new file mode 100644 index 0000000000000..a67baf429521b --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_install_review_query.ts @@ -0,0 +1,48 @@ +/* + * 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 { useCallback } from 'react'; +import type { UseQueryOptions } from '@tanstack/react-query'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { reviewRuleInstall } from '../../api'; +import { REVIEW_RULE_INSTALLATION_URL } from '../../../../../../common/detection_engine/prebuilt_rules/api/urls'; +import type { ReviewRuleInstallationResponseBody } from '../../../../../../common/detection_engine/prebuilt_rules/api/review_rule_installation/response_schema'; +import { DEFAULT_QUERY_OPTIONS } from '../constants'; + +export const REVIEW_RULE_INSTALLATION_QUERY_KEY = ['POST', REVIEW_RULE_INSTALLATION_URL]; + +export const useFetchPrebuiltRulesInstallReviewQuery = ( + options?: UseQueryOptions +) => { + return useQuery( + REVIEW_RULE_INSTALLATION_QUERY_KEY, + async ({ signal }) => { + const response = await reviewRuleInstall({ signal }); + return response; + }, + { + ...DEFAULT_QUERY_OPTIONS, + ...options, + } + ); +}; + +/** + * We should use this hook to invalidate the prebuilt rules to install cache. For + * example, rule mutations that affect rule set size, like installing a rule, + * should lead to cache invalidation. + * + * @returns A rules cache invalidation callback + */ +export const useInvalidateFetchPrebuiltRulesInstallReviewQuery = () => { + const queryClient = useQueryClient(); + + return useCallback(() => { + queryClient.invalidateQueries(REVIEW_RULE_INSTALLATION_QUERY_KEY, { + refetchType: 'active', + }); + }, [queryClient]); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_prebuilt_rules_status_query.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_status_query.ts similarity index 58% rename from x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_prebuilt_rules_status_query.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_status_query.ts index 5fd22fae143cb..7865cedc2d314 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_prebuilt_rules_status_query.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_status_query.ts @@ -7,21 +7,21 @@ import { useCallback } from 'react'; import type { UseQueryOptions } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query'; -import { getPrePackagedRulesStatus } from '../api'; -import { DEFAULT_QUERY_OPTIONS } from './constants'; -import type { PrePackagedRulesStatusResponse } from '../../logic'; -import { PREBUILT_RULES_STATUS_URL } from '../../../../../common/detection_engine/prebuilt_rules/api/urls'; +import type { PrebuiltRulesStatusStats } from '../../../../../../common/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/response_schema'; +import { getPrebuiltRulesStatus } from '../../api'; +import { DEFAULT_QUERY_OPTIONS } from '../constants'; +import { GET_PREBUILT_RULES_STATUS_URL } from '../../../../../../common/detection_engine/prebuilt_rules/api/urls'; -export const PREBUILT_RULES_STATUS_QUERY_KEY = ['GET', PREBUILT_RULES_STATUS_URL]; +export const PREBUILT_RULES_STATUS_QUERY_KEY = ['GET', GET_PREBUILT_RULES_STATUS_URL]; export const useFetchPrebuiltRulesStatusQuery = ( - options?: UseQueryOptions + options?: UseQueryOptions ) => { - return useQuery( + return useQuery( PREBUILT_RULES_STATUS_QUERY_KEY, async ({ signal }) => { - const response = await getPrePackagedRulesStatus({ signal }); - return response; + const response = await getPrebuiltRulesStatus({ signal }); + return response.stats; }, { ...DEFAULT_QUERY_OPTIONS, @@ -32,8 +32,8 @@ export const useFetchPrebuiltRulesStatusQuery = ( /** * We should use this hook to invalidate the prepackaged rules cache. For - * example, rule mutations that affect rule set size, like creation or deletion, - * should lead to cache invalidation. + * example, rule mutations that affect rule set size, like creation, deletion, + * or installing and updating (which affect the stats) should lead to cache invalidation. * * @returns A rules cache invalidation callback */ diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_upgrade_review_query.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_upgrade_review_query.ts new file mode 100644 index 0000000000000..be6b3eb14207c --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_upgrade_review_query.ts @@ -0,0 +1,48 @@ +/* + * 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 { useCallback } from 'react'; +import type { UseQueryOptions } from '@tanstack/react-query'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { reviewRuleUpgrade } from '../../api'; +import { REVIEW_RULE_UPGRADE_URL } from '../../../../../../common/detection_engine/prebuilt_rules/api/urls'; +import type { ReviewRuleUpgradeResponseBody } from '../../../../../../common/detection_engine/prebuilt_rules/api/review_rule_upgrade/response_schema'; +import { DEFAULT_QUERY_OPTIONS } from '../constants'; + +export const REVIEW_RULE_UPGRADE_QUERY_KEY = ['POST', REVIEW_RULE_UPGRADE_URL]; + +export const useFetchPrebuiltRulesUpgradeReviewQuery = ( + options?: UseQueryOptions +) => { + return useQuery( + REVIEW_RULE_UPGRADE_QUERY_KEY, + async ({ signal }) => { + const response = await reviewRuleUpgrade({ signal }); + return response; + }, + { + ...DEFAULT_QUERY_OPTIONS, + ...options, + } + ); +}; + +/** + * We should use this hook to invalidate the prebuilt rules to upgrade cache. For + * example, rule mutations that affect rule set size, like upgrading a rule, + * should lead to cache invalidation. + * + * @returns A rules cache invalidation callback + */ +export const useInvalidateFetchPrebuiltRulesUpgradeReviewQuery = () => { + const queryClient = useQueryClient(); + + return useCallback(() => { + queryClient.invalidateQueries(REVIEW_RULE_UPGRADE_QUERY_KEY, { + refetchType: 'active', + }); + }, [queryClient]); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_all_rules_install_mutation.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_all_rules_install_mutation.ts new file mode 100644 index 0000000000000..ccb9be0b0f82a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_all_rules_install_mutation.ts @@ -0,0 +1,50 @@ +/* + * 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 type { UseMutationOptions } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; +import type { PerformRuleInstallationResponseBody } from '../../../../../../common/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_response_schema'; +import { PERFORM_RULE_INSTALLATION_URL } from '../../../../../../common/detection_engine/prebuilt_rules/api/urls'; +import { useInvalidateFetchPrebuiltRulesStatusQuery } from './use_fetch_prebuilt_rules_status_query'; +import { useInvalidateFindRulesQuery } from '../use_find_rules_query'; +import { useInvalidateFetchRuleManagementFiltersQuery } from '../use_fetch_rule_management_filters_query'; +import { useInvalidateFetchRulesSnoozeSettingsQuery } from '../use_fetch_rules_snooze_settings'; +import { useInvalidateFetchPrebuiltRulesInstallReviewQuery } from './use_fetch_prebuilt_rules_install_review_query'; +import { performInstallAllRules } from '../../api'; + +export const PERFORM_ALL_RULES_INSTALLATION_KEY = [ + 'POST', + 'ALL_RULES', + PERFORM_RULE_INSTALLATION_URL, +]; + +export const usePerformAllRulesInstallMutation = ( + options?: UseMutationOptions +) => { + const invalidateFindRulesQuery = useInvalidateFindRulesQuery(); + const invalidateFetchRulesSnoozeSettings = useInvalidateFetchRulesSnoozeSettingsQuery(); + const invalidateFetchRuleManagementFilters = useInvalidateFetchRuleManagementFiltersQuery(); + const invalidateFetchPrebuiltRulesInstallReview = + useInvalidateFetchPrebuiltRulesInstallReviewQuery(); + const invalidateRuleStatus = useInvalidateFetchPrebuiltRulesStatusQuery(); + + return useMutation(() => performInstallAllRules(), { + ...options, + mutationKey: PERFORM_ALL_RULES_INSTALLATION_KEY, + onSettled: (...args) => { + invalidateFindRulesQuery(); + invalidateFetchRulesSnoozeSettings(); + invalidateFetchRuleManagementFilters(); + + invalidateFetchPrebuiltRulesInstallReview(); + invalidateRuleStatus(); + + if (options?.onSettled) { + options.onSettled(...args); + } + }, + }); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_all_rules_upgrade_mutation.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_all_rules_upgrade_mutation.ts new file mode 100644 index 0000000000000..e1137acf146f0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_all_rules_upgrade_mutation.ts @@ -0,0 +1,46 @@ +/* + * 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 type { UseMutationOptions } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; +import type { PerformRuleUpgradeResponseBody } from '../../../../../../common/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_response_schema'; +import { PERFORM_RULE_UPGRADE_URL } from '../../../../../../common/detection_engine/prebuilt_rules/api/urls'; +import { useInvalidateFindRulesQuery } from '../use_find_rules_query'; +import { useInvalidateFetchRuleManagementFiltersQuery } from '../use_fetch_rule_management_filters_query'; +import { useInvalidateFetchRulesSnoozeSettingsQuery } from '../use_fetch_rules_snooze_settings'; +import { useInvalidateFetchPrebuiltRulesUpgradeReviewQuery } from './use_fetch_prebuilt_rules_upgrade_review_query'; +import { useInvalidateFetchPrebuiltRulesStatusQuery } from './use_fetch_prebuilt_rules_status_query'; +import { performUpgradeAllRules } from '../../api'; + +export const PERFORM_ALL_RULES_UPGRADE_KEY = ['POST', 'ALL_RULES', PERFORM_RULE_UPGRADE_URL]; + +export const usePerformAllRulesUpgradeMutation = ( + options?: UseMutationOptions +) => { + const invalidateFindRulesQuery = useInvalidateFindRulesQuery(); + const invalidateFetchRulesSnoozeSettings = useInvalidateFetchRulesSnoozeSettingsQuery(); + const invalidateFetchRuleManagementFilters = useInvalidateFetchRuleManagementFiltersQuery(); + const invalidateFetchPrebuiltRulesUpgradeReview = + useInvalidateFetchPrebuiltRulesUpgradeReviewQuery(); + const invalidateRuleStatus = useInvalidateFetchPrebuiltRulesStatusQuery(); + + return useMutation(() => performUpgradeAllRules(), { + ...options, + mutationKey: PERFORM_ALL_RULES_UPGRADE_KEY, + onSettled: (...args) => { + invalidateFindRulesQuery(); + invalidateFetchRulesSnoozeSettings(); + invalidateFetchRuleManagementFilters(); + + invalidateFetchPrebuiltRulesUpgradeReview(); + invalidateRuleStatus(); + + if (options?.onSettled) { + options.onSettled(...args); + } + }, + }); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_install_mutation.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_install_mutation.ts new file mode 100644 index 0000000000000..33d6c10f5ee95 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_install_mutation.ts @@ -0,0 +1,66 @@ +/* + * 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 type { UseMutationOptions } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; +import type { PerformRuleInstallationResponseBody } from '../../../../../../common/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_response_schema'; +import { PERFORM_RULE_INSTALLATION_URL } from '../../../../../../common/detection_engine/prebuilt_rules/api/urls'; +import { useInvalidateFetchPrebuiltRulesStatusQuery } from './use_fetch_prebuilt_rules_status_query'; +import { useInvalidateFindRulesQuery } from '../use_find_rules_query'; +import { useInvalidateFetchRuleManagementFiltersQuery } from '../use_fetch_rule_management_filters_query'; +import { useInvalidateFetchRulesSnoozeSettingsQuery } from '../use_fetch_rules_snooze_settings'; +import { useInvalidateFetchPrebuiltRulesInstallReviewQuery } from './use_fetch_prebuilt_rules_install_review_query'; +import type { InstallSpecificRulesRequest } from '../../../../../../common/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_request_schema'; +import { performInstallSpecificRules } from '../../api'; + +export const PERFORM_SPECIFIC_RULES_INSTALLATION_KEY = [ + 'POST', + 'SPECIFIC_RULES', + PERFORM_RULE_INSTALLATION_URL, +]; + +export const usePerformSpecificRulesInstallMutation = ( + options?: UseMutationOptions< + PerformRuleInstallationResponseBody, + Error, + InstallSpecificRulesRequest['rules'] + > +) => { + const invalidateFindRulesQuery = useInvalidateFindRulesQuery(); + const invalidateFetchRulesSnoozeSettings = useInvalidateFetchRulesSnoozeSettingsQuery(); + const invalidatePrePackagedRulesStatus = useInvalidateFetchPrebuiltRulesStatusQuery(); + const invalidateFetchRuleManagementFilters = useInvalidateFetchRuleManagementFiltersQuery(); + const invalidateFetchPrebuiltRulesInstallReview = + useInvalidateFetchPrebuiltRulesInstallReviewQuery(); + const invalidateRuleStatus = useInvalidateFetchPrebuiltRulesStatusQuery(); + + return useMutation< + PerformRuleInstallationResponseBody, + Error, + InstallSpecificRulesRequest['rules'] + >( + (rulesToInstall: InstallSpecificRulesRequest['rules']) => { + return performInstallSpecificRules(rulesToInstall); + }, + { + ...options, + mutationKey: PERFORM_SPECIFIC_RULES_INSTALLATION_KEY, + onSettled: (...args) => { + invalidatePrePackagedRulesStatus(); + invalidateFindRulesQuery(); + invalidateFetchRulesSnoozeSettings(); + invalidateFetchRuleManagementFilters(); + + invalidateFetchPrebuiltRulesInstallReview(); + invalidateRuleStatus(); + + if (options?.onSettled) { + options.onSettled(...args); + } + }, + } + ); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_upgrade_mutation.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_upgrade_mutation.ts new file mode 100644 index 0000000000000..501e01732c962 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_upgrade_mutation.ts @@ -0,0 +1,62 @@ +/* + * 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 type { UseMutationOptions } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; +import type { PerformRuleUpgradeResponseBody } from '../../../../../../common/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_response_schema'; +import { PERFORM_RULE_UPGRADE_URL } from '../../../../../../common/detection_engine/prebuilt_rules/api/urls'; +import { useInvalidateFetchPrebuiltRulesStatusQuery } from './use_fetch_prebuilt_rules_status_query'; +import { useInvalidateFindRulesQuery } from '../use_find_rules_query'; +import { useInvalidateFetchRuleManagementFiltersQuery } from '../use_fetch_rule_management_filters_query'; +import { useInvalidateFetchRulesSnoozeSettingsQuery } from '../use_fetch_rules_snooze_settings'; +import type { UpgradeSpecificRulesRequest } from '../../../../../../common/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_request_schema'; +import { performUpgradeSpecificRules } from '../../api'; +import { useInvalidateFetchPrebuiltRulesUpgradeReviewQuery } from './use_fetch_prebuilt_rules_upgrade_review_query'; + +export const PERFORM_SPECIFIC_RULES_UPGRADE_KEY = [ + 'POST', + 'SPECIFIC_RULES', + PERFORM_RULE_UPGRADE_URL, +]; + +export const usePerformSpecificRulesUpgradeMutation = ( + options?: UseMutationOptions< + PerformRuleUpgradeResponseBody, + Error, + UpgradeSpecificRulesRequest['rules'] + > +) => { + const invalidateFindRulesQuery = useInvalidateFindRulesQuery(); + const invalidateFetchRulesSnoozeSettings = useInvalidateFetchRulesSnoozeSettingsQuery(); + const invalidatePrePackagedRulesStatus = useInvalidateFetchPrebuiltRulesStatusQuery(); + const invalidateFetchRuleManagementFilters = useInvalidateFetchRuleManagementFiltersQuery(); + const invalidateFetchPrebuiltRulesUpgradeReview = + useInvalidateFetchPrebuiltRulesUpgradeReviewQuery(); + const invalidateRuleStatus = useInvalidateFetchPrebuiltRulesStatusQuery(); + + return useMutation( + (rulesToUpgrade: UpgradeSpecificRulesRequest['rules']) => { + return performUpgradeSpecificRules(rulesToUpgrade); + }, + { + ...options, + mutationKey: PERFORM_SPECIFIC_RULES_UPGRADE_KEY, + onSettled: (...args) => { + invalidatePrePackagedRulesStatus(); + invalidateFindRulesQuery(); + invalidateFetchRulesSnoozeSettings(); + invalidateFetchRuleManagementFilters(); + + invalidateFetchPrebuiltRulesUpgradeReview(); + invalidateRuleStatus(); + + if (options?.onSettled) { + options.onSettled(...args); + } + }, + } + ); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_bulk_action_mutation.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_bulk_action_mutation.ts index 2647dfae6934e..fcb26a8aaee1b 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_bulk_action_mutation.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_bulk_action_mutation.ts @@ -11,10 +11,12 @@ import { BulkActionType } from '../../../../../common/detection_engine/rule_mana import type { BulkActionErrorResponse, BulkActionResponse, PerformBulkActionProps } from '../api'; import { performBulkAction } from '../api'; import { DETECTION_ENGINE_RULES_BULK_ACTION } from '../../../../../common/constants'; -import { useInvalidateFetchPrebuiltRulesStatusQuery } from './use_fetch_prebuilt_rules_status_query'; import { useInvalidateFindRulesQuery, useUpdateRulesCache } from './use_find_rules_query'; import { useInvalidateFetchRuleByIdQuery } from './use_fetch_rule_by_id_query'; import { useInvalidateFetchRuleManagementFiltersQuery } from './use_fetch_rule_management_filters_query'; +import { useInvalidateFetchPrebuiltRulesStatusQuery } from './prebuilt_rules/use_fetch_prebuilt_rules_status_query'; +import { useInvalidateFetchPrebuiltRulesUpgradeReviewQuery } from './prebuilt_rules/use_fetch_prebuilt_rules_upgrade_review_query'; +import { useInvalidateFetchPrebuiltRulesInstallReviewQuery } from './prebuilt_rules/use_fetch_prebuilt_rules_install_review_query'; export const BULK_ACTION_MUTATION_KEY = ['POST', DETECTION_ENGINE_RULES_BULK_ACTION]; @@ -29,6 +31,10 @@ export const useBulkActionMutation = ( const invalidateFetchRuleByIdQuery = useInvalidateFetchRuleByIdQuery(); const invalidateFetchRuleManagementFilters = useInvalidateFetchRuleManagementFiltersQuery(); const invalidateFetchPrebuiltRulesStatusQuery = useInvalidateFetchPrebuiltRulesStatusQuery(); + const invalidateFetchPrebuiltRulesInstallReviewQuery = + useInvalidateFetchPrebuiltRulesInstallReviewQuery(); + const invalidateFetchPrebuiltRulesUpgradeReviewQuery = + useInvalidateFetchPrebuiltRulesUpgradeReviewQuery(); const updateRulesCache = useUpdateRulesCache(); return useMutation< @@ -68,6 +74,8 @@ export const useBulkActionMutation = ( invalidateFetchRuleByIdQuery(); invalidateFetchRuleManagementFilters(); invalidateFetchPrebuiltRulesStatusQuery(); + invalidateFetchPrebuiltRulesInstallReviewQuery(); + invalidateFetchPrebuiltRulesUpgradeReviewQuery(); break; case BulkActionType.duplicate: invalidateFindRulesQuery(); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_bulk_install_fleet_packages_mutation.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_bulk_install_fleet_packages_mutation.ts index adbcec981ca3c..dff2bef7b39e7 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_bulk_install_fleet_packages_mutation.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_bulk_install_fleet_packages_mutation.ts @@ -11,7 +11,7 @@ import { useMutation } from '@tanstack/react-query'; import { PREBUILT_RULES_PACKAGE_NAME } from '../../../../../common/detection_engine/constants'; import type { BulkInstallFleetPackagesProps } from '../api'; import { bulkInstallFleetPackages } from '../api'; -import { useInvalidateFetchPrebuiltRulesStatusQuery } from './use_fetch_prebuilt_rules_status_query'; +import { useInvalidateFetchPrebuiltRulesStatusQuery } from './prebuilt_rules/use_fetch_prebuilt_rules_status_query'; export const BULK_INSTALL_FLEET_PACKAGES_MUTATION_KEY = [ 'POST', diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_create_prebuilt_rules_mutation.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_create_prebuilt_rules_mutation.ts deleted file mode 100644 index 37001caf43b8c..0000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_create_prebuilt_rules_mutation.ts +++ /dev/null @@ -1,43 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import type { UseMutationOptions } from '@tanstack/react-query'; -import { useMutation } from '@tanstack/react-query'; -import { PREBUILT_RULES_URL } from '../../../../../common/detection_engine/prebuilt_rules/api/urls'; -import type { CreatePrepackagedRulesResponse } from '../api'; -import { createPrepackagedRules } from '../api'; -import { useInvalidateFetchPrebuiltRulesStatusQuery } from './use_fetch_prebuilt_rules_status_query'; -import { useInvalidateFindRulesQuery } from './use_find_rules_query'; -import { useInvalidateFetchRuleManagementFiltersQuery } from './use_fetch_rule_management_filters_query'; -import { useInvalidateFetchRulesSnoozeSettingsQuery } from './use_fetch_rules_snooze_settings'; - -export const CREATE_PREBUILT_RULES_MUTATION_KEY = ['PUT', PREBUILT_RULES_URL]; - -export const useCreatePrebuiltRulesMutation = ( - options?: UseMutationOptions -) => { - const invalidateFindRulesQuery = useInvalidateFindRulesQuery(); - const invalidateFetchRulesSnoozeSettings = useInvalidateFetchRulesSnoozeSettingsQuery(); - const invalidatePrePackagedRulesStatus = useInvalidateFetchPrebuiltRulesStatusQuery(); - const invalidateFetchRuleManagementFilters = useInvalidateFetchRuleManagementFiltersQuery(); - - return useMutation(() => createPrepackagedRules(), { - ...options, - mutationKey: CREATE_PREBUILT_RULES_MUTATION_KEY, - onSettled: (...args) => { - // Always invalidate all rules and the prepackaged rules status cache as - // the number of rules might change after the installation - invalidatePrePackagedRulesStatus(); - invalidateFindRulesQuery(); - invalidateFetchRulesSnoozeSettings(); - invalidateFetchRuleManagementFilters(); - - if (options?.onSettled) { - options.onSettled(...args); - } - }, - }); -}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_install_fleet_package_mutation.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_install_fleet_package_mutation.ts index 0e6927e1745dd..3ed308c860b3d 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_install_fleet_package_mutation.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_install_fleet_package_mutation.ts @@ -11,7 +11,7 @@ import { useMutation } from '@tanstack/react-query'; import { PREBUILT_RULES_PACKAGE_NAME } from '../../../../../common/detection_engine/constants'; import type { InstallFleetPackageProps } from '../api'; import { installFleetPackage } from '../api'; -import { useInvalidateFetchPrebuiltRulesStatusQuery } from './use_fetch_prebuilt_rules_status_query'; +import { useInvalidateFetchPrebuiltRulesStatusQuery } from './prebuilt_rules/use_fetch_prebuilt_rules_status_query'; export const INSTALL_FLEET_PACKAGE_MUTATION_KEY = [ 'POST', diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/translations.ts new file mode 100644 index 0000000000000..c2d7b5a671f01 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/translations.ts @@ -0,0 +1,72 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const FAILED_ALL_RULES_INSTALL = i18n.translate( + 'xpack.securitySolution.detectionEngine.prebuiltRules.toast.failedAllRulesInstall', + { + defaultMessage: 'Failed to install Elastic prebuilt rules', + } +); + +export const FAILED_SPECIFIC_RULES_INSTALL = i18n.translate( + 'xpack.securitySolution.detectionEngine.prebuiltRules.toast.failedSepecifcRulesInstall', + { + defaultMessage: 'Failed to install selected Elastic prebuilt rules', + } +); + +export const INSTALL_RULE_SUCCESS = (succeeded: number) => + i18n.translate('xpack.securitySolution.detectionEngine.prebuiltRules.toast.installRuleSuccess', { + defaultMessage: '{succeeded, plural, one {# rule} other {# rules}} installed successfully. ', + values: { succeeded }, + }); + +export const INSTALL_RULE_SKIPPED = (skipped: number) => + i18n.translate('xpack.securitySolution.detectionEngine.prebuiltRules.toast.installRuleSkipped', { + defaultMessage: '{skipped, plural, one {# rule} other {# rules}} skipped installation. ', + values: { skipped }, + }); + +export const INSTALL_RULE_FAILED = (failed: number) => + i18n.translate('xpack.securitySolution.detectionEngine.prebuiltRules.toast.installRuleFailed', { + defaultMessage: '{failed, plural, one {# rule} other {# rules}} failed installation. ', + values: { failed }, + }); + +export const FAILED_ALL_RULES_UPGRADE = i18n.translate( + 'xpack.securitySolution.detectionEngine.prebuiltRules.toast.failedAllRulesUpgrade', + { + defaultMessage: 'Failed to upgrade Elastic prebuilt rules', + } +); + +export const FAILED_SPECIFIC_RULES_UPGRADE = i18n.translate( + 'xpack.securitySolution.detectionEngine.prebuiltRules.toast.failedSpecificRulesUpgrade', + { + defaultMessage: 'Failed to upgrade selected Elastic prebuilt rules', + } +); + +export const UPGRADE_RULE_SUCCESS = (succeeded: number) => + i18n.translate('xpack.securitySolution.detectionEngine.prebuiltRules.toast.upgradeRuleSuccess', { + defaultMessage: '{succeeded, plural, one {# rule} other {# rules}} update successfully. ', + values: { succeeded }, + }); + +export const UPGRADE_RULE_SKIPPED = (skipped: number) => + i18n.translate('xpack.securitySolution.detectionEngine.prebuiltRules.toast.upgradeRuleSkipped', { + defaultMessage: '{skipped, plural, one {# rule} other {# rules}} skipped update. ', + values: { skipped }, + }); + +export const UPGRADE_RULE_FAILED = (failed: number) => + i18n.translate('xpack.securitySolution.detectionEngine.prebuiltRules.toast.upgradeRuleFailed', { + defaultMessage: '{failed, plural, one {# rule} other {# rules}} failed update. ', + values: { failed }, + }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_install.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_install.ts new file mode 100644 index 0000000000000..b3c05bc12037d --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_install.ts @@ -0,0 +1,61 @@ +/* + * 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 { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +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'; + +export const usePerformInstallAllRules = () => { + const { addError, addSuccess } = useAppToasts(); + + return usePerformAllRulesInstallMutation({ + onError: (err) => { + addError(err, { title: i18n.FAILED_ALL_RULES_INSTALL }); + }, + onSuccess: (result) => { + addSuccess(getSuccessToastMessage(result)); + }, + }); +}; + +export const usePerformInstallSpecificRules = () => { + const { addError, addSuccess } = useAppToasts(); + + return usePerformSpecificRulesInstallMutation({ + onError: (err) => { + addError(err, { title: i18n.FAILED_SPECIFIC_RULES_INSTALL }); + }, + onSuccess: (result) => { + addSuccess(getSuccessToastMessage(result)); + }, + }); +}; + +const getSuccessToastMessage = (result: { + summary: { + total: number; + succeeded: number; + skipped: number; + failed: number; + }; +}) => { + let toastMessage: string = ''; + const { + summary: { succeeded, skipped, failed }, + } = result; + if (succeeded > 0) { + toastMessage += i18n.INSTALL_RULE_SUCCESS(succeeded); + } + if (skipped > 0) { + toastMessage += i18n.INSTALL_RULE_SKIPPED(skipped); + } + if (failed > 0) { + toastMessage += i18n.INSTALL_RULE_FAILED(failed); + } + return toastMessage; +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_upgrade.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_upgrade.ts new file mode 100644 index 0000000000000..6703d738beb3a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_upgrade.ts @@ -0,0 +1,61 @@ +/* + * 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 { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { usePerformAllRulesUpgradeMutation } from '../../api/hooks/prebuilt_rules/use_perform_all_rules_upgrade_mutation'; +import { usePerformSpecificRulesUpgradeMutation } from '../../api/hooks/prebuilt_rules/use_perform_specific_rules_upgrade_mutation'; + +import * as i18n from './translations'; + +export const usePerformUpgradeAllRules = () => { + const { addError, addSuccess } = useAppToasts(); + + return usePerformAllRulesUpgradeMutation({ + onError: (err) => { + addError(err, { title: i18n.FAILED_ALL_RULES_UPGRADE }); + }, + onSuccess: (result) => { + addSuccess(getSuccessToastMessage(result)); + }, + }); +}; + +export const usePerformUpgradeSpecificRules = () => { + const { addError, addSuccess } = useAppToasts(); + + return usePerformSpecificRulesUpgradeMutation({ + onError: (err) => { + addError(err, { title: i18n.FAILED_SPECIFIC_RULES_UPGRADE }); + }, + onSuccess: (result) => { + addSuccess(getSuccessToastMessage(result)); + }, + }); +}; + +const getSuccessToastMessage = (result: { + summary: { + total: number; + succeeded: number; + skipped: number; + failed: number; + }; +}) => { + let toastMessage: string = ''; + const { + summary: { succeeded, skipped, failed }, + } = result; + if (succeeded > 0) { + toastMessage += i18n.UPGRADE_RULE_SUCCESS(succeeded); + } + if (skipped > 0) { + toastMessage += i18n.UPGRADE_RULE_SKIPPED(skipped); + } + if (failed > 0) { + toastMessage += i18n.UPGRADE_RULE_FAILED(failed); + } + return toastMessage; +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_prebuilt_rules_install_review.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_prebuilt_rules_install_review.ts new file mode 100644 index 0000000000000..c274a1ab2f386 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_prebuilt_rules_install_review.ts @@ -0,0 +1,29 @@ +/* + * 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 type { UseQueryOptions } from '@tanstack/react-query'; +import type { ReviewRuleInstallationResponseBody } from '../../../../../common/detection_engine/prebuilt_rules/api/review_rule_installation/response_schema'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import * as i18n from '../translations'; +import { useFetchPrebuiltRulesInstallReviewQuery } from '../../api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_install_review_query'; + +/** + * A wrapper around useQuery provides default values to the underlying query, + * like query key, abortion signal, and error handler. + * + * @returns useQuery result + */ +export const usePrebuiltRulesInstallReview = ( + options?: UseQueryOptions +) => { + const { addError } = useAppToasts(); + + return useFetchPrebuiltRulesInstallReviewQuery({ + onError: (error) => addError(error, { title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE }), + ...options, + }); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_pre_packaged_rules_status.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_prebuilt_rules_status.ts similarity index 61% rename from x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_pre_packaged_rules_status.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_prebuilt_rules_status.ts index 889b670432d54..92dd17ca7bf44 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_pre_packaged_rules_status.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_prebuilt_rules_status.ts @@ -4,11 +4,11 @@ * 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 { useFetchPrebuiltRulesStatusQuery } from '../api/hooks/use_fetch_prebuilt_rules_status_query'; -import * as i18n from './translations'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { useFetchPrebuiltRulesStatusQuery } from '../../api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_status_query'; +import * as i18n from '../translations'; -export const usePrePackagedRulesStatus = () => { +export const usePrebuiltRulesStatus = () => { const { addError } = useAppToasts(); return useFetchPrebuiltRulesStatusQuery({ diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_prebuilt_rules_upgrade_review.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_prebuilt_rules_upgrade_review.ts new file mode 100644 index 0000000000000..1ea85b8aff05e --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_prebuilt_rules_upgrade_review.ts @@ -0,0 +1,29 @@ +/* + * 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 type { UseQueryOptions } from '@tanstack/react-query'; +import type { ReviewRuleUpgradeResponseBody } from '../../../../../common/detection_engine/prebuilt_rules/api/review_rule_upgrade/response_schema'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; + +import * as i18n from '../translations'; +import { useFetchPrebuiltRulesUpgradeReviewQuery } from '../../api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_upgrade_review_query'; + +/** + * A wrapper around useQuery provides default values to the underlying query, + * like query key, abortion signal, and error handler. + * + * @returns useQuery result + */ +export const usePrebuiltRulesUpgradeReview = ( + options?: UseQueryOptions +) => { + const { addError } = useAppToasts(); + + return useFetchPrebuiltRulesUpgradeReviewQuery({ + onError: (error) => addError(error, { title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE }), + ...options, + }); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_create_pre_packaged_rules.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_create_pre_packaged_rules.ts deleted file mode 100644 index c1de79159918f..0000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_create_pre_packaged_rules.ts +++ /dev/null @@ -1,31 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useCallback } from 'react'; -import { useUserData } from '../../../detections/components/user_info'; -import { useInstallPrePackagedRules } from './use_install_pre_packaged_rules'; - -export const useCreatePrePackagedRules = () => { - const [{ isSignalIndexExists, isAuthenticated, hasEncryptionKey, canUserCRUD, hasIndexWrite }] = - useUserData(); - const { mutateAsync: installPrePackagedRules, isLoading } = useInstallPrePackagedRules(); - - const canCreatePrePackagedRules = - canUserCRUD && hasIndexWrite && isAuthenticated && hasEncryptionKey && isSignalIndexExists; - - const createPrePackagedRules = useCallback(async () => { - if (canCreatePrePackagedRules) { - await installPrePackagedRules(); - } - }, [canCreatePrePackagedRules, installPrePackagedRules]); - - return { - isLoading, - createPrePackagedRules, - canCreatePrePackagedRules, - }; -}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_install_pre_packaged_rules.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_install_pre_packaged_rules.ts deleted file mode 100644 index b7fa307c0fedc..0000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_install_pre_packaged_rules.ts +++ /dev/null @@ -1,52 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { useIsMutating } from '@tanstack/react-query'; -import { useAppToasts } from '../../../common/hooks/use_app_toasts'; -import { - CREATE_PREBUILT_RULES_MUTATION_KEY, - useCreatePrebuiltRulesMutation, -} from '../api/hooks/use_create_prebuilt_rules_mutation'; -import * as i18n from './translations'; - -export const useInstallPrePackagedRules = () => { - const { addError, addSuccess } = useAppToasts(); - - return useCreatePrebuiltRulesMutation({ - onError: (err) => { - addError(err, { title: i18n.RULE_AND_TIMELINE_PREPACKAGED_FAILURE }); - }, - onSuccess: (result) => { - addSuccess(getSuccessToastMessage(result)); - }, - }); -}; - -export const useIsInstallingPrePackagedRules = () => { - const mutationsCount = useIsMutating(CREATE_PREBUILT_RULES_MUTATION_KEY); - return mutationsCount > 0; -}; - -const getSuccessToastMessage = (result: { - rules_installed: number; - rules_updated: number; - timelines_installed: number; - timelines_updated: number; -}) => { - const { - rules_installed: rulesInstalled, - rules_updated: rulesUpdated, - timelines_installed: timelinesInstalled, - timelines_updated: timelinesUpdated, - } = result; - if (rulesInstalled === 0 && (timelinesInstalled > 0 || timelinesUpdated > 0)) { - return i18n.TIMELINE_PREPACKAGED_SUCCESS; - } else if ((rulesInstalled > 0 || rulesUpdated > 0) && timelinesInstalled === 0) { - return i18n.RULE_PREPACKAGED_SUCCESS; - } else { - return i18n.RULE_AND_TIMELINE_PREPACKAGED_SUCCESS; - } -}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_pre_packaged_rules_installation_status.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_pre_packaged_rules_installation_status.ts deleted file mode 100644 index d45109ee078ed..0000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_pre_packaged_rules_installation_status.ts +++ /dev/null @@ -1,19 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getPrePackagedRuleInstallationStatus } from '../../../detections/pages/detection_engine/rules/helpers'; -import { usePrePackagedRulesStatus } from './use_pre_packaged_rules_status'; - -export const usePrePackagedRulesInstallationStatus = () => { - const { data: prePackagedRulesStatus } = usePrePackagedRulesStatus(); - - return getPrePackagedRuleInstallationStatus( - prePackagedRulesStatus?.rules_installed, - prePackagedRulesStatus?.rules_not_installed, - prePackagedRulesStatus?.rules_not_updated - ); -}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_pre_packaged_timelines_installation_status.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_pre_packaged_timelines_installation_status.ts deleted file mode 100644 index 61933d625ba18..0000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_pre_packaged_timelines_installation_status.ts +++ /dev/null @@ -1,19 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getPrePackagedTimelineInstallationStatus } from '../../../detections/pages/detection_engine/rules/helpers'; -import { usePrePackagedRulesStatus } from './use_pre_packaged_rules_status'; - -export const usePrePackagedTimelinesInstallationStatus = () => { - const { data: prePackagedRulesStatus } = usePrePackagedRulesStatus(); - - return getPrePackagedTimelineInstallationStatus( - prePackagedRulesStatus?.timelines_installed, - prePackagedRulesStatus?.timelines_not_installed, - prePackagedRulesStatus?.timelines_not_updated - ); -}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/auto_refresh_button/auto_refresh_button.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/auto_refresh_button/auto_refresh_button.test.tsx new file mode 100644 index 0000000000000..1505cc2fbe8ef --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/auto_refresh_button/auto_refresh_button.test.tsx @@ -0,0 +1,67 @@ +/* + * 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 from 'react'; +import { mount } from 'enzyme'; +import { fireEvent, render } from '@testing-library/react'; + +import { TestProviders } from '../../../../common/mock'; +import { AutoRefreshButton } from './auto_refresh_button'; + +describe('AutoRefreshButton', () => { + const reFetchRulesMock = jest.fn(); + const setIsRefreshOnMock = jest.fn(); + + afterEach(() => { + reFetchRulesMock.mockReset(); + setIsRefreshOnMock.mockReset(); + }); + + it('renders AutoRefreshButton as enabled', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="autoRefreshButton"]').at(0).text()).toEqual('On'); + }); + + it.skip('invokes refetch when enabling auto refresh', () => { + const { container } = render( + + ); + + fireEvent( + container.querySelector('[data-test-subj="autoRefreshButton"]')!, + new MouseEvent('click', { + bubbles: true, + cancelable: true, + }) + ); + + fireEvent( + container.querySelector('[data-test-subj="refreshSettingsSwitch"]')!, + new MouseEvent('click', { + bubbles: true, + cancelable: true, + }) + ); + + expect(setIsRefreshOnMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/auto_refresh_button/auto_refresh_button.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/auto_refresh_button/auto_refresh_button.tsx new file mode 100644 index 0000000000000..83499efd323c8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/auto_refresh_button/auto_refresh_button.tsx @@ -0,0 +1,117 @@ +/* + * 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, useState } from 'react'; +import type { EuiSwitchEvent } from '@elastic/eui'; +import { + EuiButtonEmpty, + EuiContextMenuPanel, + EuiPopover, + EuiSpacer, + EuiSwitch, + EuiTextColor, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { css } from '@emotion/react'; +import * as i18n from '../../../../detections/pages/detection_engine/rules/translations'; + +export interface AutoRefreshButtonProps { + isRefreshOn: boolean; + isDisabled: boolean; + reFetchRules: () => {}; + setIsRefreshOn: React.Dispatch>; +} + +/** + * AutoRefreshButton - component for toggling auto-refresh setting. + * + * @param isRefreshOn whether or not auto refresh is enabled + * @param isDisabled whether or not component is in disabled state + * @param reFetchRules action for re-fetching rules + * @param setIsRefreshOn action for enabling/disabling refresh + */ +const AutoRefreshButtonComponent = ({ + isRefreshOn, + isDisabled, + reFetchRules, + setIsRefreshOn, +}: AutoRefreshButtonProps) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const handleAutoRefreshSwitch = useCallback( + (closePopover: () => void) => (e: EuiSwitchEvent) => { + const refreshOn = e.target.checked; + if (refreshOn) { + reFetchRules(); + } + setIsRefreshOn(refreshOn); + closePopover(); + }, + [reFetchRules, setIsRefreshOn] + ); + + const handleGetRefreshSettingsPopoverContent = useCallback( + (closePopover: () => void) => ( + , + ...(isDisabled + ? [ +
+ + + + +
, + ] + : []), + ]} + /> + ), + [isRefreshOn, handleAutoRefreshSwitch, isDisabled] + ); + + return ( + setIsPopoverOpen(false)} + button={ + setIsPopoverOpen(!isPopoverOpen)} + disabled={isDisabled} + css={css` + margin-left: 10px; + `} + > + {isRefreshOn ? 'On' : 'Off'} + + } + > + {handleGetRefreshSettingsPopoverContent(() => setIsPopoverOpen(false))} + + ); +}; + +AutoRefreshButtonComponent.displayName = 'AutoRefreshButtonComponent'; + +export const AutoRefreshButton = React.memo(AutoRefreshButtonComponent); + +AutoRefreshButton.displayName = 'AutoRefreshButton'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/mini_callout/mini_callout.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/mini_callout/mini_callout.test.tsx new file mode 100644 index 0000000000000..1a6d8392d859a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/mini_callout/mini_callout.test.tsx @@ -0,0 +1,64 @@ +/* + * 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 from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; +import type { MiniCalloutProps } from './mini_callout'; +import { MiniCallout } from './mini_callout'; + +describe('MiniCallout', () => { + const defaultProps: MiniCalloutProps = { + color: 'primary', + iconType: 'iInCircle', + title: 'Mini Callout Title', + }; + + it('renders the MiniCallout component with the provided title', () => { + render(); + + expect(screen.getByText(defaultProps.title as string)).toBeInTheDocument(); + }); + + it('renders the MiniCallout component with the provided iconType and color', () => { + const { container } = render(); + + const miniCallout = screen.getByTestId('mini-callout'); + const icon = container.querySelector('[data-euiicon-type="iInCircle"]'); + expect(icon).not.toBeNull(); + expect(miniCallout).toHaveAttribute( + 'class', + expect.stringContaining(defaultProps.color as string) + ); + }); + + it('renders the MiniCallout component with no icon if not provided', () => { + const { container } = render(); + + const icon = container.querySelector('[data-euiicon-type]'); + expect(icon).toBeNull(); + }); + + it('renders the dismiss link when dismissible is true', () => { + render(); + + expect(screen.getByText('Dismiss')).toBeInTheDocument(); + }); + + it('does not render the dismiss link when dismissible is false', () => { + render(); + + expect(screen.queryByText('Dismiss')).not.toBeInTheDocument(); + }); + + it('removes the MiniCallout component from the DOM when the dismiss link is clicked', () => { + render(); + + fireEvent.click(screen.getByText('Dismiss')); + + expect(screen.queryByText(defaultProps.title as string)).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/mini_callout/mini_callout.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/mini_callout/mini_callout.tsx new file mode 100644 index 0000000000000..8b319d36c2809 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/mini_callout/mini_callout.tsx @@ -0,0 +1,107 @@ +/* + * 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 { + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLink, + EuiTextColor, + useEuiTheme, +} from '@elastic/eui'; +import type { ReactNode } from 'react'; +import React, { useState } from 'react'; +import type { IconType } from '@elastic/eui/src/components/icon'; +import type { Color } from '@elastic/eui/src/components/call_out/call_out'; +import { css } from '@emotion/react'; +import * as i18n from './translations'; + +export interface MiniCalloutProps { + color?: Color; + dismissible?: boolean; + iconType: IconType | undefined; + title: ReactNode | string; +} + +/** + * A customized mini variant of the EuiCallOut component. Includes additional styling overrides + * for displaying rich titles when callout size="s", and an option enabling dismissal. + * + * @param color color for the callout, defaults to 'primary' + * @param dismissible whether the callout can be dismissed, defaults to 'true' + * @param iconType icon for the callout + * @param title ReactNode or string title text to be displayed + * + * @constructor + */ +const MiniCalloutComponent: React.FC = ({ + color = 'primary', + dismissible = true, + iconType, + title, +}: MiniCalloutProps) => { + const { euiTheme } = useEuiTheme(); + const [isDismissed, setIsDismissed] = useState(false); + + if (isDismissed) { + return null; + } + + const calloutTitle = ( +
+ + + + + {title} + + + + {dismissible && ( + + setIsDismissed(true)} + > + {i18n.DISMISS} + + + )} + +
+ ); + + return ( + +
+ {iconType && } + {calloutTitle} +
+
+ ); +}; + +export const MiniCallout = React.memo(MiniCalloutComponent); +MiniCallout.displayName = 'MiniCallout'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/mini_callout/translations.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/mini_callout/translations.tsx new file mode 100644 index 0000000000000..4d6a26042696c --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/mini_callout/translations.tsx @@ -0,0 +1,51 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiLink } from '@elastic/eui'; +import { css } from '@emotion/css'; +import React from 'react'; + +export const DISMISS = i18n.translate('xpack.securitySolution.detectionEngine.rules.dismissTitle', { + defaultMessage: 'Dismiss', +}); + +export const NEW_PREBUILT_RULES_AVAILABLE_CALLOUT_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.newPrebuiltRulesCalloutTitle', + { + defaultMessage: + 'New Elastic rules are available to be installed. Click on the “Add Elastic Rules” button to Review and install.', + } +); + +export const RULE_UPDATES_LINK = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.ruleUpdatesLinkTitle', + { + defaultMessage: 'Rule Updates', + } +); + +type OnClick = () => void; +export const getUpdateRulesCalloutTitle = (onClick: OnClick) => ( + + {RULE_UPDATES_LINK} + + ), + }} + /> +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/ml_rule_warning_popover/ml_rule_warning_popover.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/ml_rule_warning_popover/ml_rule_warning_popover.tsx new file mode 100644 index 0000000000000..8c4e9775c35e5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/ml_rule_warning_popover/ml_rule_warning_popover.tsx @@ -0,0 +1,102 @@ +/* + * 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 from 'react'; +import { + EuiPopover, + EuiText, + EuiPopoverTitle, + EuiSpacer, + EuiPopoverFooter, + EuiButtonIcon, +} from '@elastic/eui'; + +import { RuleExecutionStatus } from '../../../../../common/detection_engine/rule_monitoring'; +import type { SecurityJob } from '../../../../common/components/ml_popover/types'; +import * as i18n from '../rules_table/translations'; + +import { useBoolState } from '../../../../common/hooks/use_bool_state'; +import { getRuleDetailsTabUrl } from '../../../../common/components/link_to/redirect_to_detection_engine'; +import { SecurityPageName } from '../../../../../common/constants'; +import { SecuritySolutionLinkButton } from '../../../../common/components/links'; +import { isMlRule } from '../../../../../common/detection_engine/utils'; +import { getCapitalizedStatusText } from '../../../../detections/components/rules/rule_execution_status/utils'; +import type { Rule } from '../../../rule_management/logic'; +import { isJobStarted } from '../../../../../common/machine_learning/helpers'; +import { RuleDetailTabs } from '../../../rule_details_ui/pages/rule_details'; + +const POPOVER_WIDTH = '340px'; + +export interface MlRuleWarningPopoverComponentProps { + rule: Rule; + loadingJobs: boolean; + jobs: SecurityJob[]; +} + +const MlRuleWarningPopoverComponent: React.FC = ({ + rule, + loadingJobs, + jobs, +}) => { + const [isPopoverOpen, , closePopover, togglePopover] = useBoolState(); + + if (!isMlRule(rule.type) || loadingJobs || !rule.machine_learning_job_id) { + return null; + } + + const jobIds = rule.machine_learning_job_id; + const notRunningJobs = jobs.filter( + (job) => jobIds.includes(job.id) && !isJobStarted(job.jobState, job.datafeedState) + ); + if (!notRunningJobs.length) { + return null; + } + + const button = ( + + ); + const popoverTitle = getCapitalizedStatusText(RuleExecutionStatus['partial failure']); + + return ( + + {popoverTitle} +
+ +

{i18n.ML_RULE_JOBS_WARNING_DESCRIPTION}

+
+
+ + {notRunningJobs.map((job) => ( + {job.customSettings?.security_app_display_name ?? job.id} + ))} + + + {i18n.ML_RULE_JOBS_WARNING_BUTTON_LABEL} + + +
+ ); +}; + +export const MlRuleWarningPopover = React.memo(MlRuleWarningPopoverComponent); + +MlRuleWarningPopover.displayName = 'MlRuleWarningPopover'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_header_buttons.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_header_buttons.tsx new file mode 100644 index 0000000000000..ec9eb2f3fb2a9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_header_buttons.tsx @@ -0,0 +1,49 @@ +/* + * 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 { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import React from 'react'; +import { useAddPrebuiltRulesTableContext } from './add_prebuilt_rules_table_context'; +import * as i18n from './translations'; + +export const AddPrebuiltRulesHeaderButtons = () => { + const { + state: { rules, selectedRules, loadingRules }, + actions: { installAllRules, installSelectedRules }, + } = useAddPrebuiltRulesTableContext(); + + const isRulesAvailableForInstall = rules.length > 0; + const numberOfSelectedRules = selectedRules.length ?? 0; + const shouldDisplayInstallSelectedRulesButton = numberOfSelectedRules > 0; + + const isRuleInstalling = loadingRules.length > 0; + + return ( + + {shouldDisplayInstallSelectedRulesButton ? ( + + + {i18n.INSTALL_SELECTED_RULES(numberOfSelectedRules)} + {isRuleInstalling ? : undefined} + + + ) : null} + + + {i18n.INSTALL_ALL} + {isRuleInstalling ? : undefined} + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table.tsx new file mode 100644 index 0000000000000..159e76e822911 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table.tsx @@ -0,0 +1,116 @@ +/* + * 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 { + EuiEmptyPrompt, + EuiInMemoryTable, + EuiSkeletonLoading, + EuiProgress, + EuiSkeletonTitle, + EuiSkeletonText, +} from '@elastic/eui'; +import React from 'react'; + +import * as i18n from '../../../../../detections/pages/detection_engine/rules/translations'; +import { useIsUpgradingSecurityPackages } from '../../../../rule_management/logic/use_upgrade_security_packages'; +import { RULES_TABLE_INITIAL_PAGE_SIZE, RULES_TABLE_PAGE_SIZE_OPTIONS } from '../constants'; +import { useAddPrebuiltRulesTableContext } from './add_prebuilt_rules_table_context'; +import { useAddPrebuiltRulesTableColumns } from './use_add_prebuilt_rules_table_columns'; + +const NO_ITEMS_MESSAGE = ( + {i18n.NO_RULES_AVAILABLE_FOR_INSTALL}} + titleSize="s" + body={i18n.NO_RULES_AVAILABLE_FOR_INSTALL_BODY} + /> +); + +/** + * Table Component for displaying new rules that are available to be installed + */ +export const AddPrebuiltRulesTable = React.memo(() => { + const isUpgradingSecurityPackages = useIsUpgradingSecurityPackages(); + + const addRulesTableContext = useAddPrebuiltRulesTableContext(); + + const { + state: { rules, tags, isFetched, isLoading, isRefetching, selectedRules }, + actions: { selectRules }, + } = addRulesTableContext; + const rulesColumns = useAddPrebuiltRulesTableColumns(); + + const isTableEmpty = isFetched && rules.length === 0; + + const shouldShowLinearProgress = (isFetched && isRefetching) || isUpgradingSecurityPackages; + const shouldShowLoadingOverlay = !isFetched && isRefetching; + + return ( + <> + {shouldShowLinearProgress && ( + + )} + + + + + } + loadedContent={ + isTableEmpty ? ( + NO_ITEMS_MESSAGE + ) : ( + ({ + value: tag, + name: tag, + field: 'tags', + })), + }, + ], + }} + pagination={{ + initialPageSize: RULES_TABLE_INITIAL_PAGE_SIZE, + pageSizeOptions: RULES_TABLE_PAGE_SIZE_OPTIONS, + }} + isSelectable + selection={{ + selectable: () => true, + onSelectionChange: selectRules, + initialSelected: selectedRules, + }} + itemId="rule_id" + data-test-subj="add-prebuilt-rules-table" + columns={rulesColumns} + /> + ) + } + /> + + ); +}); + +AddPrebuiltRulesTable.displayName = 'AddPrebuiltRulesTable'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx new file mode 100644 index 0000000000000..b3aceacc4b1b9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx @@ -0,0 +1,184 @@ +/* + * 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, { createContext, useCallback, useContext, useMemo, useState } from 'react'; +import type { RuleInstallationInfoForReview } from '../../../../../../common/detection_engine/prebuilt_rules/api/review_rule_installation/response_schema'; +import type { RuleSignatureId } from '../../../../../../common/detection_engine/rule_schema'; +import { invariant } from '../../../../../../common/utils/invariant'; +import { + usePerformInstallAllRules, + usePerformInstallSpecificRules, +} from '../../../../rule_management/logic/prebuilt_rules/use_perform_rule_install'; +import { usePrebuiltRulesInstallReview } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_install_review'; + +export interface AddPrebuiltRulesTableState { + /** + * Rules available to be installed + */ + rules: RuleInstallationInfoForReview[]; + /** + * All unique tags for all rules + */ + tags: string[]; + /** + * Is true then there is no cached data and the query is currently fetching. + */ + isLoading: boolean; + /** + * Will be true if the query has been fetched. + */ + isFetched: boolean; + /** + * Is true whenever a background refetch is in-flight, which does not include initial loading + */ + isRefetching: boolean; + /** + * List of rule IDs that are currently being upgraded + */ + loadingRules: RuleSignatureId[]; + /** + * The timestamp for when the rules were successfully fetched + */ + lastUpdated: number; + /** + * Rule rows selected in EUI InMemory Table + */ + selectedRules: RuleInstallationInfoForReview[]; +} + +export interface AddPrebuiltRulesTableActions { + reFetchRules: () => void; + installOneRule: (ruleId: RuleSignatureId) => void; + installAllRules: () => void; + installSelectedRules: () => void; + selectRules: (rules: RuleInstallationInfoForReview[]) => void; +} + +export interface AddPrebuiltRulesContextType { + state: AddPrebuiltRulesTableState; + actions: AddPrebuiltRulesTableActions; +} + +const AddPrebuiltRulesTableContext = createContext(null); + +interface AddPrebuiltRulesTableContextProviderProps { + children: React.ReactNode; +} + +export const AddPrebuiltRulesTableContextProvider = ({ + children, +}: AddPrebuiltRulesTableContextProviderProps) => { + const [loadingRules, setLoadingRules] = useState([]); + const [selectedRules, setSelectedRules] = useState([]); + + const { + data: { rules, stats: { tags } } = { + rules: [], + stats: { tags: [] }, + }, + refetch, + dataUpdatedAt, + isFetched, + isLoading, + isRefetching, + } = usePrebuiltRulesInstallReview({ + refetchInterval: 60000, // Refetch available rules for installation every minute + keepPreviousData: true, // Use this option so that the state doesn't jump between "success" and "loading" on page change + }); + + const { mutateAsync: installAllRulesRequest } = usePerformInstallAllRules(); + const { mutateAsync: installSpecificRulesRequest } = usePerformInstallSpecificRules(); + + const installOneRule = useCallback( + async (ruleId: RuleSignatureId) => { + const rule = rules.find((r) => r.rule_id === ruleId); + invariant(rule, `Rule with id ${ruleId} not found`); + + setLoadingRules((prev) => [...prev, ruleId]); + await installSpecificRulesRequest([ + { + rule_id: ruleId, + version: rule.version, + }, + ]); + setLoadingRules((prev) => prev.filter((id) => id !== ruleId)); + }, + [installSpecificRulesRequest, rules] + ); + + const installSelectedRules = useCallback(async () => { + const rulesToUpgrade = selectedRules.map((rule) => ({ + rule_id: rule.rule_id, + version: rule.version, + })); + setLoadingRules((prev) => [...prev, ...rulesToUpgrade.map((r) => r.rule_id)]); + await installSpecificRulesRequest(rulesToUpgrade); + setLoadingRules((prev) => prev.filter((id) => !rulesToUpgrade.some((r) => r.rule_id === id))); + setSelectedRules([]); + }, [installSpecificRulesRequest, selectedRules]); + + const installAllRules = useCallback(async () => { + // Unselect all rules so that the table doesn't show the "bulk actions" bar + setLoadingRules((prev) => [...prev, ...rules.map((r) => r.rule_id)]); + await installAllRulesRequest(); + setLoadingRules((prev) => prev.filter((id) => !rules.some((r) => r.rule_id === id))); + setSelectedRules([]); + }, [installAllRulesRequest, rules]); + + const actions = useMemo( + () => ({ + installAllRules, + installOneRule, + installSelectedRules, + reFetchRules: refetch, + selectRules: setSelectedRules, + }), + [installAllRules, installOneRule, installSelectedRules, refetch] + ); + + const providerValue = useMemo(() => { + return { + state: { + rules, + tags, + isFetched, + isLoading, + loadingRules, + isRefetching, + selectedRules, + lastUpdated: dataUpdatedAt, + }, + actions, + }; + }, [ + rules, + tags, + isFetched, + isLoading, + loadingRules, + isRefetching, + selectedRules, + dataUpdatedAt, + actions, + ]); + + return ( + + {children} + + ); +}; + +export const useAddPrebuiltRulesTableContext = (): AddPrebuiltRulesContextType => { + const rulesTableContext = useContext(AddPrebuiltRulesTableContext); + invariant( + rulesTableContext, + 'useAddPrebuiltRulesTableContext should be used inside AddPrebuiltRulesTableContextProvider' + ); + + return rulesTableContext; +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/translations.ts new file mode 100644 index 0000000000000..72cac13e5494d --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/translations.ts @@ -0,0 +1,25 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const INSTALL_ALL = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.addRules.installAllButtonTitle', + { + defaultMessage: 'Install all', + } +); + +export const INSTALL_SELECTED_RULES = (numberOfSelectedRules: number) => { + return i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.addRules.installSelectedRules', + { + defaultMessage: 'Install {numberOfSelectedRules} selected rule(s)', + values: { numberOfSelectedRules }, + } + ); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/use_add_prebuilt_rules_table_columns.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/use_add_prebuilt_rules_table_columns.tsx new file mode 100644 index 0000000000000..234350129986c --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/use_add_prebuilt_rules_table_columns.tsx @@ -0,0 +1,141 @@ +/* + * 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 type { EuiBasicTableColumn } from '@elastic/eui'; +import { EuiButtonEmpty, EuiBadge, EuiText, EuiLoadingSpinner } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { SHOW_RELATED_INTEGRATIONS_SETTING } from '../../../../../../common/constants'; +import { PopoverItems } from '../../../../../common/components/popover_items'; +import { useUiSetting$ } from '../../../../../common/lib/kibana'; +import { IntegrationsPopover } from '../../../../../detections/components/rules/related_integrations/integrations_popover'; +import { SeverityBadge } from '../../../../../detections/components/rules/severity_badge'; +import * as i18n from '../../../../../detections/pages/detection_engine/rules/translations'; +import type { Rule } from '../../../../rule_management/logic'; +import type { RuleInstallationInfoForReview } from '../../../../../../common/detection_engine/prebuilt_rules/api/review_rule_installation/response_schema'; +import { useUserData } from '../../../../../detections/components/user_info'; +import { hasUserCRUDPermission } from '../../../../../common/utils/privileges'; +import type { AddPrebuiltRulesTableActions } from './add_prebuilt_rules_table_context'; +import { useAddPrebuiltRulesTableContext } from './add_prebuilt_rules_table_context'; +import type { RuleSignatureId } from '../../../../../../common/detection_engine/rule_schema'; + +export type TableColumn = EuiBasicTableColumn; + +export const RULE_NAME_COLUMN: TableColumn = { + field: 'name', + name: i18n.COLUMN_RULE, + render: (value: RuleInstallationInfoForReview['name']) => ( + + {value} + + ), + sortable: true, + truncateText: true, + width: '40%', + align: 'left', +}; + +const TAGS_COLUMN: TableColumn = { + field: 'tags', + name: null, + align: 'center', + render: (tags: RuleInstallationInfoForReview['tags']) => { + if (tags == null || tags.length === 0) { + return null; + } + + const renderItem = (tag: string, i: number) => ( + + {tag} + + ); + return ( + + ); + }, + width: '65px', + truncateText: true, +}; + +const INTEGRATIONS_COLUMN: TableColumn = { + field: 'related_integrations', + name: null, + align: 'center', + render: (integrations: RuleInstallationInfoForReview['related_integrations']) => { + if (integrations == null || integrations.length === 0) { + return null; + } + + return ; + }, + width: '143px', + truncateText: true, +}; + +const createInstallButtonColumn = ( + installOneRule: AddPrebuiltRulesTableActions['installOneRule'], + loadingRules: RuleSignatureId[] +): TableColumn => ({ + field: 'rule_id', + name: '', + render: (ruleId: RuleSignatureId) => { + const isRuleInstalling = loadingRules.includes(ruleId); + return ( + installOneRule(ruleId)}> + {isRuleInstalling ? : i18n.INSTALL_RULE_BUTTON} + + ); + }, + width: '10%', + align: 'center', +}); + +export const useAddPrebuiltRulesTableColumns = (): TableColumn[] => { + const [{ canUserCRUD }] = useUserData(); + const hasCRUDPermissions = hasUserCRUDPermission(canUserCRUD); + const [showRelatedIntegrations] = useUiSetting$(SHOW_RELATED_INTEGRATIONS_SETTING); + const { + state: { loadingRules }, + actions: { installOneRule }, + } = useAddPrebuiltRulesTableContext(); + + return useMemo( + () => [ + RULE_NAME_COLUMN, + ...(showRelatedIntegrations ? [INTEGRATIONS_COLUMN] : []), + TAGS_COLUMN, + { + field: 'risk_score', + name: i18n.COLUMN_RISK_SCORE, + render: (value: Rule['risk_score']) => ( + + {value} + + ), + sortable: true, + truncateText: true, + width: '85px', + }, + { + field: 'severity', + name: i18n.COLUMN_SEVERITY, + render: (value: Rule['severity']) => , + sortable: true, + truncateText: true, + width: '12%', + }, + ...(hasCRUDPermissions ? [createInstallButtonColumn(installOneRule, loadingRules)] : []), + ], + [hasCRUDPermissions, installOneRule, loadingRules, showRelatedIntegrations] + ); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/constants.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/constants.ts index d5a8db24b14e8..b65ba5f111c0e 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/constants.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/constants.ts @@ -7,5 +7,6 @@ import { RULES_TABLE_MAX_PAGE_SIZE } from '../../../../../common/constants'; +export const RULES_TABLE_INITIAL_PAGE_SIZE = 20; export const RULES_TABLE_PAGE_SIZE_OPTIONS = [5, 10, 20, 50, RULES_TABLE_MAX_PAGE_SIZE]; export const RULES_TABLE_STATE_STORAGE_KEY = 'securitySolution.rulesTable'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/index.tsx index 58ba3c2f96a5c..6a127faacdac6 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/index.tsx @@ -11,8 +11,9 @@ import { useRouteSpy } from '../../../../common/utils/route/use_route_spy'; import { RulesManagementTour } from './rules_table/guided_onboarding/rules_management_tour'; import { useSyncRulesTableSavedState } from './rules_table/use_sync_rules_table_saved_state'; import { RulesTables } from './rules_tables'; -import type { AllRulesTabs } from './rules_table_toolbar'; -import { RulesTableToolbar } from './rules_table_toolbar'; +import { AllRulesTabs, RulesTableToolbar } from './rules_table_toolbar'; +import { UpgradePrebuiltRulesTable } from './upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table'; +import { UpgradePrebuiltRulesTableContextProvider } from './upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context'; /** * Table Component for displaying all Rules for a given cluster. Provides the ability to filter @@ -26,14 +27,26 @@ export const AllRules = React.memo(() => { useSyncRulesTableSavedState(); const [{ tabName }] = useRouteSpy(); - return ( - <> - - - - - - ); + if (tabName !== AllRulesTabs.updates) { + return ( + <> + + + + + + ); + } else { + return ( + <> + + + + + + + ); + } }); AllRules.displayName = 'AllRules'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.test.tsx index ad9ea30ed9f4d..64d82b50c9e84 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.test.tsx @@ -25,6 +25,7 @@ import { useRulesTableSavedState } from './use_rules_table_saved_state'; jest.mock('../../../../../common/lib/kibana'); jest.mock('../../../../rule_management/logic/use_find_rules'); +jest.mock('../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_install_review'); jest.mock('../../../../rule_management/api/hooks/use_fetch_rules_snooze_settings'); jest.mock('./use_rules_table_saved_state'); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.tsx index 0c5550a8c66dc..37b554e02a906 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.tsx @@ -39,7 +39,7 @@ import { import { RuleSource } from './rules_table_saved_state'; import { useRulesTableSavedState } from './use_rules_table_saved_state'; -interface RulesSnoozeSettingsState { +interface RulesSnoozeSettings { /** * A map object using rule SO's id (not ruleId) as keys and snooze settings as values */ @@ -127,7 +127,7 @@ export interface RulesTableState { /** * Rules snooze settings for the current rules */ - rulesSnoozeSettings: RulesSnoozeSettingsState; + rulesSnoozeSettings: RulesSnoozeSettings; } export type LoadingRuleAction = @@ -346,8 +346,8 @@ export const RulesTableContextProvider = ({ children }: RulesTableContextProvide ] ); - const providerValue = useMemo( - () => ({ + const providerValue = useMemo(() => { + return { state: { rules, rulesSnoozeSettings: { @@ -382,33 +382,32 @@ export const RulesTableContextProvider = ({ children }: RulesTableContextProvide }), }, actions, - }), - [ - rules, - rulesSnoozeSettingsMap, - isSnoozeSettingsLoading, - isSnoozeSettingsFetching, - isSnoozeSettingsFetchError, - page, - perPage, - total, - filterOptions, - isPreflightInProgress, - isActionInProgress, - isAllSelected, - isFetched, - isFetching, - isLoading, - isRefetching, - isRefreshOn, - dataUpdatedAt, - loadingRules.ids, - loadingRules.action, - selectedRuleIds, - sortingOptions, - actions, - ] - ); + }; + }, [ + rules, + rulesSnoozeSettingsMap, + isSnoozeSettingsLoading, + isSnoozeSettingsFetching, + isSnoozeSettingsFetchError, + page, + perPage, + total, + filterOptions, + isPreflightInProgress, + isActionInProgress, + isAllSelected, + isFetched, + isFetching, + isLoading, + isRefetching, + isRefreshOn, + dataUpdatedAt, + loadingRules.ids, + loadingRules.action, + selectedRuleIds, + sortingOptions, + actions, + ]); return {children}; }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx index bacdced332c58..b8b3b165a2ca6 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx @@ -7,30 +7,61 @@ import React, { useMemo } from 'react'; import { TabNavigation } from '../../../../common/components/navigation/tab_navigation'; -import * as i18n from '../../../../detections/pages/detection_engine/rules/translations'; +import * as i18n from './translations'; +import { useRulesTableContext } from './rules_table/rules_table_context'; +import { usePrebuiltRulesStatus } from '../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_status'; export enum AllRulesTabs { management = 'management', monitoring = 'monitoring', + updates = 'updates', } export const RulesTableToolbar = React.memo(() => { + const { + state: { + pagination: { total: installedTotal }, + }, + } = useRulesTableContext(); + + const { data: prebuiltRulesStatus } = usePrebuiltRulesStatus(); + + const updateTotal = prebuiltRulesStatus?.num_prebuilt_rules_to_upgrade ?? 0; + const ruleTabs = useMemo( () => ({ [AllRulesTabs.management]: { id: AllRulesTabs.management, - name: i18n.RULES_TAB, + name: i18n.INSTALLED_RULES_TAB, disabled: false, href: `/rules/${AllRulesTabs.management}`, + isBeta: installedTotal > 0, + betaOptions: { + text: `${installedTotal}`, + }, }, [AllRulesTabs.monitoring]: { id: AllRulesTabs.monitoring, - name: i18n.MONITORING_TAB, + name: i18n.RULE_MONITORING_TAB, disabled: false, href: `/rules/${AllRulesTabs.monitoring}`, + isBeta: installedTotal > 0, + betaOptions: { + text: `${installedTotal}`, + }, + }, + [AllRulesTabs.updates]: { + id: AllRulesTabs.updates, + name: i18n.RULE_UPDATES_TAB, + disabled: false, + href: `/rules/${AllRulesTabs.updates}`, + isBeta: updateTotal > 0, + betaOptions: { + text: `${updateTotal}`, + }, }, }), - [] + [installedTotal, updateTotal] ); return ; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_tables.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_tables.tsx index ca4d0241e508f..bae55018ec778 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_tables.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_tables.tsx @@ -5,13 +5,7 @@ * 2.0. */ -import { - EuiBasicTable, - EuiConfirmModal, - EuiEmptyPrompt, - EuiSkeletonText, - EuiProgress, -} from '@elastic/eui'; +import { EuiBasicTable, EuiConfirmModal, EuiEmptyPrompt, EuiProgress } from '@elastic/eui'; import React, { useCallback, useMemo, useRef } from 'react'; import { Loader } from '../../../../common/components/loader'; import { useBoolState } from '../../../../common/hooks/use_bool_state'; @@ -30,7 +24,7 @@ import { useRulesTableContext } from './rules_table/rules_table_context'; import { useAsyncConfirmation } from './rules_table/use_async_confirmation'; import { RulesTableFilters } from './rules_table_filters/rules_table_filters'; import { AllRulesTabs } from './rules_table_toolbar'; -import { RulesTableUtilityBar } from './rules_table_utility_bar'; +import { RulesTableUtilityBar } from '../rules_table_utility_bar/rules_table_utility_bar'; import { useMonitoringColumns, useRulesColumns } from './use_columns'; import { useUserData } from '../../../../detections/components/user_info'; import { hasUserCRUDPermission } from '../../../../common/utils/privileges'; @@ -131,15 +125,14 @@ export const RulesTables = React.memo(({ selectedTab }) => { executeBulkActionsDryRun, }); - const paginationMemo = useMemo( - () => ({ + const paginationMemo = useMemo(() => { + return { pageIndex: pagination.page - 1, pageSize: pagination.perPage, totalItemCount: pagination.total, pageSizeOptions: RULES_TABLE_PAGE_SIZE_OPTIONS, - }), - [pagination] - ); + }; + }, [pagination.page, pagination.perPage, pagination.total]); const tableOnChangeCallback = useCallback( ({ page, sort }: EuiBasicTableOnChange) => { @@ -179,6 +172,10 @@ export const RulesTables = React.memo(({ selectedTab }) => { } }, selectedRuleIds); + const isTableSelectable = + hasPermissions && + (selectedTab === AllRulesTabs.management || selectedTab === AllRulesTabs.monitoring); + const euiBasicTableSelectionProps = useMemo( () => ({ selectable: (item: Rule) => !loadingRuleIds.includes(item.id), @@ -221,13 +218,27 @@ export const RulesTables = React.memo(({ selectedTab }) => { const shouldShowRulesTable = !isLoading && !isTableEmpty; - const tableProps = - selectedTab === AllRulesTabs.management - ? { - 'data-test-subj': 'rules-management-table', - columns: rulesColumns, - } - : { 'data-test-subj': 'rules-monitoring-table', columns: monitoringColumns }; + let tableProps; + switch (selectedTab) { + case AllRulesTabs.management: + tableProps = { + 'data-test-subj': 'rules-management-table', + columns: rulesColumns, + }; + break; + case AllRulesTabs.monitoring: + tableProps = { + 'data-test-subj': 'rules-monitoring-table', + columns: monitoringColumns, + }; + break; + default: + tableProps = { + 'data-test-subj': 'rules-management-table', + columns: rulesColumns, + }; + break; + } const shouldShowLinearProgress = (isFetched && isRefetching) || isUpgradingSecurityPackages; const shouldShowLoadingOverlay = (!isFetched && isRefetching) || isPreflightInProgress; @@ -247,9 +258,6 @@ export const RulesTables = React.memo(({ selectedTab }) => { )} {isTableEmpty && } - {isLoading && ( - - )} {isDeleteConfirmationVisible && ( (({ selectedTab }) => { 0 ? numberOfSelectedRules : 1} + rulesCount={numberOfSelectedRules} /> )} {isBulkEditFlyoutVisible && bulkEditActionType !== undefined && ( @@ -299,12 +307,12 @@ export const RulesTables = React.memo(({ selectedTab }) => { { + return i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.upgradeSelected', + { + defaultMessage: 'Update {numberOfSelectedRules} selected rule(s)', + values: { numberOfSelectedRules }, + } + ); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table.tsx new file mode 100644 index 0000000000000..7a8b79037a06f --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table.tsx @@ -0,0 +1,117 @@ +/* + * 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 { + EuiEmptyPrompt, + EuiInMemoryTable, + EuiProgress, + EuiSkeletonLoading, + EuiSkeletonText, + EuiSkeletonTitle, +} from '@elastic/eui'; +import React from 'react'; +import * as i18n from '../../../../../detections/pages/detection_engine/rules/translations'; +import { useIsUpgradingSecurityPackages } from '../../../../rule_management/logic/use_upgrade_security_packages'; +import { RULES_TABLE_INITIAL_PAGE_SIZE, RULES_TABLE_PAGE_SIZE_OPTIONS } from '../constants'; +import { UpgradePrebuiltRulesTableButtons } from './upgrade_prebuilt_rules_table_buttons'; +import { useUpgradePrebuiltRulesTableContext } from './upgrade_prebuilt_rules_table_context'; +import { useUpgradePrebuiltRulesTableColumns } from './use_upgrade_prebuilt_rules_table_columns'; + +const NO_ITEMS_MESSAGE = ( + {i18n.NO_RULES_AVAILABLE_FOR_UPGRADE}} + titleSize="s" + body={i18n.NO_RULES_AVAILABLE_FOR_UPGRADE_BODY} + /> +); + +/** + * Table Component for displaying rules that have available updates + */ +export const UpgradePrebuiltRulesTable = React.memo(() => { + const isUpgradingSecurityPackages = useIsUpgradingSecurityPackages(); + + const upgradeRulesTableContext = useUpgradePrebuiltRulesTableContext(); + + const { + state: { rules, tags, isFetched, isLoading, isRefetching, selectedRules }, + actions: { selectRules }, + } = upgradeRulesTableContext; + const rulesColumns = useUpgradePrebuiltRulesTableColumns(); + + const isTableEmpty = isFetched && rules.length === 0; + + const shouldShowLinearProgress = (isFetched && isRefetching) || isUpgradingSecurityPackages; + const shouldShowLoadingOverlay = !isFetched && isRefetching; + + return ( + <> + {shouldShowLinearProgress && ( + + )} + + + + + } + loadedContent={ + isTableEmpty ? ( + NO_ITEMS_MESSAGE + ) : ( + ], + filters: [ + { + type: 'field_value_selection', + field: 'rule.tags', + name: 'Tags', + multiSelect: true, + options: tags.map((tag) => ({ + value: tag, + name: tag, + field: 'rule.tags', + })), + }, + ], + }} + pagination={{ + initialPageSize: RULES_TABLE_INITIAL_PAGE_SIZE, + pageSizeOptions: RULES_TABLE_PAGE_SIZE_OPTIONS, + }} + isSelectable + selection={{ + selectable: () => true, + onSelectionChange: selectRules, + initialSelected: selectedRules, + }} + itemId="rule_id" + data-test-subj="rules-upgrades-table" + columns={rulesColumns} + /> + ) + } + /> + + ); +}); + +UpgradePrebuiltRulesTable.displayName = 'UpgradePrebuiltRulesTable'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_buttons.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_buttons.tsx new file mode 100644 index 0000000000000..640fb5a74f9cf --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_buttons.tsx @@ -0,0 +1,50 @@ +/* + * 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 { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import React from 'react'; +import * as i18n from './translations'; +import { useUpgradePrebuiltRulesTableContext } from './upgrade_prebuilt_rules_table_context'; + +export const UpgradePrebuiltRulesTableButtons = () => { + const { + state: { rules, selectedRules, loadingRules }, + actions: { upgradeAllRules, upgradeSelectedRules }, + } = useUpgradePrebuiltRulesTableContext(); + + const isRulesAvailableForUpgrade = rules.length > 0; + const numberOfSelectedRules = selectedRules.length ?? 0; + const shouldDisplayUpgradeSelectedRulesButton = numberOfSelectedRules > 0; + + const isRuleUpgrading = loadingRules.length > 0; + + return ( + + {shouldDisplayUpgradeSelectedRulesButton ? ( + + + <> + {i18n.UPDATE_SELECTED_RULES(numberOfSelectedRules)} + {isRuleUpgrading ? : undefined} + + + + ) : null} + + + {i18n.UPDATE_ALL} + {isRuleUpgrading ? : undefined} + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx new file mode 100644 index 0000000000000..319b9b47e4c54 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx @@ -0,0 +1,190 @@ +/* + * 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 { isEqual } from 'lodash'; +import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'; +import type { RuleUpgradeInfoForReview } from '../../../../../../common/detection_engine/prebuilt_rules/api/review_rule_upgrade/response_schema'; +import type { RuleSignatureId } from '../../../../../../common/detection_engine/rule_schema'; +import { invariant } from '../../../../../../common/utils/invariant'; +import { + usePerformUpgradeAllRules, + usePerformUpgradeSpecificRules, +} from '../../../../rule_management/logic/prebuilt_rules/use_perform_rule_upgrade'; +import { usePrebuiltRulesUpgradeReview } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_upgrade_review'; + +export interface UpgradePrebuiltRulesTableState { + /** + * Rules available to be updated + */ + rules: RuleUpgradeInfoForReview[]; + /** + * All unique tags for all rules + */ + tags: string[]; + /** + * Is true then there is no cached data and the query is currently fetching. + */ + isLoading: boolean; + /** + * Will be true if the query has been fetched. + */ + isFetched: boolean; + /** + * Is true whenever a background refetch is in-flight, which does not include initial loading + */ + isRefetching: boolean; + /** + * List of rule IDs that are currently being upgraded + */ + loadingRules: RuleSignatureId[]; + /** + /** + * The timestamp for when the rules were successfully fetched + */ + lastUpdated: number; + /** + * Rule rows selected in EUI InMemory Table + */ + selectedRules: RuleUpgradeInfoForReview[]; +} + +export interface UpgradePrebuiltRulesTableActions { + reFetchRules: () => void; + upgradeOneRule: (ruleId: string) => void; + upgradeSelectedRules: () => void; + upgradeAllRules: () => void; + selectRules: (rules: RuleUpgradeInfoForReview[]) => void; +} + +export interface UpgradePrebuiltRulesContextType { + state: UpgradePrebuiltRulesTableState; + actions: UpgradePrebuiltRulesTableActions; +} + +const UpgradePrebuiltRulesTableContext = createContext( + null +); + +interface UpgradePrebuiltRulesTableContextProviderProps { + children: React.ReactNode; +} + +export const UpgradePrebuiltRulesTableContextProvider = ({ + children, +}: UpgradePrebuiltRulesTableContextProviderProps) => { + const [loadingRules, setLoadingRules] = useState([]); + const [selectedRules, setSelectedRules] = useState([]); + + const { + data: { rules, stats: { tags } } = { + rules: [], + stats: { tags: [] }, + }, + refetch, + dataUpdatedAt, + isFetched, + isLoading, + isRefetching, + } = usePrebuiltRulesUpgradeReview({ + refetchInterval: false, // Disable automatic refetching since request is expensive + keepPreviousData: true, // Use this option so that the state doesn't jump between "success" and "loading" on page change + }); + + const { mutateAsync: upgradeAllRulesRequest } = usePerformUpgradeAllRules(); + const { mutateAsync: upgradeSpecificRulesRequest } = usePerformUpgradeSpecificRules(); + + const upgradeOneRule = useCallback( + async (ruleId: RuleSignatureId) => { + const rule = rules.find((r) => r.rule_id === ruleId); + invariant(rule, `Rule with id ${ruleId} not found`); + + setLoadingRules((prev) => [...prev, ruleId]); + await upgradeSpecificRulesRequest([ + { + rule_id: ruleId, + version: rule.diff.fields.version?.target_version ?? rule.rule.version, + revision: rule.revision, + }, + ]); + setLoadingRules((prev) => prev.filter((id) => id !== ruleId)); + }, + [rules, upgradeSpecificRulesRequest] + ); + + const upgradeSelectedRules = useCallback(async () => { + const rulesToUpgrade = selectedRules.map((rule) => ({ + rule_id: rule.rule_id, + version: rule.diff.fields.version?.target_version ?? rule.rule.version, + revision: rule.revision, + })); + setLoadingRules((prev) => [...prev, ...rulesToUpgrade.map((r) => r.rule_id)]); + await upgradeSpecificRulesRequest(rulesToUpgrade); + setLoadingRules((prev) => prev.filter((id) => !rulesToUpgrade.some((r) => r.rule_id === id))); + setSelectedRules([]); + }, [selectedRules, upgradeSpecificRulesRequest]); + + const upgradeAllRules = useCallback(async () => { + // Unselect all rules so that the table doesn't show the "bulk actions" bar + setLoadingRules((prev) => [...prev, ...rules.map((r) => r.rule_id)]); + await upgradeAllRulesRequest(); + setLoadingRules((prev) => prev.filter((id) => !rules.some((r) => r.rule_id === id))); + setSelectedRules([]); + }, [rules, upgradeAllRulesRequest]); + + const actions = useMemo( + () => ({ + reFetchRules: refetch, + upgradeOneRule, + upgradeSelectedRules, + upgradeAllRules, + selectRules: setSelectedRules, + }), + [refetch, upgradeAllRules, upgradeOneRule, upgradeSelectedRules] + ); + + const providerValue = useMemo(() => { + return { + state: { + rules, + tags, + isFetched, + isLoading, + isRefetching, + selectedRules, + loadingRules, + lastUpdated: dataUpdatedAt, + }, + actions, + }; + }, [ + rules, + tags, + isFetched, + isLoading, + isRefetching, + selectedRules, + loadingRules, + dataUpdatedAt, + actions, + ]); + + return ( + + {children} + + ); +}; + +export const useUpgradePrebuiltRulesTableContext = (): UpgradePrebuiltRulesContextType => { + const rulesTableContext = useContext(UpgradePrebuiltRulesTableContext); + invariant( + rulesTableContext, + 'useUpgradePrebuiltRulesTableContext should be used inside UpgradePrebuiltRulesTableContextProvider' + ); + + return rulesTableContext; +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_prebuilt_rules_table_columns.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_prebuilt_rules_table_columns.tsx new file mode 100644 index 0000000000000..78ef1f7069ace --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_prebuilt_rules_table_columns.tsx @@ -0,0 +1,141 @@ +/* + * 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 type { EuiBasicTableColumn } from '@elastic/eui'; +import { EuiBadge, EuiButtonEmpty, EuiLoadingSpinner, EuiText } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { SHOW_RELATED_INTEGRATIONS_SETTING } from '../../../../../../common/constants'; +import type { RuleUpgradeInfoForReview } from '../../../../../../common/detection_engine/prebuilt_rules/api/review_rule_upgrade/response_schema'; +import type { RuleSignatureId } from '../../../../../../common/detection_engine/rule_schema'; +import { PopoverItems } from '../../../../../common/components/popover_items'; +import { useUiSetting$ } from '../../../../../common/lib/kibana'; +import { hasUserCRUDPermission } from '../../../../../common/utils/privileges'; +import { IntegrationsPopover } from '../../../../../detections/components/rules/related_integrations/integrations_popover'; +import { SeverityBadge } from '../../../../../detections/components/rules/severity_badge'; +import { useUserData } from '../../../../../detections/components/user_info'; +import * as i18n from '../../../../../detections/pages/detection_engine/rules/translations'; +import type { Rule } from '../../../../rule_management/logic'; +import type { UpgradePrebuiltRulesTableActions } from './upgrade_prebuilt_rules_table_context'; +import { useUpgradePrebuiltRulesTableContext } from './upgrade_prebuilt_rules_table_context'; + +export type TableColumn = EuiBasicTableColumn; + +const RULE_NAME_COLUMN: TableColumn = { + field: 'rule.name', + name: i18n.COLUMN_RULE, + render: (value: RuleUpgradeInfoForReview['rule']['name']) => ( + + {value} + + ), + sortable: true, + truncateText: true, + width: '60%', + align: 'left', +}; + +const TAGS_COLUMN: TableColumn = { + field: 'rule.tags', + name: null, + align: 'center', + render: (tags: Rule['tags']) => { + if (tags == null || tags.length === 0) { + return null; + } + + const renderItem = (tag: string, i: number) => ( + + {tag} + + ); + return ( + + ); + }, + width: '65px', + truncateText: true, +}; + +const INTEGRATIONS_COLUMN: TableColumn = { + field: 'rule.related_integrations', + name: null, + align: 'center', + render: (integrations: Rule['related_integrations']) => { + if (integrations == null || integrations.length === 0) { + return null; + } + + return ; + }, + width: '143px', + truncateText: true, +}; + +const createUpgradeButtonColumn = ( + upgradeOneRule: UpgradePrebuiltRulesTableActions['upgradeOneRule'], + loadingRules: RuleSignatureId[] +): TableColumn => ({ + field: 'rule_id', + name: '', + render: (ruleId: RuleUpgradeInfoForReview['rule_id']) => { + const isRuleUpgrading = loadingRules.includes(ruleId); + return ( + upgradeOneRule(ruleId)}> + {isRuleUpgrading ? : i18n.UPDATE_RULE_BUTTON} + + ); + }, + width: '10%', + align: 'center', +}); + +export const useUpgradePrebuiltRulesTableColumns = (): TableColumn[] => { + const [{ canUserCRUD }] = useUserData(); + const hasCRUDPermissions = hasUserCRUDPermission(canUserCRUD); + const [showRelatedIntegrations] = useUiSetting$(SHOW_RELATED_INTEGRATIONS_SETTING); + const { + state: { loadingRules }, + actions: { upgradeOneRule }, + } = useUpgradePrebuiltRulesTableContext(); + + return useMemo( + () => [ + RULE_NAME_COLUMN, + ...(showRelatedIntegrations ? [INTEGRATIONS_COLUMN] : []), + TAGS_COLUMN, + { + field: 'rule.risk_score', + name: i18n.COLUMN_RISK_SCORE, + render: (value: Rule['risk_score']) => ( + + {value} + + ), + sortable: true, + truncateText: true, + width: '85px', + }, + { + field: 'rule.severity', + name: i18n.COLUMN_SEVERITY, + render: (value: Rule['severity']) => , + sortable: true, + truncateText: true, + width: '12%', + }, + ...(hasCRUDPermissions ? [createUpgradeButtonColumn(upgradeOneRule, loadingRules)] : []), + ], + [hasCRUDPermissions, loadingRules, showRelatedIntegrations, upgradeOneRule] + ); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_columns.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_columns.tsx index cccd9f394d65e..4cd1d43a04ef2 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_columns.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_columns.tsx @@ -45,7 +45,7 @@ import { TableHeaderTooltipCell } from './table_header_tooltip_cell'; import { useHasActionsPrivileges } from './use_has_actions_privileges'; import { useHasMlPermissions } from './use_has_ml_permissions'; import { useRulesTableActions } from './use_rules_table_actions'; -import { MlRuleWarningPopover } from './ml_rule_warning_popover'; +import { MlRuleWarningPopover } from '../ml_rule_warning_popover/ml_rule_warning_popover'; export type TableColumn = EuiBasicTableColumn | EuiTableActionsColumnType; @@ -191,7 +191,7 @@ const TAGS_COLUMN: TableColumn = { name: null, align: 'center', render: (tags: Rule['tags']) => { - if (tags.length === 0) { + if (tags == null || tags.length === 0) { return null; } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_utility_bar.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table_utility_bar/rules_table_utility_bar.test.tsx similarity index 70% rename from x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_utility_bar.test.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table_utility_bar/rules_table_utility_bar.test.tsx index 2b88020f4ff35..650ec328fb5b4 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_utility_bar.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table_utility_bar/rules_table_utility_bar.test.tsx @@ -7,14 +7,13 @@ import React from 'react'; import { mount } from 'enzyme'; -import { waitFor } from '@testing-library/react'; import { getShowingRulesParams, RulesTableUtilityBar } from './rules_table_utility_bar'; import { TestProviders } from '../../../../common/mock'; -import { useRulesTableContextMock } from './rules_table/__mocks__/rules_table_context'; -import { useRulesTableContext } from './rules_table/rules_table_context'; +import { useRulesTableContextMock } from '../rules_table/rules_table/__mocks__/rules_table_context'; +import { useRulesTableContext } from '../rules_table/rules_table/rules_table_context'; -jest.mock('./rules_table/rules_table_context'); +jest.mock('../rules_table/rules_table/rules_table_context'); describe('RulesTableUtilityBar', () => { it('renders RulesTableUtilityBar total rules and selected rules', () => { @@ -92,51 +91,6 @@ describe('RulesTableUtilityBar', () => { expect(rulesTableContext.actions.reFetchRules).toHaveBeenCalledTimes(1); }); - it('invokes rule refetch when auto refresh switch is clicked if there are not selected items', async () => { - const rulesTableContext = useRulesTableContextMock.create(); - rulesTableContext.state.isRefreshOn = false; - (useRulesTableContext as jest.Mock).mockReturnValue(rulesTableContext); - - const wrapper = mount( - - - - ); - - await waitFor(() => { - wrapper.find('[data-test-subj="refreshSettings"] button').first().simulate('click'); - wrapper.find('[data-test-subj="refreshSettingsSwitch"] button').first().simulate('click'); - expect(rulesTableContext.actions.reFetchRules).toHaveBeenCalledTimes(1); - }); - }); - - it('does not invokes onRefreshSwitch when auto refresh switch is clicked if there are selected items', async () => { - const rulesTableContext = useRulesTableContextMock.create(); - rulesTableContext.state.isRefreshOn = false; - rulesTableContext.state.selectedRuleIds = ['testId']; - (useRulesTableContext as jest.Mock).mockReturnValue(rulesTableContext); - - const wrapper = mount( - - - - ); - - await waitFor(() => { - wrapper.find('[data-test-subj="refreshSettings"] button').first().simulate('click'); - wrapper.find('[data-test-subj="refreshSettingsSwitch"] button').first().simulate('click'); - expect(rulesTableContext.actions.reFetchRules).not.toHaveBeenCalled(); - }); - }); - describe('getShowingRulesParams creates correct label when', () => { it('there are 0 rules to display', () => { const pagination = { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_utility_bar.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table_utility_bar/rules_table_utility_bar.tsx similarity index 69% rename from x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_utility_bar.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table_utility_bar/rules_table_utility_bar.tsx index 9c681d95ae520..254387ddd7a30 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_utility_bar.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table_utility_bar/rules_table_utility_bar.tsx @@ -5,16 +5,9 @@ * 2.0. */ -import type { EuiSwitchEvent, EuiContextMenuPanelDescriptor } from '@elastic/eui'; -import { - EuiContextMenu, - EuiContextMenuPanel, - EuiSwitch, - EuiTextColor, - EuiSpacer, -} from '@elastic/eui'; +import type { EuiContextMenuPanelDescriptor } from '@elastic/eui'; +import { EuiContextMenu } from '@elastic/eui'; import React, { useCallback } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; import { UtilityBar, @@ -25,10 +18,11 @@ import { } from '../../../../common/components/utility_bar'; import * as i18n from '../../../../detections/pages/detection_engine/rules/translations'; import { useKibana } from '../../../../common/lib/kibana'; -import { useRulesTableContext } from './rules_table/rules_table_context'; +import { useRulesTableContext } from '../rules_table/rules_table/rules_table_context'; import type { PaginationOptions } from '../../../rule_management/logic/types'; import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction'; import { RULES_TABLE_ACTIONS } from '../../../../common/lib/apm/user_actions'; +import { AutoRefreshButton } from '../auto_refresh_button/auto_refresh_button'; export const getShowingRulesParams = ({ page, perPage, total: totalRules }: PaginationOptions) => { const firstInPage = totalRules === 0 ? 0 : (page - 1) * perPage + 1; @@ -37,7 +31,7 @@ export const getShowingRulesParams = ({ page, perPage, total: totalRules }: Pagi return [firstInPage, lastInPage, totalRules] as const; }; -interface RulesTableUtilityBarProps { +export interface RulesTableUtilityBarProps { canBulkEdit: boolean; onGetBulkItemsPopoverContent?: (closePopover: () => void) => EuiContextMenuPanelDescriptor[]; onToggleSelectAll: () => void; @@ -77,50 +71,6 @@ export const RulesTableUtilityBar = React.memo( [onGetBulkItemsPopoverContent] ); - const handleAutoRefreshSwitch = useCallback( - (closePopover: () => void) => (e: EuiSwitchEvent) => { - const refreshOn = e.target.checked; - if (refreshOn) { - reFetchRules(); - } - setIsRefreshOn(refreshOn); - closePopover(); - }, - [reFetchRules, setIsRefreshOn] - ); - - const handleGetRefreshSettingsPopoverContent = useCallback( - (closePopover: () => void) => ( - , - ...(isAnyRuleSelected - ? [ -
- - - - -
, - ] - : []), - ]} - /> - ), - [isRefreshOn, handleAutoRefreshSwitch, isAnyRuleSelected] - ); - return ( @@ -170,15 +120,6 @@ export const RulesTableUtilityBar = React.memo( > {i18n.REFRESH} - - {i18n.REFRESH_RULE_POPOVER_LABEL} - {!rulesTableContext.state.isDefault && ( ( showUpdating: rulesTableContext.state.isFetching, updatedAt: rulesTableContext.state.lastUpdated, })} + ); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/add_rules/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/add_rules/index.tsx new file mode 100644 index 0000000000000..3a776a1029522 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/add_rules/index.tsx @@ -0,0 +1,70 @@ +/* + * 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 from 'react'; + +import { redirectToDetections } from '../../../../detections/pages/detection_engine/rules/helpers'; +import { SecurityPageName } from '../../../../app/types'; +import { HeaderPage } from '../../../../common/components/header_page'; +import { SecuritySolutionPageWrapper } from '../../../../common/components/page_wrapper'; +import { useKibana } from '../../../../common/lib/kibana'; +import { SpyRoute } from '../../../../common/utils/route/spy_routes'; + +import { useUserData } from '../../../../detections/components/user_info'; +import { useListsConfig } from '../../../../detections/containers/detection_engine/lists/use_lists_config'; + +import * as i18n from './translations'; +import { AddPrebuiltRulesTable } from '../../components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table'; +import { AddPrebuiltRulesTableContextProvider } from '../../components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context'; +import { AddPrebuiltRulesHeaderButtons } from '../../components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_header_buttons'; +import { APP_UI_ID } from '../../../../../common'; +import { NeedAdminForUpdateRulesCallOut } from '../../../../detections/components/callouts/need_admin_for_update_callout'; +import { MissingPrivilegesCallOut } from '../../../../detections/components/callouts/missing_privileges_callout'; +import { getDetectionEngineUrl } from '../../../../common/components/link_to'; + +const AddRulesPageComponent: React.FC = () => { + const { navigateToApp } = useKibana().services.application; + + const [{ isSignalIndexExists, isAuthenticated, hasEncryptionKey }] = useUserData(); + const { needsConfiguration: needsListsConfiguration } = useListsConfig(); + + if ( + redirectToDetections( + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + needsListsConfiguration + ) + ) { + navigateToApp(APP_UI_ID, { + deepLinkId: SecurityPageName.alerts, + path: getDetectionEngineUrl(), + }); + return null; + } + + return ( + <> + + + + + + + + + + + + + + + ); +}; + +export const AddRulesPage = React.memo(AddRulesPageComponent); +AddRulesPage.displayName = 'AddRulesPage'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/add_rules/translations.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/add_rules/translations.tsx new file mode 100644 index 0000000000000..e9c43e411ee37 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/add_rules/translations.tsx @@ -0,0 +1,15 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const PAGE_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.addRules.pageTitle', + { + defaultMessage: 'Add Elastic Rules', + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx index 1e2ae5b88fba7..7af3ef9e74fcf 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx @@ -6,12 +6,15 @@ */ import React, { useCallback } from 'react'; -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiToolTip } from '@elastic/eui'; import { APP_UI_ID } from '../../../../../common/constants'; import { SecurityPageName } from '../../../../app/types'; import { ImportDataModal } from '../../../../common/components/import_data_modal'; -import { SecuritySolutionLinkButton } from '../../../../common/components/links'; +import { + SecuritySolutionLinkButton, + useGetSecuritySolutionLinkProps, +} from '../../../../common/components/links'; import { getDetectionEngineUrl } from '../../../../common/components/link_to/redirect_to_detection_engine'; import { SecuritySolutionPageWrapper } from '../../../../common/components/page_wrapper'; import { useBoolState } from '../../../../common/hooks/use_bool_state'; @@ -22,9 +25,7 @@ import { SpyRoute } from '../../../../common/utils/route/spy_routes'; import { MissingPrivilegesCallOut } from '../../../../detections/components/callouts/missing_privileges_callout'; import { MlJobCompatibilityCallout } from '../../../../detections/components/callouts/ml_job_compatibility_callout'; import { NeedAdminForUpdateRulesCallOut } from '../../../../detections/components/callouts/need_admin_for_update_callout'; -import { LoadPrePackagedRules } from '../../../../detections/components/rules/pre_packaged_rules/load_prepackaged_rules'; import { LoadPrePackagedRulesButton } from '../../../../detections/components/rules/pre_packaged_rules/load_prepackaged_rules_button'; -import { UpdatePrePackagedRulesCallOut } from '../../../../detections/components/rules/pre_packaged_rules/update_callout'; import { ValueListsFlyout } from '../../../../detections/components/value_lists_management_flyout'; import { useUserData } from '../../../../detections/components/user_info'; import { useListsConfig } from '../../../../detections/containers/detection_engine/lists/use_lists_config'; @@ -32,17 +33,22 @@ import { redirectToDetections } from '../../../../detections/pages/detection_eng import { useInvalidateFindRulesQuery } from '../../../rule_management/api/hooks/use_find_rules_query'; import { importRules } from '../../../rule_management/logic'; -import { usePrePackagedRulesInstallationStatus } from '../../../rule_management/logic/use_pre_packaged_rules_installation_status'; -import { usePrePackagedTimelinesInstallationStatus } from '../../../rule_management/logic/use_pre_packaged_timelines_installation_status'; import { AllRules } from '../../components/rules_table'; import { RulesTableContextProvider } from '../../components/rules_table/rules_table/rules_table_context'; import * as i18n from '../../../../detections/pages/detection_engine/rules/translations'; import { useInvalidateFetchRuleManagementFiltersQuery } from '../../../rule_management/api/hooks/use_fetch_rule_management_filters_query'; +import { MiniCallout } from '../../components/mini_callout/mini_callout'; +import { usePrebuiltRulesStatus } from '../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_status'; import { MaintenanceWindowCallout } from '../../components/maintenance_window_callout/maintenance_window_callout'; import { SuperHeader } from './super_header'; +import { + NEW_PREBUILT_RULES_AVAILABLE_CALLOUT_TITLE, + getUpdateRulesCalloutTitle, +} from '../../components/mini_callout/translations'; +import { AllRulesTabs } from '../../components/rules_table/rules_table_toolbar'; const RulesPageComponent: React.FC = () => { const [isImportModalVisible, showImportModal, hideImportModal] = useBoolState(); @@ -55,6 +61,15 @@ const RulesPageComponent: React.FC = () => { invalidateFetchRuleManagementFilters(); }, [invalidateFindRulesQuery, invalidateFetchRuleManagementFilters]); + const { data: prebuiltRulesStatus } = usePrebuiltRulesStatus(); + + const rulesToInstallCount = prebuiltRulesStatus?.num_prebuilt_rules_to_install ?? 0; + const rulesToUpgradeCount = prebuiltRulesStatus?.num_prebuilt_rules_to_upgrade ?? 0; + + // Check against rulesInstalledCount since we don't want to show banners if we're showing the empty prompt + const shouldDisplayNewRulesCallout = rulesToInstallCount > 0; + const shouldDisplayUpdateRulesCallout = rulesToUpgradeCount > 0; + const [ { loading: userInfoLoading, @@ -70,8 +85,19 @@ const RulesPageComponent: React.FC = () => { needsConfiguration: needsListsConfiguration, } = useListsConfig(); const loading = userInfoLoading || listsConfigLoading; - const prePackagedRuleStatus = usePrePackagedRulesInstallationStatus(); - const prePackagedTimelineStatus = usePrePackagedTimelinesInstallationStatus(); + + const getSecuritySolutionLinkProps = useGetSecuritySolutionLinkProps(); + const { href } = getSecuritySolutionLinkProps({ + deepLinkId: SecurityPageName.rules, + path: AllRulesTabs.updates, + }); + const { + application: { navigateToUrl }, + } = useKibana().services; + + const updateCallOutOnClick = useCallback(() => { + navigateToUrl(href); + }, [navigateToUrl, href]); if ( redirectToDetections( @@ -117,31 +143,29 @@ const RulesPageComponent: React.FC = () => { - - {(renderProps) => } - + - {i18n.IMPORT_VALUE_LISTS} - + - {i18n.IMPORT_RULE} - + { - {(prePackagedRuleStatus === 'ruleNeedUpdate' || - prePackagedTimelineStatus === 'timelineNeedUpdate') && ( - + + {shouldDisplayUpdateRulesCallout && ( + + )} + + {shouldDisplayUpdateRulesCallout && shouldDisplayNewRulesCallout && ( + + )} + + {shouldDisplayNewRulesCallout && ( + )} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx deleted file mode 100644 index bd1b951a7002f..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx +++ /dev/null @@ -1,181 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { waitFor } from '@testing-library/react'; -import type { ReactWrapper } from 'enzyme'; -import { mount } from 'enzyme'; -import React from 'react'; -import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; -import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock'; -import { TestProviders } from '../../../../common/mock'; -import '../../../../common/mock/match_media'; -import { getPrePackagedRulesStatus } from '../../../../detection_engine/rule_management/api/api'; -import { PrePackagedRulesPrompt } from './load_empty_prompt'; - -jest.mock('react-router-dom', () => { - const original = jest.requireActual('react-router-dom'); - - return { - ...original, - useHistory: () => ({ - useHistory: jest.fn(), - }), - }; -}); - -jest.mock('../../../../common/components/link_to'); -jest.mock('../../../../common/lib/kibana/kibana_react', () => { - const original = jest.requireActual('../../../../common/lib/kibana/kibana_react'); - return { - ...original, - useKibana: () => ({ - services: { - application: { - navigateToApp: jest.fn(), - getUrlForApp: jest.fn(), - }, - }, - }), - }; -}); - -jest.mock('../../../../detection_engine/rule_management/api/api', () => ({ - getPrePackagedRulesStatus: jest.fn().mockResolvedValue({ - rules_not_installed: 0, - rules_installed: 0, - rules_not_updated: 0, - timelines_not_installed: 0, - timelines_installed: 0, - timelines_not_updated: 0, - }), - createPrepackagedRules: jest.fn(), -})); -jest.mock('../../../../common/hooks/use_app_toasts'); - -const props = { - createPrePackagedRules: jest.fn(), - loading: false, - userHasPermissions: true, - 'data-test-subj': 'load-prebuilt-rules', -}; - -describe('PrePackagedRulesPrompt', () => { - let appToastsMock: jest.Mocked>; - - beforeEach(() => { - appToastsMock = useAppToastsMock.create(); - (useAppToasts as jest.Mock).mockReturnValue(appToastsMock); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('renders correctly', () => { - const wrapper = mount(, { - wrappingComponent: TestProviders, - }); - - expect(wrapper.find('EmptyPrompt')).toHaveLength(1); - }); -}); - -describe('LoadPrebuiltRulesAndTemplatesButton', () => { - it('renders correct button with correct text - Load Elastic prebuilt rules and timeline templates', async () => { - (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ - rules_not_installed: 3, - rules_installed: 0, - rules_not_updated: 0, - timelines_not_installed: 3, - timelines_installed: 0, - timelines_not_updated: 0, - }); - - const wrapper: ReactWrapper = mount(, { - wrappingComponent: TestProviders, - }); - await waitFor(() => { - wrapper.update(); - - expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').exists()).toEqual(true); - expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').last().text()).toEqual( - 'Load Elastic prebuilt rules and timeline templates' - ); - }); - }); - - it('renders correct button with correct text - Load Elastic prebuilt rules', async () => { - (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ - rules_not_installed: 3, - rules_installed: 0, - rules_not_updated: 0, - timelines_not_installed: 0, - timelines_installed: 0, - timelines_not_updated: 0, - }); - - const wrapper: ReactWrapper = mount(, { - wrappingComponent: TestProviders, - }); - await waitFor(() => { - wrapper.update(); - - expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').exists()).toEqual(true); - expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').last().text()).toEqual( - 'Load Elastic prebuilt rules' - ); - }); - }); - - it('renders correct button with correct text - Load Elastic prebuilt timeline templates', async () => { - (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ - rules_not_installed: 0, - rules_installed: 0, - rules_not_updated: 0, - timelines_not_installed: 3, - timelines_installed: 0, - timelines_not_updated: 0, - }); - - const wrapper: ReactWrapper = mount(, { - wrappingComponent: TestProviders, - }); - await waitFor(() => { - wrapper.update(); - - expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').exists()).toEqual(true); - expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').last().text()).toEqual( - 'Load Elastic prebuilt timeline templates' - ); - }); - }); - - it('renders disabled button if loading is true', async () => { - (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ - rules_not_installed: 0, - rules_installed: 0, - rules_not_updated: 0, - timelines_not_installed: 3, - timelines_installed: 0, - timelines_not_updated: 0, - }); - - const wrapper: ReactWrapper = mount( - , - { - wrappingComponent: TestProviders, - } - ); - await waitFor(() => { - wrapper.update(); - - expect(wrapper.find('button[data-test-subj="load-prebuilt-rules"]').props().disabled).toEqual( - true - ); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx index f44008622eb9e..da6ccd39753c8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx @@ -8,11 +8,6 @@ import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { memo } from 'react'; import styled from 'styled-components'; -import { SecurityPageName } from '../../../../app/types'; -import { SecuritySolutionLinkButton } from '../../../../common/components/links'; -import { hasUserCRUDPermission } from '../../../../common/utils/privileges'; -import { useUserData } from '../../user_info'; -import { LoadPrePackagedRules } from './load_prepackaged_rules'; import { LoadPrePackagedRulesButton } from './load_prepackaged_rules_button'; import * as i18n from './translations'; @@ -23,9 +18,6 @@ const EmptyPrompt = styled(EuiEmptyPrompt)` EmptyPrompt.displayName = 'EmptyPrompt'; const PrePackagedRulesPromptComponent = () => { - const [{ canUserCRUD }] = useUserData(); - const hasPermissions = hasUserCRUDPermission(canUserCRUD); - return ( { actions={ - - {(renderProps) => ( - - )} - - - - - {i18n.CREATE_RULE_ACTION} - + } diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_prepackaged_rules.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_prepackaged_rules.tsx deleted file mode 100644 index 9ec37ecfcb7c0..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_prepackaged_rules.tsx +++ /dev/null @@ -1,77 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useCallback } from 'react'; -import { useInstalledSecurityJobs } from '../../../../common/components/ml/hooks/use_installed_security_jobs'; -import { useBoolState } from '../../../../common/hooks/use_bool_state'; -import { RULES_TABLE_ACTIONS } from '../../../../common/lib/apm/user_actions'; -import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction'; -import { useCreatePrePackagedRules } from '../../../../detection_engine/rule_management/logic/use_create_pre_packaged_rules'; -import { useIsInstallingPrePackagedRules } from '../../../../detection_engine/rule_management/logic/use_install_pre_packaged_rules'; -import { usePrePackagedRulesStatus } from '../../../../detection_engine/rule_management/logic/use_pre_packaged_rules_status'; -import { affectedJobIds } from '../../callouts/ml_job_compatibility_callout/affected_job_ids'; -import { MlJobUpgradeModal } from '../../modals/ml_job_upgrade_modal'; - -interface LoadPrePackagedRulesRenderProps { - isLoading: boolean; - isDisabled: boolean; - onClick: () => Promise; -} - -interface LoadPrePackagedRulesProps { - children: (renderProps: LoadPrePackagedRulesRenderProps) => React.ReactNode; -} - -export const LoadPrePackagedRules = ({ children }: LoadPrePackagedRulesProps) => { - const { isFetching: isFetchingPrepackagedStatus } = usePrePackagedRulesStatus(); - const isInstallingPrebuiltRules = useIsInstallingPrePackagedRules(); - const { createPrePackagedRules, canCreatePrePackagedRules } = useCreatePrePackagedRules(); - - const { startTransaction } = useStartTransaction(); - const handleCreatePrePackagedRules = useCallback(async () => { - startTransaction({ name: RULES_TABLE_ACTIONS.LOAD_PREBUILT }); - await createPrePackagedRules(); - }, [createPrePackagedRules, startTransaction]); - - const [isUpgradeModalVisible, showUpgradeModal, hideUpgradeModal] = useBoolState(false); - const { loading: loadingJobs, jobs } = useInstalledSecurityJobs(); - const legacyJobsInstalled = jobs.filter((job) => affectedJobIds.includes(job.id)); - - const handleInstallPrePackagedRules = useCallback(async () => { - if (legacyJobsInstalled.length > 0) { - showUpgradeModal(); - } else { - await handleCreatePrePackagedRules(); - } - }, [handleCreatePrePackagedRules, legacyJobsInstalled.length, showUpgradeModal]); - - // Wrapper to add confirmation modal for users who may be running older ML Jobs that would - // be overridden by updating their rules. For details, see: https://github.com/elastic/kibana/issues/128121 - const mlJobUpgradeModalConfirm = useCallback(() => { - hideUpgradeModal(); - handleCreatePrePackagedRules(); - }, [handleCreatePrePackagedRules, hideUpgradeModal]); - - const isDisabled = !canCreatePrePackagedRules || isFetchingPrepackagedStatus || loadingJobs; - - return ( - <> - {children({ - isLoading: isInstallingPrebuiltRules, - isDisabled, - onClick: handleInstallPrePackagedRules, - })} - {isUpgradeModalVisible && ( - hideUpgradeModal()} - onConfirm={mlJobUpgradeModalConfirm} - /> - )} - - ); -}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_prepackaged_rules_button.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_prepackaged_rules_button.tsx index dc59cc64eee9f..78efbab06024c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_prepackaged_rules_button.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_prepackaged_rules_button.tsx @@ -5,111 +5,58 @@ * 2.0. */ -import { EuiButton } from '@elastic/eui'; +import { EuiBadge, EuiButton, EuiButtonEmpty } from '@elastic/eui'; import React from 'react'; -import { usePrePackagedRulesInstallationStatus } from '../../../../detection_engine/rule_management/logic/use_pre_packaged_rules_installation_status'; -import { usePrePackagedRulesStatus } from '../../../../detection_engine/rule_management/logic/use_pre_packaged_rules_status'; -import { usePrePackagedTimelinesInstallationStatus } from '../../../../detection_engine/rule_management/logic/use_pre_packaged_timelines_installation_status'; +import { css } from '@emotion/react'; import { INSTALL_PREBUILT_RULES_ANCHOR } from '../../../../detection_engine/rule_management_ui/components/rules_table/rules_table/guided_onboarding/rules_management_tour'; -import type { - PrePackagedRuleInstallationStatus, - PrePackagedTimelineInstallationStatus, -} from '../../../pages/detection_engine/rules/helpers'; import * as i18n from './translations'; +import { useGetSecuritySolutionLinkProps } from '../../../../common/components/links'; +import { SecurityPageName } from '../../../../../common'; +import { usePrebuiltRulesStatus } from '../../../../detection_engine/rule_management/logic/prebuilt_rules/use_prebuilt_rules_status'; -const getLoadRulesOrTimelinesButtonTitle = ( - rulesStatus: PrePackagedRuleInstallationStatus, - timelineStatus: PrePackagedTimelineInstallationStatus -) => { - if (rulesStatus === 'ruleNotInstalled' && timelineStatus === 'timelinesNotInstalled') - return i18n.LOAD_PREPACKAGED_RULES_AND_TEMPLATES; - else if (rulesStatus === 'ruleNotInstalled' && timelineStatus !== 'timelinesNotInstalled') - return i18n.LOAD_PREPACKAGED_RULES; - else if (rulesStatus !== 'ruleNotInstalled' && timelineStatus === 'timelinesNotInstalled') - return i18n.LOAD_PREPACKAGED_TIMELINE_TEMPLATES; -}; - -const getMissingRulesOrTimelinesButtonTitle = (missingRules: number, missingTimelines: number) => { - if (missingRules > 0 && missingTimelines === 0) - return i18n.RELOAD_MISSING_PREPACKAGED_RULES(missingRules); - else if (missingRules === 0 && missingTimelines > 0) - return i18n.RELOAD_MISSING_PREPACKAGED_TIMELINES(missingTimelines); - else if (missingRules > 0 && missingTimelines > 0) - return i18n.RELOAD_MISSING_PREPACKAGED_RULES_AND_TIMELINES(missingRules, missingTimelines); -}; +// TODO: Still need to load timeline templates interface LoadPrePackagedRulesButtonProps { - fill?: boolean; 'data-test-subj'?: string; - isLoading: boolean; - isDisabled: boolean; - onClick: () => Promise; + fill?: boolean; + showBadge?: boolean; } export const LoadPrePackagedRulesButton = ({ - fill, 'data-test-subj': dataTestSubj = 'loadPrebuiltRulesBtn', - isLoading, - isDisabled, - onClick, + fill, + showBadge = true, }: LoadPrePackagedRulesButtonProps) => { - const { data: prePackagedRulesStatus } = usePrePackagedRulesStatus(); - const prePackagedAssetsStatus = usePrePackagedRulesInstallationStatus(); - const prePackagedTimelineStatus = usePrePackagedTimelinesInstallationStatus(); - - const showInstallButton = - (prePackagedAssetsStatus === 'ruleNotInstalled' || - prePackagedTimelineStatus === 'timelinesNotInstalled') && - prePackagedAssetsStatus !== 'someRuleUninstall'; - - if (showInstallButton) { - // Without the outer div EuiStepTour crashes with Uncaught DOMException: - // Failed to execute 'removeChild' on 'Node': The node to be removed is not - // a child of this node. - return ( -
- - {getLoadRulesOrTimelinesButtonTitle(prePackagedAssetsStatus, prePackagedTimelineStatus)} - -
- ); - } - - const showUpdateButton = - prePackagedAssetsStatus === 'someRuleUninstall' || - prePackagedTimelineStatus === 'someTimelineUninstall'; - - if (showUpdateButton) { - // Without the outer div EuiStepTour crashes with Uncaught DOMException: - // Failed to execute 'removeChild' on 'Node': The node to be removed is not - // a child of this node. - return ( -
- + {i18n.ADD_ELASTIC_RULES} + {newRulesCount > 0 && showBadge && ( + - {getMissingRulesOrTimelinesButtonTitle( - prePackagedRulesStatus?.rules_not_installed ?? 0, - prePackagedRulesStatus?.timelines_not_installed ?? 0 - )} - -
- ); - } - - return null; + {newRulesCount} + + )} + + ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts index b1207b59faf8b..aa36b18beecdf 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts @@ -28,47 +28,23 @@ export const CREATE_RULE_ACTION = i18n.translate( defaultMessage: 'Create your own rules', } ); - -export const UPDATE_PREPACKAGED_RULES_TITLE = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.updatePrePackagedRulesTitle', +export const RULE_UPDATES_LINK = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.ruleUpdatesLinkTitle', { - defaultMessage: 'Update available for Elastic prebuilt rules or timeline templates', + defaultMessage: 'Rule Updates', } ); -export const UPDATE_PREPACKAGED_RULES_MSG = (updateRules: number) => - i18n.translate('xpack.securitySolution.detectionEngine.rules.updatePrePackagedRulesMsg', { - values: { updateRules }, - defaultMessage: - 'You can update {updateRules} Elastic prebuilt {updateRules, plural, =1 {rule} other {rules}}', - }); - -export const UPDATE_PREPACKAGED_TIMELINES_MSG = (updateTimelines: number) => - i18n.translate('xpack.securitySolution.detectionEngine.rules.updatePrePackagedTimelinesMsg', { - values: { updateTimelines }, - defaultMessage: - 'You can update {updateTimelines} Elastic prebuilt {updateTimelines, plural, =1 {timeline} other {timelines}}', - }); - -export const UPDATE_PREPACKAGED_RULES_AND_TIMELINES_MSG = ( - updateRules: number, - updateTimelines: number -) => - i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.updatePrePackagedRulesAndTimelinesMsg', - { - values: { updateRules, updateTimelines }, - defaultMessage: - 'You can update {updateRules} Elastic prebuilt {updateRules, plural, =1 {rule} other {rules}} and {updateTimelines} Elastic prebuilt {updateTimelines, plural, =1 {timeline} other {timelines}}. Note that this will reload deleted Elastic prebuilt rules.', - } - ); +export const ADD_ELASTIC_RULES = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.addElasticRulesButtonTitle', + { + defaultMessage: 'Add Elastic rules', + } +); -export const UPDATE_PREPACKAGED_RULES = (updateRules: number) => - i18n.translate('xpack.securitySolution.detectionEngine.rules.updatePrePackagedRulesButton', { - values: { updateRules }, - defaultMessage: - 'Update {updateRules} Elastic prebuilt {updateRules, plural, =1 {rule} other {rules}}', - }); +export const DISMISS = i18n.translate('xpack.securitySolution.detectionEngine.rules.dismissTitle', { + defaultMessage: 'Dismiss', +}); export const UPDATE_PREPACKAGED_TIMELINES = (updateTimelines: number) => i18n.translate('xpack.securitySolution.detectionEngine.rules.updatePrePackagedTimelinesButton', { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.test.tsx deleted file mode 100644 index 6d882fe2930a3..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.test.tsx +++ /dev/null @@ -1,167 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { render } from '@testing-library/react'; -import React from 'react'; -import { useKibana } from '../../../../common/lib/kibana'; -import { TestProviders } from '../../../../common/mock'; -import { useFetchPrebuiltRulesStatusQuery } from '../../../../detection_engine/rule_management/api/hooks/use_fetch_prebuilt_rules_status_query'; -import { mockReactQueryResponse } from '../../../../detection_engine/rule_management/api/hooks/__mocks__/mock_react_query_response'; -import { UpdatePrePackagedRulesCallOut } from './update_callout'; - -jest.mock('../../../../common/lib/kibana'); -jest.mock( - '../../../../detection_engine/rule_management/api/hooks/use_fetch_prebuilt_rules_status_query' -); - -describe('UpdatePrePackagedRulesCallOut', () => { - beforeAll(() => { - (useKibana as jest.Mock).mockReturnValue({ - services: { - docLinks: { - ELASTIC_WEBSITE_URL: '', - DOC_LINK_VERSION: '', - links: { - siem: { ruleChangeLog: '' }, - }, - }, - }, - }); - }); - - it('renders callOutMessage correctly: numberOfUpdatedRules > 0 and numberOfUpdatedTimelines = 0', () => { - (useFetchPrebuiltRulesStatusQuery as jest.Mock).mockReturnValue( - mockReactQueryResponse({ - data: { - rules_custom_installed: 0, - rules_installed: 0, - rules_not_installed: 0, - rules_not_updated: 1, - timelines_updated: 0, - timelines_not_installed: 0, - timelines_not_updated: 0, - }, - }) - ); - - const { getByTestId } = render(, { wrapper: TestProviders }); - - expect(getByTestId('update-callout')).toHaveTextContent( - 'You can update 1 Elastic prebuilt ruleRelease notes' - ); - }); - - it('renders buttonTitle correctly: numberOfUpdatedRules > 0 and numberOfUpdatedTimelines = 0', () => { - (useFetchPrebuiltRulesStatusQuery as jest.Mock).mockReturnValue( - mockReactQueryResponse({ - data: { - rules_custom_installed: 0, - rules_installed: 0, - rules_not_installed: 0, - rules_not_updated: 1, - timelines_updated: 0, - timelines_not_installed: 0, - timelines_not_updated: 0, - }, - }) - ); - - const { getByTestId } = render(, { wrapper: TestProviders }); - - expect(getByTestId('update-callout-button')).toHaveTextContent( - 'Update 1 Elastic prebuilt rule' - ); - }); - - it('renders callOutMessage correctly: numberOfUpdatedRules = 0 and numberOfUpdatedTimelines > 0', () => { - (useFetchPrebuiltRulesStatusQuery as jest.Mock).mockReturnValue( - mockReactQueryResponse({ - data: { - rules_custom_installed: 0, - rules_installed: 0, - rules_not_installed: 0, - rules_not_updated: 0, - timelines_updated: 0, - timelines_not_installed: 0, - timelines_not_updated: 1, - }, - }) - ); - - const { getByTestId } = render(, { wrapper: TestProviders }); - - expect(getByTestId('update-callout')).toHaveTextContent( - 'You can update 1 Elastic prebuilt timelineRelease notes' - ); - }); - - it('renders buttonTitle correctly: numberOfUpdatedRules = 0 and numberOfUpdatedTimelines > 0', () => { - (useFetchPrebuiltRulesStatusQuery as jest.Mock).mockReturnValue( - mockReactQueryResponse({ - data: { - rules_custom_installed: 0, - rules_installed: 0, - rules_not_installed: 0, - rules_not_updated: 0, - timelines_updated: 0, - timelines_not_installed: 0, - timelines_not_updated: 1, - }, - }) - ); - - const { getByTestId } = render(, { wrapper: TestProviders }); - - expect(getByTestId('update-callout-button')).toHaveTextContent( - 'Update 1 Elastic prebuilt timeline' - ); - }); - - it('renders callOutMessage correctly: numberOfUpdatedRules > 0 and numberOfUpdatedTimelines > 0', () => { - (useFetchPrebuiltRulesStatusQuery as jest.Mock).mockReturnValue( - mockReactQueryResponse({ - data: { - rules_custom_installed: 0, - rules_installed: 0, - rules_not_installed: 0, - rules_not_updated: 1, - timelines_updated: 0, - timelines_not_installed: 0, - timelines_not_updated: 1, - }, - }) - ); - - const { getByTestId } = render(, { wrapper: TestProviders }); - - expect(getByTestId('update-callout')).toHaveTextContent( - 'You can update 1 Elastic prebuilt rule and 1 Elastic prebuilt timeline. Note that this will reload deleted Elastic prebuilt rules.Release notes' - ); - }); - - it('renders buttonTitle correctly: numberOfUpdatedRules > 0 and numberOfUpdatedTimelines > 0', () => { - (useFetchPrebuiltRulesStatusQuery as jest.Mock).mockReturnValue( - mockReactQueryResponse({ - data: { - rules_custom_installed: 0, - rules_installed: 0, - rules_not_installed: 0, - rules_not_updated: 1, - timelines_updated: 0, - timelines_not_installed: 0, - timelines_not_updated: 1, - }, - }) - ); - - const { getByTestId } = render(, { wrapper: TestProviders }); - - expect(getByTestId('update-callout-button')).toHaveTextContent( - 'Update 1 Elastic prebuilt rule and 1 Elastic prebuilt timeline' - ); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.tsx deleted file mode 100644 index 1526c211990e3..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.tsx +++ /dev/null @@ -1,65 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiButton, EuiCallOut, EuiLink } from '@elastic/eui'; -import React, { memo, useMemo } from 'react'; -import { useKibana } from '../../../../common/lib/kibana'; -import { usePrePackagedRulesStatus } from '../../../../detection_engine/rule_management/logic/use_pre_packaged_rules_status'; -import { LoadPrePackagedRules } from './load_prepackaged_rules'; -import * as i18n from './translations'; - -const UpdatePrePackagedRulesCallOutComponent = () => { - const { services } = useKibana(); - const { data: prePackagedRulesStatus } = usePrePackagedRulesStatus(); - const rulesNotUpdated = prePackagedRulesStatus?.rules_not_updated ?? 0; - const timelinesNotUpdated = prePackagedRulesStatus?.timelines_not_updated ?? 0; - - const prepackagedRulesOrTimelines = useMemo(() => { - if (rulesNotUpdated > 0 && timelinesNotUpdated === 0) { - return { - callOutMessage: i18n.UPDATE_PREPACKAGED_RULES_MSG(rulesNotUpdated), - buttonTitle: i18n.UPDATE_PREPACKAGED_RULES(rulesNotUpdated), - }; - } else if (rulesNotUpdated === 0 && timelinesNotUpdated > 0) { - return { - callOutMessage: i18n.UPDATE_PREPACKAGED_TIMELINES_MSG(timelinesNotUpdated), - buttonTitle: i18n.UPDATE_PREPACKAGED_TIMELINES(timelinesNotUpdated), - }; - } else if (rulesNotUpdated > 0 && timelinesNotUpdated > 0) - return { - callOutMessage: i18n.UPDATE_PREPACKAGED_RULES_AND_TIMELINES_MSG( - rulesNotUpdated, - timelinesNotUpdated - ), - buttonTitle: i18n.UPDATE_PREPACKAGED_RULES_AND_TIMELINES( - rulesNotUpdated, - timelinesNotUpdated - ), - }; - }, [rulesNotUpdated, timelinesNotUpdated]); - - return ( - -

- {prepackagedRulesOrTimelines?.callOutMessage} -
- - {i18n.RELEASE_NOTES_HELP} - -

- - {(renderProps) => ( - - {prepackagedRulesOrTimelines?.buttonTitle} - - )} - -
- ); -}; - -export const UpdatePrePackagedRulesCallOut = memo(UpdatePrePackagedRulesCallOutComponent); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx index 6b15a59528067..2e76e2f9c8988 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx @@ -15,7 +15,6 @@ import { getActionsStepsData, getHumanizedDuration, getModifiedAboutDetailsData, - getPrePackagedRuleInstallationStatus, getPrePackagedTimelineInstallationStatus, determineDetailsValue, fillEmptySeverityMappings, @@ -430,73 +429,6 @@ describe('rule helpers', () => { }); }); - describe('getPrePackagedRuleStatus', () => { - test('ruleNotInstalled', () => { - const rulesInstalled = 0; - const rulesNotInstalled = 1; - const rulesNotUpdated = 0; - const result: string = getPrePackagedRuleInstallationStatus( - rulesInstalled, - rulesNotInstalled, - rulesNotUpdated - ); - - expect(result).toEqual('ruleNotInstalled'); - }); - - test('ruleInstalled', () => { - const rulesInstalled = 1; - const rulesNotInstalled = 0; - const rulesNotUpdated = 0; - const result: string = getPrePackagedRuleInstallationStatus( - rulesInstalled, - rulesNotInstalled, - rulesNotUpdated - ); - - expect(result).toEqual('ruleInstalled'); - }); - - test('someRuleUninstall', () => { - const rulesInstalled = 1; - const rulesNotInstalled = 1; - const rulesNotUpdated = 0; - const result: string = getPrePackagedRuleInstallationStatus( - rulesInstalled, - rulesNotInstalled, - rulesNotUpdated - ); - - expect(result).toEqual('someRuleUninstall'); - }); - - test('ruleNeedUpdate', () => { - const rulesInstalled = 1; - const rulesNotInstalled = 0; - const rulesNotUpdated = 1; - const result: string = getPrePackagedRuleInstallationStatus( - rulesInstalled, - rulesNotInstalled, - rulesNotUpdated - ); - - expect(result).toEqual('ruleNeedUpdate'); - }); - - test('unknown', () => { - const rulesInstalled = undefined; - const rulesNotInstalled = undefined; - const rulesNotUpdated = undefined; - const result: string = getPrePackagedRuleInstallationStatus( - rulesInstalled, - rulesNotInstalled, - rulesNotUpdated - ); - - expect(result).toEqual('unknown'); - }); - }); - describe('getPrePackagedTimelineStatus', () => { test('timelinesNotInstalled', () => { const timelinesInstalled = 0; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index 9d7c89bbfac02..40280def1d3a2 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -287,45 +287,6 @@ export type PrePackagedTimelineInstallationStatus = | 'timelineNeedUpdate' | 'unknown'; -export const getPrePackagedRuleInstallationStatus = ( - rulesInstalled?: number, - rulesNotInstalled?: number, - rulesNotUpdated?: number -): PrePackagedRuleInstallationStatus => { - 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 getPrePackagedTimelineInstallationStatus = ( timelinesInstalled?: number, timelinesNotInstalled?: number, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts index 3ad119a67c4ac..42820374c22c3 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts @@ -686,6 +686,33 @@ export const NO_RULES_BODY = i18n.translate( } ); +export const NO_RULES_AVAILABLE_FOR_INSTALL = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.addRules.noRulesTitle', + { + defaultMessage: 'All Elastic rules have been installed', + } +); + +export const NO_RULES_AVAILABLE_FOR_INSTALL_BODY = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.addRules.noRulesBodyTitle', + { + defaultMessage: 'There are no prebuilt detection rules available for installation', + } +); +export const NO_RULES_AVAILABLE_FOR_UPGRADE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.noRulesTitle', + { + defaultMessage: 'All Elastic rules are up to date', + } +); + +export const NO_RULES_AVAILABLE_FOR_UPGRADE_BODY = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.noRulesBodyTitle', + { + defaultMessage: 'There are currently no available updates to your installed Elastic rules.', + } +); + export const DEFINE_RULE = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.defineRuleTitle', { @@ -1170,3 +1197,17 @@ export const RULE_MANAGEMENT_CONTEXT_TOOLTIP = i18n.translate( defaultMessage: 'Add this alert as context', } ); + +export const INSTALL_RULE_BUTTON = i18n.translate( + 'xpack.securitySolution.addRules.installRuleButton', + { + defaultMessage: 'Install rule', + } +); + +export const UPDATE_RULE_BUTTON = i18n.translate( + 'xpack.securitySolution.addRules.upgradeRuleButton', + { + defaultMessage: 'Update rule', + } +); diff --git a/x-pack/plugins/security_solution/public/management/links.ts b/x-pack/plugins/security_solution/public/management/links.ts index 745f639438352..3506b4e4d962a 100644 --- a/x-pack/plugins/security_solution/public/management/links.ts +++ b/x-pack/plugins/security_solution/public/management/links.ts @@ -22,6 +22,7 @@ import { MANAGE_PATH, POLICIES_PATH, RESPONSE_ACTIONS_HISTORY_PATH, + RULES_ADD_PATH, RULES_CREATE_PATH, RULES_PATH, SecurityPageName, @@ -29,6 +30,7 @@ import { TRUSTED_APPS_PATH, } from '../../common/constants'; import { + ADD_RULES, BLOCKLIST, CREATE_NEW_RULE, ENDPOINTS, @@ -115,6 +117,13 @@ export const links: LinkItem = { }), ], links: [ + { + id: SecurityPageName.rulesAdd, + title: ADD_RULES, + path: RULES_ADD_PATH, + skipUrlState: true, + hideTimeline: true, + }, { id: SecurityPageName.rulesCreate, title: CREATE_NEW_RULE, diff --git a/x-pack/plugins/security_solution/public/rules/routes.tsx b/x-pack/plugins/security_solution/public/rules/routes.tsx index 9f8192fa7866c..b7fc674499b20 100644 --- a/x-pack/plugins/security_solution/public/rules/routes.tsx +++ b/x-pack/plugins/security_solution/public/rules/routes.tsx @@ -23,6 +23,7 @@ import { useReadonlyHeader } from '../use_readonly_header'; import { PluginTemplateWrapper } from '../common/components/plugin_template_wrapper'; import { SpyRoute } from '../common/utils/route/spy_routes'; import { AllRulesTabs } from '../detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar'; +import { AddRulesPage } from '../detection_engine/rule_management_ui/pages/add_rules'; const RulesSubRoutes = [ { @@ -41,10 +42,15 @@ const RulesSubRoutes = [ exact: true, }, { - path: `/rules/:tabName(${AllRulesTabs.management}|${AllRulesTabs.monitoring})`, + path: `/rules/:tabName(${AllRulesTabs.management}|${AllRulesTabs.monitoring}|${AllRulesTabs.updates})`, main: RulesPage, exact: true, }, + { + path: '/rules/add_rules', + main: AddRulesPage, + exact: true, + }, ]; const RulesContainerComponent: React.FC = () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/register_routes.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/register_routes.ts index 35fd65338a1e4..51fc32fca542f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/register_routes.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/register_routes.ts @@ -5,7 +5,6 @@ * 2.0. */ -import type { ConfigType } from '../../../../config'; import type { SetupPlugins } from '../../../../plugin_contract'; import type { SecuritySolutionPluginRouter } from '../../../../types'; @@ -20,24 +19,19 @@ import { performRuleUpgradeRoute } from './perform_rule_upgrade/perform_rule_upg export const registerPrebuiltRulesRoutes = ( router: SecuritySolutionPluginRouter, - config: ConfigType, security: SetupPlugins['security'] ) => { - const { prebuiltRulesNewUpgradeAndInstallationWorkflowsEnabled } = config.experimentalFeatures; - // Legacy endpoints that we're going to deprecate getPrebuiltRulesAndTimelinesStatusRoute(router, security); installPrebuiltRulesAndTimelinesRoute(router); - if (prebuiltRulesNewUpgradeAndInstallationWorkflowsEnabled) { - // New endpoints for the rule upgrade and installation workflows - getPrebuiltRulesStatusRoute(router); - performRuleInstallationRoute(router); - performRuleUpgradeRoute(router); - reviewRuleInstallationRoute(router); - reviewRuleUpgradeRoute(router); + // New endpoints for the rule upgrade and installation workflows + getPrebuiltRulesStatusRoute(router); + performRuleInstallationRoute(router); + performRuleUpgradeRoute(router); + reviewRuleInstallationRoute(router); + reviewRuleUpgradeRoute(router); - // Helper endpoints for development and testing. Should be removed later. - generateAssetsRoute(router); - } + // Helper endpoints for development and testing. Should be removed later. + generateAssetsRoute(router); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts index 844e0a4c188c8..65d912cb1ac46 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts @@ -92,6 +92,7 @@ const calculateRuleInfos = (results: CalculateRuleDiffResult[]): RuleUpgradeInfo return { id: installedCurrentVersion.id, rule_id: installedCurrentVersion.rule_id, + revision: installedCurrentVersion.revision, rule: diffableCurrentVersion, diff: { fields: pickBy>( diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index dd1a4aa843767..38dea982d5a9e 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -92,7 +92,7 @@ export const initRoutes = ( ) => { registerFleetIntegrationsRoutes(router, logger); registerLegacyRuleActionsRoutes(router, logger); - registerPrebuiltRulesRoutes(router, config, security); + registerPrebuiltRulesRoutes(router, security); registerRuleExceptionsRoutes(router); registerManageExceptionsRoutes(router); registerRuleManagementRoutes(router, config, ml, logger); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 65e1cae7b8ec6..e302758ce1c05 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -29109,11 +29109,7 @@ "xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedTimelinesButton": "Installer la/les {missingTimelines, plural, =1 {chronologie} one {chronologies} many {chronologies} other {chronologies}} Elastic prédéfinie(s) {missingTimelines} ", "xpack.securitySolution.detectionEngine.rules.update.successfullySavedRuleTitle": "{ruleName} a été enregistré", "xpack.securitySolution.detectionEngine.rules.updatePrePackagedRulesAndTimelinesButton": "Mettre à jour la/les {updateRules, plural, =1 {règle} one {règles} many {règles} other {règles}} Elastic prédéfinie(s) {updateRules} et la/les {updateTimelines, plural, =1 {chronologie} one {chronologies} many {chronologies} other {chronologies}} Elastic prédéfinie(s) {updateTimelines}", - "xpack.securitySolution.detectionEngine.rules.updatePrePackagedRulesAndTimelinesMsg": "Vous pouvez mettre à jour la/les {updateRules, plural, =1 {règle} one {règles} many {règles} other {règles}} Elastic prédéfinie(s) {updateRules} et la/les {updateTimelines, plural, =1 {chronologie} one {chronologies} many {chronologies} other {chronologies}} Elastic prédéfinie(s) {updateTimelines} Notez que cela rechargera les règles prédéfinies d'Elastic supprimées.", - "xpack.securitySolution.detectionEngine.rules.updatePrePackagedRulesButton": "Mettre à jour la/les {updateRules, plural, =1 {règle} one {règles} many {règles} other {règles}} Elastic prédéfinie(s) {updateRules}", - "xpack.securitySolution.detectionEngine.rules.updatePrePackagedRulesMsg": "Vous pouvez mettre à jour la/les {updateRules, plural, =1 {règle} one {règles} many {règles} other {règles}} Elastic prédéfinies {updateRules}", "xpack.securitySolution.detectionEngine.rules.updatePrePackagedTimelinesButton": "Mettre à jour la/les {updateTimelines, plural, =1 {chronologie} one {chronologies} many {chronologies} other {chronologies}} Elastic prédéfinies {updateTimelines}", - "xpack.securitySolution.detectionEngine.rules.updatePrePackagedTimelinesMsg": "Vous pouvez mettre à jour la/les {updateTimelines, plural, =1 {chronologie} one {chronologies} many {chronologies} other {chronologies}} Elastic prédéfinies {updateTimelines}", "xpack.securitySolution.detectionEngine.signals.alertReasonDescription": "{eventCategory, select, null {} other {{eventCategory}{whitespace}}}événement{hasFieldOfInterest, select, false {} other {{whitespace}avec}}{processName, select, null {} other {{whitespace}processus {processName},}}{processParentName, select, null {} other {{whitespace}processus parent {processParentName},}}{fileName, select, null {} other {{whitespace}fichier {fileName},}}{sourceAddress, select, null {} other {{whitespace}source {sourceAddress}}}{sourcePort, select, null {} other {: {sourcePort},}}{destinationAddress, select, null {} other {{whitespace}destination {destinationAddress}}}{destinationPort, select, null {} other {: {destinationPort},}}{userName, select, null {} other {{whitespace}par {userName}}}{hostName, select, null {} other {{whitespace}le {hostName}}} a créé l'alerte {alertName} {alertSeverity}.", "xpack.securitySolution.detectionEngine.stepDefineRule.dataViewNotFoundDescription": "Votre vue de données avec l'ID \"{dataView}\" est introuvable. Il est possible qu'elle ait été supprimée.", "xpack.securitySolution.detectionResponse.alertsByStatus.totalAlerts": "Total : {totalAlerts, plural, =1 {alerte} one {alertes} many {alertes} other {alertes}}", @@ -31391,7 +31387,6 @@ "xpack.securitySolution.detectionEngine.rules.tour.createRuleTourContent": "Les options de suppression d'alerte sont maintenant disponibles pour les règles de requête personnalisée, et plusieurs champs peuvent être sélectionnés dans les règles relatives aux nouveaux termes", "xpack.securitySolution.detectionEngine.rules.tour.createRuleTourTitle": "De nouvelles fonctionnalités de règle de sécurité sont disponibles", "xpack.securitySolution.detectionEngine.rules.updateButtonTitle": "Mettre à jour", - "xpack.securitySolution.detectionEngine.rules.updatePrePackagedRulesTitle": "Mise à jour disponible pour les règles ou les modèles de chronologie prédéfinis d'Elastic", "xpack.securitySolution.detectionEngine.rulesSnoozeBadge.error.unableToFetch": "Impossible de récupérer les paramètres de répétition", "xpack.securitySolution.detectionEngine.ruleStatus.errorCalloutTitle": "Échec de règle à", "xpack.securitySolution.detectionEngine.ruleStatus.partialErrorCalloutTitle": "Avertissement à", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 30ca83889d63f..f0f986628419e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -29090,11 +29090,7 @@ "xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedTimelinesButton": "{missingTimelines}個のElastic事前構築済み{missingTimelines, plural, =1 {タイムライン} other {タイムライン}}をインストール ", "xpack.securitySolution.detectionEngine.rules.update.successfullySavedRuleTitle": "{ruleName} が保存されました", "xpack.securitySolution.detectionEngine.rules.updatePrePackagedRulesAndTimelinesButton": "{updateRules}個のElastic事前構築済み{updateRules, plural, =1 {ルール} other {ルール}}と{updateTimelines}個のElastic事前構築済み{updateTimelines, plural, =1 {タイムライン} other {タイムライン}}を更新", - "xpack.securitySolution.detectionEngine.rules.updatePrePackagedRulesAndTimelinesMsg": "{updateRules}個のElastic事前構築済み{updateRules, plural, =1 {ルール} other {ルール}}と{updateTimelines}個のElastic事前構築済み{updateTimelines, plural, =1 {タイムライン} other {タイムライン}}を更新できます。これにより、削除されたElastic事前構築済みルールが再読み込みされます。", - "xpack.securitySolution.detectionEngine.rules.updatePrePackagedRulesButton": "{updateRules}個のElastic事前構築済み{updateRules, plural, =1 {ルール} other {ルール}}を更新", - "xpack.securitySolution.detectionEngine.rules.updatePrePackagedRulesMsg": "{updateRules}個のElastic事前構築済み{updateRules, plural, =1 {ルール} other {ルール}}を更新できます", "xpack.securitySolution.detectionEngine.rules.updatePrePackagedTimelinesButton": "{updateTimelines}個のElastic事前構築済み{updateTimelines, plural, =1 {タイムライン} other {タイムライン}}を更新", - "xpack.securitySolution.detectionEngine.rules.updatePrePackagedTimelinesMsg": "{updateTimelines}個のElastic事前構築済み{updateTimelines, plural, =1 {タイムライン} other {タイムライン}}を更新できます", "xpack.securitySolution.detectionEngine.signals.alertReasonDescription": "{eventCategory, select, null {} other {{eventCategory}{whitespace}}}イベント{hasFieldOfInterest, select, false {} other {{whitespace}の}}{processName, select, null {} other {{whitespace}プロセス{processName}、}}{processParentName, select, null {} other {{whitespace}親プロセス{processParentName}、}}{fileName, select, null {} other {{whitespace}ファイル{fileName}、}}{sourceAddress, select, null {} other {{whitespace}ソース{sourceAddress}}}{sourcePort, select, null {} other {:{sourcePort}、}}{destinationAddress, select, null {} other {{whitespace}{destinationAddress}のデスティネーション}}{destinationPort, select, null {} other {:{destinationPort}、}}{userName, select, null {} other {{whitespace}{userName}によって}}{hostName, select, null {} other {{whitespace}{hostName}で}}で{alertSeverity}アラート{alertName}が作成されました。", "xpack.securitySolution.detectionEngine.stepDefineRule.dataViewNotFoundDescription": "ID \"{dataView}\"のデータビューが見つかりませんでした。削除された可能性があります。", "xpack.securitySolution.detectionResponse.alertsByStatus.totalAlerts": "合計{totalAlerts, plural, =1 {アラート} other {アラート}}", @@ -31372,7 +31368,6 @@ "xpack.securitySolution.detectionEngine.rules.tour.createRuleTourContent": "カスタムクエリルールでアラート抑制オプションが利用可能になり、新規条件ルールで複数のフィールドを選択できるようになりました", "xpack.securitySolution.detectionEngine.rules.tour.createRuleTourTitle": "新しいセキュリティルール機能が利用可能です", "xpack.securitySolution.detectionEngine.rules.updateButtonTitle": "更新", - "xpack.securitySolution.detectionEngine.rules.updatePrePackagedRulesTitle": "Elastic事前構築済みルールまたはタイムラインテンプレートを更新", "xpack.securitySolution.detectionEngine.rulesSnoozeBadge.error.unableToFetch": "スヌーズ設定を取得できません", "xpack.securitySolution.detectionEngine.ruleStatus.errorCalloutTitle": "ルール失敗", "xpack.securitySolution.detectionEngine.ruleStatus.partialErrorCalloutTitle": "警告", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 212f1bed8ff47..0f00f726dfd25 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -29086,11 +29086,7 @@ "xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedTimelinesButton": "安装 {missingTimelines} 个 Elastic 预构建{missingTimelines, plural, =1 {时间线} other {时间线}} ", "xpack.securitySolution.detectionEngine.rules.update.successfullySavedRuleTitle": "{ruleName} 已保存", "xpack.securitySolution.detectionEngine.rules.updatePrePackagedRulesAndTimelinesButton": "更新 {updateRules} 个 Elastic 预构建{updateRules, plural, =1 {规则} other {规则}}和 {updateTimelines} 个 Elastic 预构建{updateTimelines, plural, =1 {时间线} other {时间线}}", - "xpack.securitySolution.detectionEngine.rules.updatePrePackagedRulesAndTimelinesMsg": "您可以更新 {updateRules} 个 Elastic 预构建{updateRules, plural, =1 {规则} other {规则}}和 {updateTimelines} 个 Elastic 预构建{updateTimelines, plural, =1 {时间线} other {时间线}}。注意,这将重新加载删除的 Elastic 预构建规则。", - "xpack.securitySolution.detectionEngine.rules.updatePrePackagedRulesButton": "更新 {updateRules} 个 Elastic 预构建{updateRules, plural, =1 {规则} other {规则}}", - "xpack.securitySolution.detectionEngine.rules.updatePrePackagedRulesMsg": "您可以更新 {updateRules} 个 Elastic 预构建{updateRules, plural, =1 {规则} other {规则}}", "xpack.securitySolution.detectionEngine.rules.updatePrePackagedTimelinesButton": "更新 {updateTimelines} 个 Elastic 预构建{updateTimelines, plural, =1 {时间线} other {时间线}}", - "xpack.securitySolution.detectionEngine.rules.updatePrePackagedTimelinesMsg": "您可以更新 {updateTimelines} 个 Elastic 预构建{updateTimelines, plural, =1 {时间线} other {时间线}}", "xpack.securitySolution.detectionEngine.signals.alertReasonDescription": "{eventCategory, select, null {} other {{eventCategory}{whitespace}}}事件{hasFieldOfInterest, select, false {} other {{whitespace}具有}}{processName, select, null {} other {{whitespace}进程 {processName},}}{processParentName, select, null {} other {{whitespace}父进程 {processParentName},}}{fileName, select, null {} other {{whitespace}文件 {fileName},}}{sourceAddress, select, null {} other {{whitespace}源 {sourceAddress}}}{sourcePort, select, null {} other {:{sourcePort},}}{destinationAddress, select, null {} other {{whitespace}目标 {destinationAddress}}}{destinationPort, select, null {} other {:{destinationPort},}}{userName, select, null {} other {{whitespace}由 {userName}}}{hostName, select, null {} other {{whitespace}于{hostName}}} 创建了 {alertSeverity} 告警 {alertName}。", "xpack.securitySolution.detectionEngine.stepDefineRule.dataViewNotFoundDescription": "未找到“id”为“{dataView}”的数据视图。可能是因为它已被删除。", "xpack.securitySolution.detectionResponse.alertsByStatus.totalAlerts": "{totalAlerts, plural, =1 {告警} other {告警}}总计", @@ -31368,7 +31364,6 @@ "xpack.securitySolution.detectionEngine.rules.tour.createRuleTourContent": "告警阻止选项现在可用于定制查询规则,并且可以在新字词规则中选择多个字段", "xpack.securitySolution.detectionEngine.rules.tour.createRuleTourTitle": "有新的安全规则功能可用", "xpack.securitySolution.detectionEngine.rules.updateButtonTitle": "更新", - "xpack.securitySolution.detectionEngine.rules.updatePrePackagedRulesTitle": "更新可用于 Elastic 预构建规则或时间线模板", "xpack.securitySolution.detectionEngine.rulesSnoozeBadge.error.unableToFetch": "无法提取暂停设置", "xpack.securitySolution.detectionEngine.ruleStatus.errorCalloutTitle": "规则错误位置", "xpack.securitySolution.detectionEngine.ruleStatus.partialErrorCalloutTitle": "警告于",