[Security Solution] New Rules Installation and Upgrade UI Workflows (#158450)

Addresses: https://github.com/elastic/kibana/issues/154614
https://github.com/elastic/kibana/issues/154615

Figma designs:
https://www.figma.com/file/gLHm8LpTtSkAUQHrkG3RHU/%5B8.7%5D-%5BRules%5D-Rule-Immutability%2FCustomization?type=design&node-id=2935-577576&t=ziqgnlEJBpowqa7F-0

## Summary

- Removes `prebuiltRulesNewUpgradeAndInstallationWorkflowsEnabled`
feature flag. All new prebuilt endpoints now available by default.
- Creates the UI for the new **rules installation** and **rules
upgrade** workflows.
- Creates new **Add Rules** page, which lists rules available for
installation.
- Creates new **Rule Updates** page, which lists rules which have
available updates.
- Creates new, separate contexts for the **Add Rules** and the **Rule
Updates** page, and the hooks to use them
(`useAddPrebuiltRulesTableContext` and
`useUpgradePrebuiltRulesTableContext` respectively)
    - Creates prebuilt rule hooks, which consume new endpoints:
- `useFetchPrebuiltRulesStatusQuery` and `usePrebuiltRulesStatus`
consume the `/internal/detection_engine/prebuilt_rules/status` endpoint
and provide information about number of rules available for
installation, number of installed rules, and number of rules with
available updates.
- `useFetchPrebuiltRulesInstallReviewQuery` and
`usePrebuiltRulesInstallReview` consume the
`/internal/detection_engine/prebuilt_rules/installation/_review`
endpoint and return the rules available for installation which are
listed in the **Add Rules** page.
- `useFetchPrebuiltRulesUpgradeReviewQuery` and
`usePrebuiltRulesUpgradeReview` consume the
`/internal/detection_engine/prebuilt_rules/upgrade/_review` endpoint and
return the rules which have available updates, and are listed in the
**Rule Updates** page.
- `usePerformInstallAllRules`, `usePerformInstallSpecificRules`, and its
respective mutation hooks `usePerformAllRulesInstallMutation` and
`usePerformSpecificRulesInstallMutation` consume the
`/internal/detection_engine/prebuilt_rules/upgrade/_perform` endpoint in
order to install rules.
- `usePerformUpgradeAllRules`, `usePerformUpgradeSpecificRules` and its
respective mutation hooks `usePerformAllRulesUpgradeMutation` and
`usePerformSpecificRulesUpgradeMutation` consume the
`/internal/detection_engine/prebuilt_rules/upgrade/_perform` endpoint in
order to upgrade rules.

### Deprecated code

**Hooks:**
- `useCreatePrebuiltRulesMutation`
- `useInstallPrePackagedRules`
- `useCreatePrePackagedRules`
- `usePrePackagedRulesInstallationStatus`
- `usePrePackagedTimelinesInstallationStatus`

### Major points to resolve

- **Timeline templates installation**: Since this PR stops using the
`/api/detection_engine/rules/prepackaged` endpoint in favour of the new
ones, we are not currently installing timeline templates. Serverside, we
will need a new endpoint to install them separately from rules? In the
UI, how would this still work: would they get installed in the
background now? Or maybe have a new button for it somewhere?
- **ML Jobs warning**: when updating rules, we currently have a wrapper
to add confirmation modal for users who may be running older ML Jobs
that would be overridden by updating their rules. This PR removes that
code, but we'll need to reintroduce it for the cases of: upgrading
single rules, upgrading a selection of rules, upgrading all rules.


### Deviations from design

This PR includes a reduced scope to the final workflow shown in the
Figma designs.

Most notably, in Milestone 2, to be released in 8.9, we did not build
the flyout that, in the Add Rules page, shows the rule details when the
user clicks on it, so the user can review it before installing. The same
is true in the Rule Updates table, which does not allow, for now,
reviewing the rules. In both cases, the user can only click in "Install
Rule" and "Upgrade Rule".

There are other differences in the UI, for technical reasons:
- Both for the Add Rules page and the Rule Updates table we decided to
use **EUI's InMemoryTable**. Since the endpoint that return the data to
populate both of these tables do not allow for sorting, filtering and
paging, we decided to use the InMemoryTable for both cases, as all of
those functions are handled out-of-the-box by the EUI component. The
relatively low number of items that populate these tables means that we
won't face significant performance issues. However, this meant a number
of deviations from the designs:
- Since filtering, sorting and pagination are handled by the table, the
contexts for these table do not includes any internal state relating to
these functions. This makes it hard to recreate the RuleUtilityBar for
each of these components or make the existing one reusable. We therefore
decided to leave the Utility Bar for the new two tables out of scope,
and deviate from the design by moving the button that the user can click
on o install or upgrade the selected rules to beside the "Install all"
or "Upgrade all" buttons. This button is shown only when at least one
rule of the table is selected.
- The **tags filter box** that comes out-of-the-box with the
InMemoryTable can only be positioned to the right of the search bar,
instead of the left like we have in our main **Installed Rules** table.
Also, clicking on the tabs adds the text to the search bar, and the box
does not allow for negative selection of tabs (exclusion).
- The search bar filters on keystroke rather than on Enter. This
behaviour can be changed, but it feels more useful than the other
behaviour for these new two tables.
- The search bar filters by searching the user's input in any of the
string properties of first order within the rule object. This means that
the search bar can be used to look up rules according to their name,
description, rule_id, etc (but not for example for MITRE techniques,
which are an object.) This behaviour, however, is also customisable.
- Neither the Add Rules table nor the Rule Updates tables display the
_Last updated_ column which is shown in the design. Since the original
intent of the designers is to show when the rule asset (`security-rule`)
was created or updated, this is information we don't currently have
within the SO. After discussion with @ksevasilyeva and @ARWNightingale,
we decided, for now, to remove the column. In the meantime,
@terrancedejesus [created an issue to include `createdAt` and
`updatedAt`
fields](https://github.com/elastic/detection-rules/issues/2826) within
the rule assets, that we can use to display in the table in later
iterations.

#### Other remaining work:

- Introduce confirmation modals when the user clicks on the "Install
all" or "Upgrade all" modal.
- Unit testing for new hooks and components.
- Other component redesign: Rule Filter, Tag Filter 

#### How to test rule upgrade

1. Have at least one rule installed
2. Find its `rule_id` from the Network tab.
3. Make a request to `PATCH /api/detection_engine/rules` with the
`rule_id` in the payload, and also set the `version` to a number lower
than the current version.
4. Reload the page.
5. The `/upgrade/_review` endpoint will now return that rule as
available for upgrade.

### Videos

#### Rule Installation Workflow



5a219625-beb1-48ee-a9fc-ff48b69eeae0

#### Rule Upgrade Workflow



b5f3c23b-004a-462c-bbdd-ed04321f5ce7

### TODO

- [x] Align copy, use "update" instead of "upgrade"
- [ ] Persist user's choice when they dismiss the upgrade/install rules
callouts till the next package release (create a separate task for that)
- [ ] Unify table controls (search bar and tags), use the ones we have
on the rules management table
- [ ] After rule installation, adjust copy, and display that all
available rules have been installed. Add a "Go Back" CTA
- [ ] Add links from the available rules table to docs
- [ ] Rule severity sorting should take semantics into consideration

---------

Co-authored-by: Dmitrii <dmitrii.shevchenko@elastic.co>
Co-authored-by: Dmitrii Shevchenko <dmshevch@gmail.com>
Co-authored-by: Sergi Massaneda <sergi.massaneda@gmail.com>
This commit is contained in:
Juan Pablo Djeredjian 2023-06-14 10:01:39 +02:00 committed by GitHub
parent 56e9cebe21
commit 17b5fd39b4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
95 changed files with 2828 additions and 1606 deletions

View file

@ -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;

View file

@ -14,6 +14,8 @@ export const RuleVersionSpecifier = t.exact(
);
export type RuleVersionSpecifier = t.TypeOf<typeof RuleVersionSpecifier>;
export type InstallSpecificRulesRequest = t.TypeOf<typeof InstallSpecificRulesRequest>;
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<typeof InstallAllRulesRequest>;
export const InstallAllRulesRequest = t.exact(
t.type({
mode: t.literal(`ALL_RULES`),

View file

@ -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<typeof RuleUpgradeSpecifier>;
export type UpgradeSpecificRulesRequest = t.TypeOf<typeof UpgradeSpecificRulesRequest>;
export const UpgradeSpecificRulesRequest = t.exact(
t.intersection([
t.type({

View file

@ -30,4 +30,5 @@ export interface RuleUpgradeInfoForReview {
rule_id: RuleSignatureId;
rule: DiffableRule;
diff: PartialRuleDiff;
revision: number;
}

View file

@ -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
*/

View file

@ -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();

View file

@ -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);

View file

@ -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();

View file

@ -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();
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');
// Check that the rules table contains all rules
cy.get(ELASTIC_RULES_BTN).should(
'have.text',
`Elastic rules (${expectedNumberOfRulesAfterRecovering})`

View file

@ -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();

View file

@ -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"]';

View file

@ -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

View file

@ -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();
};

View file

@ -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',
});

View file

@ -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,
},
];

View file

@ -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);
});
});

View file

@ -1,138 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`JobsTableComponent renders correctly against snapshot 1`] = `
<EuiBasicTable
columns={
Array [
Object {
"name": "Job name",
"render": [Function],
},
Object {
"name": "Groups",
"render": [Function],
"width": "80px",
},
Object {
"align": "center",
"name": "Run job",
"render": [Function],
"width": "80px",
},
]
}
data-test-subj="jobs-table"
items={
Array [
Object {
"awaitingNodeAssignment": false,
"bucketSpanSeconds": 900,
"customSettings": Object {
"security_app_display_name": "Unusual Network Activity",
},
"datafeedId": "datafeed-linux_anomalous_network_activity_ecs",
"datafeedIndices": Array [
"auditbeat-*",
],
"datafeedState": "stopped",
"defaultIndexPattern": "auditbeat-*",
"description": "SIEM Auditbeat: Looks for unusual processes using the network which could indicate command-and-control, lateral movement, persistence, or data exfiltration activity (beta)",
"earliestTimestampMs": 1569812391387,
"groups": Array [
"auditbeat",
"process",
"siem",
],
"hasDatafeed": true,
"id": "linux_anomalous_network_activity_ecs",
"isCompatible": true,
"isElasticJob": true,
"isInstalled": true,
"isSingleMetricViewerJob": true,
"jobState": "closed",
"jobTags": Object {},
"latestResultsTimestampMs": 1571022900000,
"latestTimestampMs": 1571022859393,
"memory_status": "ok",
"moduleId": "security_linux_v3",
"processed_record_count": 32010,
},
Object {
"awaitingNodeAssignment": false,
"bucketSpanSeconds": 900,
"customSettings": Object {
"security_app_display_name": "Unusually Linux Processes",
},
"datafeedId": "datafeed-rare_process_by_host_linux_ecs",
"datafeedIndices": Array [
"auditbeat-*",
],
"datafeedState": "stopped",
"defaultIndexPattern": "auditbeat-*",
"description": "SIEM Auditbeat: Detect unusually rare processes on Linux (beta)",
"groups": Array [
"auditbeat",
"process",
"siem",
],
"hasDatafeed": true,
"id": "rare_process_by_host_linux_ecs",
"isCompatible": true,
"isElasticJob": true,
"isInstalled": true,
"isSingleMetricViewerJob": true,
"jobState": "closed",
"jobTags": Object {},
"memory_status": "ok",
"moduleId": "security_linux_v3",
"processed_record_count": 0,
},
Object {
"awaitingNodeAssignment": false,
"bucketSpanSeconds": 900,
"customSettings": Object {
"security_app_display_name": "Unusually Windows Processes",
},
"datafeedId": "",
"datafeedIndices": Array [],
"datafeedState": "",
"defaultIndexPattern": "winlogbeat-*",
"description": "SIEM Winlogbeat: Detect unusually rare processes on Windows (beta)",
"groups": Array [
"process",
"siem",
"winlogbeat",
],
"hasDatafeed": false,
"id": "rare_process_by_host_windows_ecs",
"isCompatible": false,
"isElasticJob": true,
"isInstalled": false,
"isSingleMetricViewerJob": false,
"jobState": "closed",
"jobTags": Object {},
"memory_status": "",
"moduleId": "security_windows_v3",
"processed_record_count": 0,
},
]
}
loading={true}
noItemsMessage={
<Memo(NoItemsMessage)
basePath="/test/base/path"
/>
}
onChange={[Function]}
pagination={
Object {
"pageIndex": 0,
"pageSize": 5,
"showPerPageOptions": false,
"totalItemCount": 3,
}
}
responsive={false}
tableLayout="fixed"
/>
`;

View file

@ -1,148 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`JobsTableFilters renders correctly against snapshot 1`] = `
<EuiFlexGroup
gutterSize="m"
justifyContent="flexEnd"
>
<EuiFlexItem
grow={true}
>
<EuiSearchBar
box={
Object {
"incremental": true,
"placeholder": "e.g. Unusual Linux Process",
}
}
data-test-subj="jobs-filter-bar"
onChange={[Function]}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiFilterGroup>
<GroupsFilterPopover
onSelectedGroupsChanged={[Function]}
securityJobs={
Array [
Object {
"awaitingNodeAssignment": false,
"bucketSpanSeconds": 900,
"customSettings": Object {
"security_app_display_name": "Unusual Network Activity",
},
"datafeedId": "datafeed-linux_anomalous_network_activity_ecs",
"datafeedIndices": Array [
"auditbeat-*",
],
"datafeedState": "stopped",
"defaultIndexPattern": "auditbeat-*",
"description": "SIEM Auditbeat: Looks for unusual processes using the network which could indicate command-and-control, lateral movement, persistence, or data exfiltration activity (beta)",
"earliestTimestampMs": 1569812391387,
"groups": Array [
"auditbeat",
"process",
"siem",
],
"hasDatafeed": true,
"id": "linux_anomalous_network_activity_ecs",
"isCompatible": true,
"isElasticJob": true,
"isInstalled": true,
"isSingleMetricViewerJob": true,
"jobState": "closed",
"jobTags": Object {},
"latestResultsTimestampMs": 1571022900000,
"latestTimestampMs": 1571022859393,
"memory_status": "ok",
"moduleId": "security_linux_v3",
"processed_record_count": 32010,
},
Object {
"awaitingNodeAssignment": false,
"bucketSpanSeconds": 900,
"customSettings": Object {
"security_app_display_name": "Unusually Linux Processes",
},
"datafeedId": "datafeed-rare_process_by_host_linux_ecs",
"datafeedIndices": Array [
"auditbeat-*",
],
"datafeedState": "stopped",
"defaultIndexPattern": "auditbeat-*",
"description": "SIEM Auditbeat: Detect unusually rare processes on Linux (beta)",
"groups": Array [
"auditbeat",
"process",
"siem",
],
"hasDatafeed": true,
"id": "rare_process_by_host_linux_ecs",
"isCompatible": true,
"isElasticJob": true,
"isInstalled": true,
"isSingleMetricViewerJob": true,
"jobState": "closed",
"jobTags": Object {},
"memory_status": "ok",
"moduleId": "security_linux_v3",
"processed_record_count": 0,
},
Object {
"awaitingNodeAssignment": false,
"bucketSpanSeconds": 900,
"customSettings": Object {
"security_app_display_name": "Unusually Windows Processes",
},
"datafeedId": "",
"datafeedIndices": Array [],
"datafeedState": "",
"defaultIndexPattern": "winlogbeat-*",
"description": "SIEM Winlogbeat: Detect unusually rare processes on Windows (beta)",
"groups": Array [
"process",
"siem",
"winlogbeat",
],
"hasDatafeed": false,
"id": "rare_process_by_host_windows_ecs",
"isCompatible": false,
"isElasticJob": true,
"isInstalled": false,
"isSingleMetricViewerJob": false,
"jobState": "closed",
"jobTags": Object {},
"memory_status": "",
"moduleId": "security_windows_v3",
"processed_record_count": 0,
},
]
}
/>
</EuiFilterGroup>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiFilterGroup>
<EuiFilterButton
data-test-subj="show-elastic-jobs-filter-button"
hasActiveFilters={false}
onClick={[Function]}
withNext={true}
>
Elastic jobs
</EuiFilterButton>
<EuiFilterButton
data-test-subj="show-custom-jobs-filter-button"
hasActiveFilters={false}
onClick={[Function]}
>
Custom jobs
</EuiFilterButton>
</EuiFilterGroup>
</EuiFlexItem>
</EuiFlexGroup>
`;

View file

@ -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(
<JobsTableFiltersComponent securityJobs={securityJobs} onFilterChanged={jest.fn()} />
);
expect(wrapper).toMatchSnapshot();
});
test('when you click Elastic Jobs filter, state is updated and it is selected', () => {
const onFilterChanged = jest.fn();
const wrapper = mount(

View file

@ -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(
<JobsTableComponent
isLoading={true}
jobs={securityJobs}
onJobStateChange={onJobStateChangeMock}
mlNodesAvailable={true}
/>
);
expect(wrapper).toMatchSnapshot();
});
test('should render the hyperlink which points specifically to the job id', async () => {
const href = await getRenderedHref(
() => (

View file

@ -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:

View file

@ -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 && <EuiBetaBadge label={betaOptions?.text ?? BETA} size="s" />}
append={isBeta && <EuiBadge color={'#E0E5EE'}>{betaOptions?.text ?? BETA}</EuiBadge>}
>
{name}
</EuiTab>

View file

@ -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}) {

View file

@ -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));
});
});

View file

@ -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;

View file

@ -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,

View file

@ -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<CreatePrepackagedRulesResponse> => {
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<GetPrebuiltRulesStatusResponseBody> =>
KibanaServices.get().http.fetch<GetPrebuiltRulesStatusResponseBody>(
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<ReviewRuleUpgradeResponseBody> =>
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<ReviewRuleInstallationResponseBody> =>
KibanaServices.get().http.fetch(REVIEW_RULE_INSTALLATION_URL, {
method: 'POST',
signal,
});
export const performInstallAllRules = async (): Promise<PerformRuleInstallationResponseBody> =>
KibanaServices.get().http.fetch(PERFORM_RULE_INSTALLATION_URL, {
method: 'POST',
body: JSON.stringify({
mode: 'ALL_RULES',
}),
});
export const performInstallSpecificRules = async (
rules: InstallSpecificRulesRequest['rules']
): Promise<PerformRuleInstallationResponseBody> =>
KibanaServices.get().http.fetch(PERFORM_RULE_INSTALLATION_URL, {
method: 'POST',
body: JSON.stringify({
mode: 'SPECIFIC_RULES',
rules,
}),
});
export const performUpgradeAllRules = async (): Promise<PerformRuleUpgradeResponseBody> =>
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<PerformRuleUpgradeResponseBody> =>
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
}),
});

View file

@ -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<ReviewRuleInstallationResponseBody>
) => {
return useQuery<ReviewRuleInstallationResponseBody>(
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]);
};

View file

@ -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<PrePackagedRulesStatusResponse>
options?: UseQueryOptions<PrebuiltRulesStatusStats>
) => {
return useQuery<PrePackagedRulesStatusResponse>(
return useQuery<PrebuiltRulesStatusStats>(
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
*/

View file

@ -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<ReviewRuleUpgradeResponseBody>
) => {
return useQuery<ReviewRuleUpgradeResponseBody>(
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]);
};

View file

@ -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<PerformRuleInstallationResponseBody, Error>
) => {
const invalidateFindRulesQuery = useInvalidateFindRulesQuery();
const invalidateFetchRulesSnoozeSettings = useInvalidateFetchRulesSnoozeSettingsQuery();
const invalidateFetchRuleManagementFilters = useInvalidateFetchRuleManagementFiltersQuery();
const invalidateFetchPrebuiltRulesInstallReview =
useInvalidateFetchPrebuiltRulesInstallReviewQuery();
const invalidateRuleStatus = useInvalidateFetchPrebuiltRulesStatusQuery();
return useMutation<PerformRuleInstallationResponseBody, Error>(() => performInstallAllRules(), {
...options,
mutationKey: PERFORM_ALL_RULES_INSTALLATION_KEY,
onSettled: (...args) => {
invalidateFindRulesQuery();
invalidateFetchRulesSnoozeSettings();
invalidateFetchRuleManagementFilters();
invalidateFetchPrebuiltRulesInstallReview();
invalidateRuleStatus();
if (options?.onSettled) {
options.onSettled(...args);
}
},
});
};

View file

@ -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<PerformRuleUpgradeResponseBody, Error>
) => {
const invalidateFindRulesQuery = useInvalidateFindRulesQuery();
const invalidateFetchRulesSnoozeSettings = useInvalidateFetchRulesSnoozeSettingsQuery();
const invalidateFetchRuleManagementFilters = useInvalidateFetchRuleManagementFiltersQuery();
const invalidateFetchPrebuiltRulesUpgradeReview =
useInvalidateFetchPrebuiltRulesUpgradeReviewQuery();
const invalidateRuleStatus = useInvalidateFetchPrebuiltRulesStatusQuery();
return useMutation<PerformRuleUpgradeResponseBody, Error>(() => performUpgradeAllRules(), {
...options,
mutationKey: PERFORM_ALL_RULES_UPGRADE_KEY,
onSettled: (...args) => {
invalidateFindRulesQuery();
invalidateFetchRulesSnoozeSettings();
invalidateFetchRuleManagementFilters();
invalidateFetchPrebuiltRulesUpgradeReview();
invalidateRuleStatus();
if (options?.onSettled) {
options.onSettled(...args);
}
},
});
};

View file

@ -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);
}
},
}
);
};

View file

@ -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<PerformRuleUpgradeResponseBody, Error, UpgradeSpecificRulesRequest['rules']>(
(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);
}
},
}
);
};

View file

@ -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();

View file

@ -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',

View file

@ -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<CreatePrepackagedRulesResponse>
) => {
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);
}
},
});
};

View file

@ -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',

View file

@ -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 },
});

View file

@ -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;
};

View file

@ -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;
};

View file

@ -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<ReviewRuleInstallationResponseBody>
) => {
const { addError } = useAppToasts();
return useFetchPrebuiltRulesInstallReviewQuery({
onError: (error) => addError(error, { title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE }),
...options,
});
};

View file

@ -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({

View file

@ -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<ReviewRuleUpgradeResponseBody>
) => {
const { addError } = useAppToasts();
return useFetchPrebuiltRulesUpgradeReviewQuery({
onError: (error) => addError(error, { title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE }),
...options,
});
};

View file

@ -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,
};
};

View file

@ -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;
}
};

View file

@ -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
);
};

View file

@ -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
);
};

View file

@ -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(
<TestProviders>
<AutoRefreshButton
isDisabled={false}
isRefreshOn={true}
reFetchRules={reFetchRulesMock}
setIsRefreshOn={setIsRefreshOnMock}
/>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="autoRefreshButton"]').at(0).text()).toEqual('On');
});
it.skip('invokes refetch when enabling auto refresh', () => {
const { container } = render(
<AutoRefreshButton
isDisabled={false}
isRefreshOn={false}
reFetchRules={reFetchRulesMock}
setIsRefreshOn={setIsRefreshOnMock}
/>
);
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);
});
});

View file

@ -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<React.SetStateAction<boolean>>;
}
/**
* 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) => (
<EuiContextMenuPanel
items={[
<EuiSwitch
key="allRulesAutoRefreshSwitch"
label={i18n.REFRESH_RULE_POPOVER_DESCRIPTION}
checked={isRefreshOn ?? false}
onChange={handleAutoRefreshSwitch(closePopover)}
compressed
disabled={isDisabled}
data-test-subj="refreshSettingsSwitch"
/>,
...(isDisabled
? [
<div key="refreshSettingsSelectionNote">
<EuiSpacer size="s" />
<EuiTextColor color="subdued" data-test-subj="refreshSettingsSelectionNote">
<FormattedMessage
id="xpack.securitySolution.detectionEngine.rules.refreshRulePopoverSelectionHelpText"
defaultMessage="Note: Refresh is disabled while there is an active selection."
/>
</EuiTextColor>
</div>,
]
: []),
]}
/>
),
[isRefreshOn, handleAutoRefreshSwitch, isDisabled]
);
return (
<EuiPopover
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(false)}
button={
<EuiButtonEmpty
data-test-subj="autoRefreshButton"
color={'text'}
iconType={'timeRefresh'}
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
disabled={isDisabled}
css={css`
margin-left: 10px;
`}
>
{isRefreshOn ? 'On' : 'Off'}
</EuiButtonEmpty>
}
>
{handleGetRefreshSettingsPopoverContent(() => setIsPopoverOpen(false))}
</EuiPopover>
);
};
AutoRefreshButtonComponent.displayName = 'AutoRefreshButtonComponent';
export const AutoRefreshButton = React.memo(AutoRefreshButtonComponent);
AutoRefreshButton.displayName = 'AutoRefreshButton';

View file

@ -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(<MiniCallout {...defaultProps} />);
expect(screen.getByText(defaultProps.title as string)).toBeInTheDocument();
});
it('renders the MiniCallout component with the provided iconType and color', () => {
const { container } = render(<MiniCallout {...defaultProps} />);
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(<MiniCallout {...{ ...defaultProps, iconType: undefined }} />);
const icon = container.querySelector('[data-euiicon-type]');
expect(icon).toBeNull();
});
it('renders the dismiss link when dismissible is true', () => {
render(<MiniCallout {...defaultProps} dismissible />);
expect(screen.getByText('Dismiss')).toBeInTheDocument();
});
it('does not render the dismiss link when dismissible is false', () => {
render(<MiniCallout {...defaultProps} dismissible={false} />);
expect(screen.queryByText('Dismiss')).not.toBeInTheDocument();
});
it('removes the MiniCallout component from the DOM when the dismiss link is clicked', () => {
render(<MiniCallout {...defaultProps} />);
fireEvent.click(screen.getByText('Dismiss'));
expect(screen.queryByText(defaultProps.title as string)).not.toBeInTheDocument();
});
});

View file

@ -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<MiniCalloutProps> = ({
color = 'primary',
dismissible = true,
iconType,
title,
}: MiniCalloutProps) => {
const { euiTheme } = useEuiTheme();
const [isDismissed, setIsDismissed] = useState(false);
if (isDismissed) {
return null;
}
const calloutTitle = (
<div
css={css`
width: 97%;
margin-left: ${euiTheme.size.s};
`}
>
<EuiFlexGroup
justifyContent="spaceBetween"
css={css`
display: flex;
`}
>
<EuiFlexItem>
<EuiFlexGroup gutterSize="none">
<EuiTextColor
color="default"
css={css`
font-weight: ${euiTheme.font.weight.regular};
`}
>
{title}
</EuiTextColor>
</EuiFlexGroup>
</EuiFlexItem>
{dismissible && (
<EuiFlexItem grow={false}>
<EuiLink
css={css`
font-weight: ${euiTheme.font.weight.regular};
`}
onClick={() => setIsDismissed(true)}
>
{i18n.DISMISS}
</EuiLink>
</EuiFlexItem>
)}
</EuiFlexGroup>
</div>
);
return (
<EuiCallOut size="s" color={color} data-test-subj="mini-callout">
<div style={{ display: 'flex' }}>
{iconType && <EuiIcon type={iconType} color={color} />}
{calloutTitle}
</div>
</EuiCallOut>
);
};
export const MiniCallout = React.memo(MiniCalloutComponent);
MiniCallout.displayName = 'MiniCallout';

View file

@ -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) => (
<FormattedMessage
id="xpack.securitySolution.detectionEngine.rules.updatePrebuiltRulesCalloutTitle"
defaultMessage="Updates available for installed rules. Review and update in&nbsp;{link}."
values={{
link: (
<EuiLink
className={css`
font-weight: 400;
`}
onClick={onClick}
>
{RULE_UPDATES_LINK}
</EuiLink>
),
}}
/>
);

View file

@ -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<MlRuleWarningPopoverComponentProps> = ({
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 = (
<EuiButtonIcon
display={'empty'}
color={'warning'}
iconType={'warning'}
onClick={togglePopover}
/>
);
const popoverTitle = getCapitalizedStatusText(RuleExecutionStatus['partial failure']);
return (
<EuiPopover
button={button}
isOpen={isPopoverOpen}
closePopover={closePopover}
anchorPosition="leftCenter"
>
<EuiPopoverTitle>{popoverTitle}</EuiPopoverTitle>
<div style={{ width: POPOVER_WIDTH }}>
<EuiText size="s">
<p>{i18n.ML_RULE_JOBS_WARNING_DESCRIPTION}</p>
</EuiText>
</div>
<EuiSpacer size="s" />
{notRunningJobs.map((job) => (
<EuiText key={job.id}>{job.customSettings?.security_app_display_name ?? job.id}</EuiText>
))}
<EuiPopoverFooter>
<SecuritySolutionLinkButton
data-test-subj="open-rule-details"
fullWidth
deepLinkId={SecurityPageName.rules}
path={getRuleDetailsTabUrl(rule.id, RuleDetailTabs.alerts)}
>
{i18n.ML_RULE_JOBS_WARNING_BUTTON_LABEL}
</SecuritySolutionLinkButton>
</EuiPopoverFooter>
</EuiPopover>
);
};
export const MlRuleWarningPopover = React.memo(MlRuleWarningPopoverComponent);
MlRuleWarningPopover.displayName = 'MlRuleWarningPopover';

View file

@ -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 (
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false} wrap={true}>
{shouldDisplayInstallSelectedRulesButton ? (
<EuiFlexItem grow={false}>
<EuiButton onClick={installSelectedRules} disabled={isRuleInstalling}>
{i18n.INSTALL_SELECTED_RULES(numberOfSelectedRules)}
{isRuleInstalling ? <EuiLoadingSpinner size="s" /> : undefined}
</EuiButton>
</EuiFlexItem>
) : null}
<EuiFlexItem grow={false}>
<EuiButton
fill
iconType="plusInCircle"
data-test-subj="installAllRulesButton"
onClick={installAllRules}
disabled={!isRulesAvailableForInstall || isRuleInstalling}
>
{i18n.INSTALL_ALL}
{isRuleInstalling ? <EuiLoadingSpinner size="s" /> : undefined}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -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 = (
<EuiEmptyPrompt
title={<h3>{i18n.NO_RULES_AVAILABLE_FOR_INSTALL}</h3>}
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 && (
<EuiProgress
data-test-subj="loadingRulesInfoProgress"
size="xs"
position="absolute"
color="accent"
/>
)}
<EuiSkeletonLoading
isLoading={isLoading || shouldShowLoadingOverlay}
loadingContent={
<>
<EuiSkeletonTitle />
<EuiSkeletonText />
</>
}
loadedContent={
isTableEmpty ? (
NO_ITEMS_MESSAGE
) : (
<EuiInMemoryTable
items={rules}
sorting
search={{
box: {
incremental: true,
isClearable: true,
},
filters: [
{
type: 'field_value_selection',
field: 'tags',
name: 'Tags',
multiSelect: true,
options: tags.map((tag) => ({
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';

View file

@ -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<AddPrebuiltRulesContextType | null>(null);
interface AddPrebuiltRulesTableContextProviderProps {
children: React.ReactNode;
}
export const AddPrebuiltRulesTableContextProvider = ({
children,
}: AddPrebuiltRulesTableContextProviderProps) => {
const [loadingRules, setLoadingRules] = useState<RuleSignatureId[]>([]);
const [selectedRules, setSelectedRules] = useState<RuleInstallationInfoForReview[]>([]);
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<AddPrebuiltRulesContextType>(() => {
return {
state: {
rules,
tags,
isFetched,
isLoading,
loadingRules,
isRefetching,
selectedRules,
lastUpdated: dataUpdatedAt,
},
actions,
};
}, [
rules,
tags,
isFetched,
isLoading,
loadingRules,
isRefetching,
selectedRules,
dataUpdatedAt,
actions,
]);
return (
<AddPrebuiltRulesTableContext.Provider value={providerValue}>
{children}
</AddPrebuiltRulesTableContext.Provider>
);
};
export const useAddPrebuiltRulesTableContext = (): AddPrebuiltRulesContextType => {
const rulesTableContext = useContext(AddPrebuiltRulesTableContext);
invariant(
rulesTableContext,
'useAddPrebuiltRulesTableContext should be used inside AddPrebuiltRulesTableContextProvider'
);
return rulesTableContext;
};

View file

@ -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 },
}
);
};

View file

@ -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<RuleInstallationInfoForReview>;
export const RULE_NAME_COLUMN: TableColumn = {
field: 'name',
name: i18n.COLUMN_RULE,
render: (value: RuleInstallationInfoForReview['name']) => (
<EuiText id={value} size="s">
{value}
</EuiText>
),
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) => (
<EuiBadge color="hollow" key={`${tag}-${i}`} data-test-subj="tag">
{tag}
</EuiBadge>
);
return (
<PopoverItems
items={tags}
popoverTitle={i18n.COLUMN_TAGS}
popoverButtonTitle={tags.length.toString()}
popoverButtonIcon="tag"
dataTestPrefix="tags"
renderItem={renderItem}
/>
);
},
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 <IntegrationsPopover relatedIntegrations={integrations} />;
},
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 (
<EuiButtonEmpty size="s" disabled={isRuleInstalling} onClick={() => installOneRule(ruleId)}>
{isRuleInstalling ? <EuiLoadingSpinner size="s" /> : i18n.INSTALL_RULE_BUTTON}
</EuiButtonEmpty>
);
},
width: '10%',
align: 'center',
});
export const useAddPrebuiltRulesTableColumns = (): TableColumn[] => {
const [{ canUserCRUD }] = useUserData();
const hasCRUDPermissions = hasUserCRUDPermission(canUserCRUD);
const [showRelatedIntegrations] = useUiSetting$<boolean>(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']) => (
<EuiText data-test-subj="riskScore" size="s">
{value}
</EuiText>
),
sortable: true,
truncateText: true,
width: '85px',
},
{
field: 'severity',
name: i18n.COLUMN_SEVERITY,
render: (value: Rule['severity']) => <SeverityBadge value={value} />,
sortable: true,
truncateText: true,
width: '12%',
},
...(hasCRUDPermissions ? [createInstallButtonColumn(installOneRule, loadingRules)] : []),
],
[hasCRUDPermissions, installOneRule, loadingRules, showRelatedIntegrations]
);
};

View file

@ -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';

View file

@ -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 (
<>
<RulesManagementTour />
<RulesTableToolbar />
<EuiSpacer />
<RulesTables selectedTab={tabName as AllRulesTabs} />
</>
);
if (tabName !== AllRulesTabs.updates) {
return (
<>
<RulesManagementTour />
<RulesTableToolbar />
<EuiSpacer />
<RulesTables selectedTab={tabName as AllRulesTabs} />
</>
);
} else {
return (
<>
<UpgradePrebuiltRulesTableContextProvider>
<RulesTableToolbar />
<EuiSpacer />
<UpgradePrebuiltRulesTable />
</UpgradePrebuiltRulesTableContextProvider>
</>
);
}
});
AllRules.displayName = 'AllRules';

View file

@ -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');

View file

@ -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 <RulesTableContext.Provider value={providerValue}>{children}</RulesTableContext.Provider>;
};

View file

@ -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 <TabNavigation navTabs={ruleTabs} />;

View file

@ -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<RulesTableProps>(({ 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<RulesTableProps>(({ 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<RulesTableProps>(({ 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<RulesTableProps>(({ selectedTab }) => {
<Loader data-test-subj="loadingPanelAllRulesTable" overlay size="xl" />
)}
{isTableEmpty && <PrePackagedRulesPrompt />}
{isLoading && (
<EuiSkeletonText data-test-subj="initialLoadingPanelAllRulesTable" lines={10} />
)}
{isDeleteConfirmationVisible && (
<EuiConfirmModal
title={i18n.DELETE_CONFIRMATION_TITLE}
@ -276,7 +284,7 @@ export const RulesTables = React.memo<RulesTableProps>(({ selectedTab }) => {
<BulkActionDuplicateExceptionsConfirmation
onCancel={cancelRuleDuplication}
onConfirm={confirmRuleDuplication}
rulesCount={numberOfSelectedRules > 0 ? numberOfSelectedRules : 1}
rulesCount={numberOfSelectedRules}
/>
)}
{isBulkEditFlyoutVisible && bulkEditActionType !== undefined && (
@ -299,12 +307,12 @@ export const RulesTables = React.memo<RulesTableProps>(({ selectedTab }) => {
<EuiBasicTable
itemId="id"
items={rules}
isSelectable={hasPermissions}
isSelectable={isTableSelectable}
noItemsMessage={NO_ITEMS_MESSAGE}
onChange={tableOnChangeCallback}
pagination={paginationMemo}
ref={tableRef}
selection={hasPermissions ? euiBasicTableSelectionProps : undefined}
selection={isTableSelectable ? euiBasicTableSelectionProps : undefined}
sorting={{
sort: {
// EuiBasicTable has incorrect `sort.field` types which accept only `keyof Item` and reject fields in dot notation

View file

@ -21,3 +21,31 @@ export const ML_RULE_JOBS_WARNING_BUTTON_LABEL = i18n.translate(
defaultMessage: 'Visit rule details page to investigate',
}
);
export const INSTALLED_RULES_TAB = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleManagementUi.rulesTable.allRules.tabs.rules',
{
defaultMessage: `Installed Rules`,
}
);
export const RULE_MONITORING_TAB = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleManagementUi.rulesTable.allRules.tabs.monitoring',
{
defaultMessage: 'Rule Monitoring',
}
);
export const RULE_UPDATES_TAB = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleManagementUi.rulesTable.allRules.tabs.updates',
{
defaultMessage: 'Rule Updates',
}
);
export const ADD_RULES_TAB = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleManagementUi.rulesTable.allRules.tabs.addRules',
{
defaultMessage: 'Add Rules',
}
);

View file

@ -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 UPDATE_ALL = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.upgradeAll',
{
defaultMessage: 'Update all',
}
);
export const UPDATE_SELECTED_RULES = (numberOfSelectedRules: number) => {
return i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.upgradeSelected',
{
defaultMessage: 'Update {numberOfSelectedRules} selected rule(s)',
values: { numberOfSelectedRules },
}
);
};

View file

@ -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 = (
<EuiEmptyPrompt
title={<h3>{i18n.NO_RULES_AVAILABLE_FOR_UPGRADE}</h3>}
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 && (
<EuiProgress
data-test-subj="loadingRulesInfoProgress"
size="xs"
position="absolute"
color="accent"
/>
)}
<EuiSkeletonLoading
isLoading={isLoading || shouldShowLoadingOverlay}
loadingContent={
<>
<EuiSkeletonTitle />
<EuiSkeletonText />
</>
}
loadedContent={
isTableEmpty ? (
NO_ITEMS_MESSAGE
) : (
<EuiInMemoryTable
items={rules}
sorting
search={{
box: {
incremental: true,
isClearable: true,
},
toolsRight: [<UpgradePrebuiltRulesTableButtons />],
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';

View file

@ -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 (
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false} wrap={true}>
{shouldDisplayUpgradeSelectedRulesButton ? (
<EuiFlexItem grow={false}>
<EuiButton onClick={upgradeSelectedRules} disabled={isRuleUpgrading}>
<>
{i18n.UPDATE_SELECTED_RULES(numberOfSelectedRules)}
{isRuleUpgrading ? <EuiLoadingSpinner size="s" /> : undefined}
</>
</EuiButton>
</EuiFlexItem>
) : null}
<EuiFlexItem grow={false}>
<EuiButton
fill
iconType="plusInCircle"
onClick={upgradeAllRules}
disabled={!isRulesAvailableForUpgrade || isRuleUpgrading}
>
{i18n.UPDATE_ALL}
{isRuleUpgrading ? <EuiLoadingSpinner size="s" /> : undefined}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -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<UpgradePrebuiltRulesContextType | null>(
null
);
interface UpgradePrebuiltRulesTableContextProviderProps {
children: React.ReactNode;
}
export const UpgradePrebuiltRulesTableContextProvider = ({
children,
}: UpgradePrebuiltRulesTableContextProviderProps) => {
const [loadingRules, setLoadingRules] = useState<RuleSignatureId[]>([]);
const [selectedRules, setSelectedRules] = useState<RuleUpgradeInfoForReview[]>([]);
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<UpgradePrebuiltRulesTableActions>(
() => ({
reFetchRules: refetch,
upgradeOneRule,
upgradeSelectedRules,
upgradeAllRules,
selectRules: setSelectedRules,
}),
[refetch, upgradeAllRules, upgradeOneRule, upgradeSelectedRules]
);
const providerValue = useMemo<UpgradePrebuiltRulesContextType>(() => {
return {
state: {
rules,
tags,
isFetched,
isLoading,
isRefetching,
selectedRules,
loadingRules,
lastUpdated: dataUpdatedAt,
},
actions,
};
}, [
rules,
tags,
isFetched,
isLoading,
isRefetching,
selectedRules,
loadingRules,
dataUpdatedAt,
actions,
]);
return (
<UpgradePrebuiltRulesTableContext.Provider value={providerValue}>
{children}
</UpgradePrebuiltRulesTableContext.Provider>
);
};
export const useUpgradePrebuiltRulesTableContext = (): UpgradePrebuiltRulesContextType => {
const rulesTableContext = useContext(UpgradePrebuiltRulesTableContext);
invariant(
rulesTableContext,
'useUpgradePrebuiltRulesTableContext should be used inside UpgradePrebuiltRulesTableContextProvider'
);
return rulesTableContext;
};

View file

@ -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<RuleUpgradeInfoForReview>;
const RULE_NAME_COLUMN: TableColumn = {
field: 'rule.name',
name: i18n.COLUMN_RULE,
render: (value: RuleUpgradeInfoForReview['rule']['name']) => (
<EuiText id={value} size="s">
{value}
</EuiText>
),
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) => (
<EuiBadge color="hollow" key={`${tag}-${i}`} data-test-subj="tag">
{tag}
</EuiBadge>
);
return (
<PopoverItems
items={tags}
popoverTitle={i18n.COLUMN_TAGS}
popoverButtonTitle={tags.length.toString()}
popoverButtonIcon="tag"
dataTestPrefix="tags"
renderItem={renderItem}
/>
);
},
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 <IntegrationsPopover relatedIntegrations={integrations} />;
},
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 (
<EuiButtonEmpty size="s" disabled={isRuleUpgrading} onClick={() => upgradeOneRule(ruleId)}>
{isRuleUpgrading ? <EuiLoadingSpinner size="s" /> : i18n.UPDATE_RULE_BUTTON}
</EuiButtonEmpty>
);
},
width: '10%',
align: 'center',
});
export const useUpgradePrebuiltRulesTableColumns = (): TableColumn[] => {
const [{ canUserCRUD }] = useUserData();
const hasCRUDPermissions = hasUserCRUDPermission(canUserCRUD);
const [showRelatedIntegrations] = useUiSetting$<boolean>(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']) => (
<EuiText data-test-subj="riskScore" size="s">
{value}
</EuiText>
),
sortable: true,
truncateText: true,
width: '85px',
},
{
field: 'rule.severity',
name: i18n.COLUMN_SEVERITY,
render: (value: Rule['severity']) => <SeverityBadge value={value} />,
sortable: true,
truncateText: true,
width: '12%',
},
...(hasCRUDPermissions ? [createUpgradeButtonColumn(upgradeOneRule, loadingRules)] : []),
],
[hasCRUDPermissions, loadingRules, showRelatedIntegrations, upgradeOneRule]
);
};

View file

@ -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<Rule> | EuiTableActionsColumnType<Rule>;
@ -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;
}

View file

@ -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(
<TestProviders>
<RulesTableUtilityBar
canBulkEdit
onGetBulkItemsPopoverContent={jest.fn()}
onToggleSelectAll={jest.fn()}
/>
</TestProviders>
);
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(
<TestProviders>
<RulesTableUtilityBar
canBulkEdit
onGetBulkItemsPopoverContent={jest.fn()}
onToggleSelectAll={jest.fn()}
/>
</TestProviders>
);
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 = {

View file

@ -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<RulesTableUtilityBarProps>(
[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) => (
<EuiContextMenuPanel
items={[
<EuiSwitch
key="allRulesAutoRefreshSwitch"
label={i18n.REFRESH_RULE_POPOVER_DESCRIPTION}
checked={isRefreshOn ?? false}
onChange={handleAutoRefreshSwitch(closePopover)}
compressed
disabled={isAnyRuleSelected}
data-test-subj="refreshSettingsSwitch"
/>,
...(isAnyRuleSelected
? [
<div key="refreshSettingsSelectionNote">
<EuiSpacer size="s" />
<EuiTextColor color="subdued" data-test-subj="refreshSettingsSelectionNote">
<FormattedMessage
id="xpack.securitySolution.detectionEngine.rules.refreshRulePopoverSelectionHelpText"
defaultMessage="Note: Refresh is disabled while there is an active selection."
/>
</EuiTextColor>
</div>,
]
: []),
]}
/>
),
[isRefreshOn, handleAutoRefreshSwitch, isAnyRuleSelected]
);
return (
<UtilityBar border>
<UtilityBarSection>
@ -170,15 +120,6 @@ export const RulesTableUtilityBar = React.memo<RulesTableUtilityBarProps>(
>
{i18n.REFRESH}
</UtilityBarAction>
<UtilityBarAction
disabled={hasDisabledActions}
dataTestSubj="refreshSettings"
iconSide="right"
iconType="arrowDown"
popoverContent={handleGetRefreshSettingsPopoverContent}
>
{i18n.REFRESH_RULE_POPOVER_LABEL}
</UtilityBarAction>
{!rulesTableContext.state.isDefault && (
<UtilityBarAction
dataTestSubj="clearTableFilters"
@ -197,6 +138,12 @@ export const RulesTableUtilityBar = React.memo<RulesTableUtilityBarProps>(
showUpdating: rulesTableContext.state.isFetching,
updatedAt: rulesTableContext.state.lastUpdated,
})}
<AutoRefreshButton
isDisabled={hasDisabledActions || isAnyRuleSelected}
isRefreshOn={isRefreshOn}
reFetchRules={reFetchRules}
setIsRefreshOn={setIsRefreshOn}
/>
</UtilityBarSection>
</UtilityBar>
);

View file

@ -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 (
<>
<NeedAdminForUpdateRulesCallOut />
<MissingPrivilegesCallOut />
<AddPrebuiltRulesTableContextProvider>
<SecuritySolutionPageWrapper>
<HeaderPage title={i18n.PAGE_TITLE}>
<AddPrebuiltRulesHeaderButtons />
</HeaderPage>
<AddPrebuiltRulesTable />
</SecuritySolutionPageWrapper>
</AddPrebuiltRulesTableContextProvider>
<SpyRoute pageName={SecurityPageName.rulesAdd} />
</>
);
};
export const AddRulesPage = React.memo(AddRulesPageComponent);
AddRulesPage.displayName = 'AddRulesPage';

View file

@ -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',
}
);

View file

@ -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 = () => {
<SuperHeader>
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false} wrap={true}>
<EuiFlexItem grow={false}>
<LoadPrePackagedRules>
{(renderProps) => <LoadPrePackagedRulesButton {...renderProps} />}
</LoadPrePackagedRules>
<LoadPrePackagedRulesButton />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip position="top" content={i18n.UPLOAD_VALUE_LISTS_TOOLTIP}>
<EuiButton
<EuiButtonEmpty
data-test-subj="open-value-lists-modal-button"
iconType="importAction"
isDisabled={!canWriteListsIndex || !canUserCRUD || loading}
onClick={showValueListFlyout}
>
{i18n.IMPORT_VALUE_LISTS}
</EuiButton>
</EuiButtonEmpty>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
<EuiButtonEmpty
data-test-subj="rules-import-modal-button"
iconType="importAction"
isDisabled={!hasUserCRUDPermission(canUserCRUD) || loading}
onClick={showImportModal}
>
{i18n.IMPORT_RULE}
</EuiButton>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<SecuritySolutionLinkButton
@ -156,9 +180,26 @@ const RulesPageComponent: React.FC = () => {
</EuiFlexItem>
</EuiFlexGroup>
</SuperHeader>
{(prePackagedRuleStatus === 'ruleNeedUpdate' ||
prePackagedTimelineStatus === 'timelineNeedUpdate') && (
<UpdatePrePackagedRulesCallOut data-test-subj="update-callout-button" />
{shouldDisplayUpdateRulesCallout && (
<MiniCallout
iconType={'iInCircle'}
data-test-subj="prebuilt-rules-update-callout"
title={getUpdateRulesCalloutTitle(updateCallOutOnClick)}
/>
)}
{shouldDisplayUpdateRulesCallout && shouldDisplayNewRulesCallout && (
<EuiSpacer size={'s'} />
)}
{shouldDisplayNewRulesCallout && (
<MiniCallout
color="success"
data-test-subj="prebuilt-rules-new-callout"
iconType={'iInCircle'}
title={NEW_PREBUILT_RULES_AVAILABLE_CALLOUT_TITLE}
/>
)}
<MaintenanceWindowCallout />
<AllRules data-test-subj="all-rules" />

View file

@ -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<ReturnType<typeof useAppToastsMock.create>>;
beforeEach(() => {
appToastsMock = useAppToastsMock.create();
(useAppToasts as jest.Mock).mockReturnValue(appToastsMock);
});
afterEach(() => {
jest.clearAllMocks();
});
it('renders correctly', () => {
const wrapper = mount(<PrePackagedRulesPrompt {...props} />, {
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(<PrePackagedRulesPrompt {...props} />, {
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(<PrePackagedRulesPrompt {...props} />, {
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(<PrePackagedRulesPrompt {...props} />, {
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(
<PrePackagedRulesPrompt {...{ ...props, loading: true }} />,
{
wrappingComponent: TestProviders,
}
);
await waitFor(() => {
wrapper.update();
expect(wrapper.find('button[data-test-subj="load-prebuilt-rules"]').props().disabled).toEqual(
true
);
});
});
});

View file

@ -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 (
<EmptyPrompt
data-test-subj="rulesEmptyPrompt"
@ -34,24 +26,11 @@ const PrePackagedRulesPromptComponent = () => {
actions={
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<LoadPrePackagedRules>
{(renderProps) => (
<LoadPrePackagedRulesButton
fill
data-test-subj="load-prebuilt-rules"
{...renderProps}
/>
)}
</LoadPrePackagedRules>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<SecuritySolutionLinkButton
isDisabled={!hasPermissions}
iconType="plusInCircle"
deepLinkId={SecurityPageName.rulesCreate}
>
{i18n.CREATE_RULE_ACTION}
</SecuritySolutionLinkButton>
<LoadPrePackagedRulesButton
fill={true}
data-test-subj="load-prebuilt-rules"
showBadge={false}
/>
</EuiFlexItem>
</EuiFlexGroup>
}

View file

@ -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<void>;
}
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 && (
<MlJobUpgradeModal
jobs={legacyJobsInstalled}
onCancel={() => hideUpgradeModal()}
onConfirm={mlJobUpgradeModalConfirm}
/>
)}
</>
);
};

View file

@ -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<void>;
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 getSecuritySolutionLinkProps = useGetSecuritySolutionLinkProps();
const { onClick: onClickLink } = getSecuritySolutionLinkProps({
deepLinkId: SecurityPageName.rulesAdd,
});
const showInstallButton =
(prePackagedAssetsStatus === 'ruleNotInstalled' ||
prePackagedTimelineStatus === 'timelinesNotInstalled') &&
prePackagedAssetsStatus !== 'someRuleUninstall';
const { data: preBuiltRulesStatus } = usePrebuiltRulesStatus();
const newRulesCount = preBuiltRulesStatus?.num_prebuilt_rules_to_install ?? 0;
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 (
<div>
<EuiButton
id={INSTALL_PREBUILT_RULES_ANCHOR}
fill={fill}
iconType="indexOpen"
isLoading={isLoading}
isDisabled={isDisabled}
onClick={onClick}
data-test-subj={dataTestSubj}
const ButtonComponent = fill ? EuiButton : EuiButtonEmpty;
return (
<ButtonComponent
id={INSTALL_PREBUILT_RULES_ANCHOR}
fill={fill}
iconType="plusInCircle"
color={'primary'}
onClick={onClickLink}
data-test-subj={dataTestSubj}
>
{i18n.ADD_ELASTIC_RULES}
{newRulesCount > 0 && showBadge && (
<EuiBadge
color={'#E0E5EE'}
css={css`
margin-left: 5px;
`}
>
{getLoadRulesOrTimelinesButtonTitle(prePackagedAssetsStatus, prePackagedTimelineStatus)}
</EuiButton>
</div>
);
}
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 (
<div>
<EuiButton
id={INSTALL_PREBUILT_RULES_ANCHOR}
fill={fill}
iconType="plusInCircle"
isLoading={isLoading}
isDisabled={isDisabled}
onClick={onClick}
data-test-subj={dataTestSubj}
>
{getMissingRulesOrTimelinesButtonTitle(
prePackagedRulesStatus?.rules_not_installed ?? 0,
prePackagedRulesStatus?.timelines_not_installed ?? 0
)}
</EuiButton>
</div>
);
}
return null;
{newRulesCount}
</EuiBadge>
)}
</ButtonComponent>
);
};

View file

@ -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 ADD_ELASTIC_RULES = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.addElasticRulesButtonTitle',
{
defaultMessage: 'Add Elastic 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 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', {

View file

@ -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(<UpdatePrePackagedRulesCallOut />, { 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(<UpdatePrePackagedRulesCallOut />, { 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(<UpdatePrePackagedRulesCallOut />, { 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(<UpdatePrePackagedRulesCallOut />, { 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(<UpdatePrePackagedRulesCallOut />, { 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(<UpdatePrePackagedRulesCallOut />, { wrapper: TestProviders });
expect(getByTestId('update-callout-button')).toHaveTextContent(
'Update 1 Elastic prebuilt rule and 1 Elastic prebuilt timeline'
);
});
});

View file

@ -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 (
<EuiCallOut title={i18n.UPDATE_PREPACKAGED_RULES_TITLE} data-test-subj="update-callout">
<p>
{prepackagedRulesOrTimelines?.callOutMessage}
<br />
<EuiLink href={`${services.docLinks.links.siem.ruleChangeLog}`} target="_blank">
{i18n.RELEASE_NOTES_HELP}
</EuiLink>
</p>
<LoadPrePackagedRules>
{(renderProps) => (
<EuiButton size="s" data-test-subj="update-callout-button" {...renderProps}>
{prepackagedRulesOrTimelines?.buttonTitle}
</EuiButton>
)}
</LoadPrePackagedRules>
</EuiCallOut>
);
};
export const UpdatePrePackagedRulesCallOut = memo(UpdatePrePackagedRulesCallOutComponent);

View file

@ -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;

View file

@ -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,

View file

@ -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',
}
);

View file

@ -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,

View file

@ -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 = () => {

View file

@ -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);
};

View file

@ -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<ThreeWayDiff<unknown>>(

View file

@ -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);

View file

@ -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 à",

View file

@ -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": "警告",

View file

@ -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": "警告于",