[Security Solution] Fix Add Rules Page not refetching rules after package installed in background (#160389)

Fixes https://github.com/elastic/kibana/issues/160396

## Summary

- Invalidates cache after `security_detection_engine` package is
installed in the background so rules available for installation and for
upgrade are refetched, and their respective tables correctly populated
with them.
- Disable Install and Upgrade buttons in both tables while the package
is being installed in the background to prevent the user from attempting
to install/update outdated version.
- Add a Loading Skeleton in the Add Elastic Rules when the user
navigates to that page while the `security_detection_engine` is being
installed for the first time, to prevent the user from seeing a flash of
the "No available rules" component, before rules are loaded.


### Checklist

Delete any items that are not applicable to this PR.

- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
This commit is contained in:
Juan Pablo Djeredjian 2023-06-29 15:51:00 +02:00 committed by GitHub
parent c30251c46a
commit 2f03a25362
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 120 additions and 37 deletions

View file

@ -20,6 +20,9 @@ export interface PrebuiltRulesStatusStats {
/** Number of installed prebuilt rules available for upgrade (stock + customized) */
num_prebuilt_rules_to_upgrade: number;
/** Total number of prebuilt rules available in package (including already installed) */
num_prebuilt_rules_total_in_package: number;
// In the future we could add more stats such as:
// - number of installed prebuilt rules which were deprecated
// - number of installed prebuilt rules which are not compatible with the current version of Kibana

View file

@ -11,7 +11,9 @@ 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 { useInvalidateFetchPrebuiltRulesInstallReviewQuery } from './prebuilt_rules/use_fetch_prebuilt_rules_install_review_query';
import { useInvalidateFetchPrebuiltRulesStatusQuery } from './prebuilt_rules/use_fetch_prebuilt_rules_status_query';
import { useInvalidateFetchPrebuiltRulesUpgradeReviewQuery } from './prebuilt_rules/use_fetch_prebuilt_rules_upgrade_review_query';
export const BULK_INSTALL_FLEET_PACKAGES_MUTATION_KEY = [
'POST',
@ -22,6 +24,8 @@ export const useBulkInstallFleetPackagesMutation = (
options?: UseMutationOptions<BulkInstallPackagesResponse, Error, BulkInstallFleetPackagesProps>
) => {
const invalidatePrePackagedRulesStatus = useInvalidateFetchPrebuiltRulesStatusQuery();
const invalidatePrebuiltRulesInstallReview = useInvalidateFetchPrebuiltRulesInstallReviewQuery();
const invalidatePrebuiltRulesUpdateReview = useInvalidateFetchPrebuiltRulesUpgradeReviewQuery();
return useMutation((props: BulkInstallFleetPackagesProps) => bulkInstallFleetPackages(props), {
...options,
@ -34,6 +38,8 @@ export const useBulkInstallFleetPackagesMutation = (
if (rulesPackage && 'result' in rulesPackage && rulesPackage.result.status === 'installed') {
// The rules package was installed/updated, so invalidate the pre-packaged rules status query
invalidatePrePackagedRulesStatus();
invalidatePrebuiltRulesInstallReview();
invalidatePrebuiltRulesUpdateReview();
}
if (options?.onSettled) {

View file

@ -11,7 +11,9 @@ 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 { useInvalidateFetchPrebuiltRulesInstallReviewQuery } from './prebuilt_rules/use_fetch_prebuilt_rules_install_review_query';
import { useInvalidateFetchPrebuiltRulesStatusQuery } from './prebuilt_rules/use_fetch_prebuilt_rules_status_query';
import { useInvalidateFetchPrebuiltRulesUpgradeReviewQuery } from './prebuilt_rules/use_fetch_prebuilt_rules_upgrade_review_query';
export const INSTALL_FLEET_PACKAGE_MUTATION_KEY = [
'POST',
@ -22,6 +24,8 @@ export const useInstallFleetPackageMutation = (
options?: UseMutationOptions<InstallPackageResponse, Error, InstallFleetPackageProps>
) => {
const invalidatePrePackagedRulesStatus = useInvalidateFetchPrebuiltRulesStatusQuery();
const invalidatePrebuiltRulesInstallReview = useInvalidateFetchPrebuiltRulesInstallReviewQuery();
const invalidatePrebuiltRulesUpdateReview = useInvalidateFetchPrebuiltRulesUpgradeReviewQuery();
return useMutation((props: InstallFleetPackageProps) => installFleetPackage(props), {
...options,
@ -31,6 +35,8 @@ export const useInstallFleetPackageMutation = (
if (packageName === PREBUILT_RULES_PACKAGE_NAME) {
// Invalidate the pre-packaged rules status query as there might be new rules to install
invalidatePrePackagedRulesStatus();
invalidatePrebuiltRulesInstallReview();
invalidatePrebuiltRulesUpdateReview();
}
if (options?.onSettled) {

View file

@ -12,7 +12,7 @@ import * as i18n from './translations';
export const AddPrebuiltRulesHeaderButtons = () => {
const {
state: { rules, selectedRules, loadingRules },
state: { rules, selectedRules, loadingRules, isRefetching, isUpgradingSecurityPackages },
actions: { installAllRules, installSelectedRules },
} = useAddPrebuiltRulesTableContext();
@ -21,12 +21,13 @@ export const AddPrebuiltRulesHeaderButtons = () => {
const shouldDisplayInstallSelectedRulesButton = numberOfSelectedRules > 0;
const isRuleInstalling = loadingRules.length > 0;
const isRequestInProgress = isRuleInstalling || isRefetching || isUpgradingSecurityPackages;
return (
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false} wrap={true}>
{shouldDisplayInstallSelectedRulesButton ? (
<EuiFlexItem grow={false}>
<EuiButton onClick={installSelectedRules} disabled={isRuleInstalling}>
<EuiButton onClick={installSelectedRules} disabled={isRequestInProgress}>
{i18n.INSTALL_SELECTED_RULES(numberOfSelectedRules)}
{isRuleInstalling ? <EuiLoadingSpinner size="s" /> : undefined}
</EuiButton>
@ -38,7 +39,7 @@ export const AddPrebuiltRulesHeaderButtons = () => {
iconType="plusInCircle"
data-test-subj="installAllRulesButton"
onClick={installAllRules}
disabled={!isRulesAvailableForInstall || isRuleInstalling}
disabled={!isRulesAvailableForInstall || isRequestInProgress}
>
{i18n.INSTALL_ALL}
{isRuleInstalling ? <EuiLoadingSpinner size="s" /> : undefined}

View file

@ -14,7 +14,6 @@ import {
} from '@elastic/eui';
import React from 'react';
import { useIsUpgradingSecurityPackages } from '../../../../rule_management/logic/use_upgrade_security_packages';
import { RULES_TABLE_INITIAL_PAGE_SIZE, RULES_TABLE_PAGE_SIZE_OPTIONS } from '../constants';
import { AddPrebuiltRulesTableNoItemsMessage } from './add_prebuilt_rules_no_items_message';
import { useAddPrebuiltRulesTableContext } from './add_prebuilt_rules_table_context';
@ -25,24 +24,29 @@ import { useAddPrebuiltRulesTableColumns } from './use_add_prebuilt_rules_table_
* 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, filteredRules, isFetched, isLoading, isRefetching, selectedRules },
state: {
rules,
filteredRules,
isFetched,
isLoading,
isRefetching,
selectedRules,
isUpgradingSecurityPackages,
},
actions: { selectRules },
} = addRulesTableContext;
const rulesColumns = useAddPrebuiltRulesTableColumns();
const isTableEmpty = isFetched && rules.length === 0;
const shouldShowLinearProgress = (isFetched && isRefetching) || isUpgradingSecurityPackages;
const shouldShowLoadingOverlay = !isFetched && isRefetching;
const shouldShowProgress = isUpgradingSecurityPackages || isRefetching;
return (
<>
{shouldShowLinearProgress && (
{shouldShowProgress && (
<EuiProgress
data-test-subj="loadingRulesInfoProgress"
size="xs"
@ -51,7 +55,7 @@ export const AddPrebuiltRulesTable = React.memo(() => {
/>
)}
<EuiSkeletonLoading
isLoading={isLoading || shouldShowLoadingOverlay}
isLoading={isLoading}
loadingContent={
<>
<EuiSkeletonTitle />

View file

@ -7,6 +7,8 @@
import type { Dispatch, SetStateAction } from 'react';
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
import { useFetchPrebuiltRulesStatusQuery } from '../../../../rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_status_query';
import { useIsUpgradingSecurityPackages } from '../../../../rule_management/logic/use_upgrade_security_packages';
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';
@ -47,6 +49,11 @@ export interface AddPrebuiltRulesTableState {
* Is true whenever a background refetch is in-flight, which does not include initial loading
*/
isRefetching: boolean;
/**
* Is true when installing security_detection_rules
* package in background
*/
isUpgradingSecurityPackages: boolean;
/**
* List of rule IDs that are currently being upgraded
*/
@ -92,6 +99,10 @@ export const AddPrebuiltRulesTableContextProvider = ({
tags: [],
});
const { data: prebuiltRulesStatus } = useFetchPrebuiltRulesStatusQuery();
const isUpgradingSecurityPackages = useIsUpgradingSecurityPackages();
const {
data: { rules, stats: { tags } } = {
rules: [],
@ -105,6 +116,12 @@ export const AddPrebuiltRulesTableContextProvider = ({
} = 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
// Fetch rules to install only after background installation of security_detection_rules package is complete
enabled: Boolean(
!isUpgradingSecurityPackages &&
prebuiltRulesStatus &&
prebuiltRulesStatus.num_prebuilt_rules_total_in_package > 0
),
});
const { mutateAsync: installAllRulesRequest } = usePerformInstallAllRules();
@ -175,6 +192,7 @@ export const AddPrebuiltRulesTableContextProvider = ({
isLoading,
loadingRules,
isRefetching,
isUpgradingSecurityPackages,
selectedRules,
lastUpdated: dataUpdatedAt,
},
@ -189,6 +207,7 @@ export const AddPrebuiltRulesTableContextProvider = ({
isLoading,
loadingRules,
isRefetching,
isUpgradingSecurityPackages,
selectedRules,
dataUpdatedAt,
actions,

View file

@ -85,14 +85,20 @@ const INTEGRATIONS_COLUMN: TableColumn = {
const createInstallButtonColumn = (
installOneRule: AddPrebuiltRulesTableActions['installOneRule'],
loadingRules: RuleSignatureId[]
loadingRules: RuleSignatureId[],
isDisabled: boolean
): TableColumn => ({
field: 'rule_id',
name: '',
render: (ruleId: RuleSignatureId) => {
const isRuleInstalling = loadingRules.includes(ruleId);
const isInstallButtonDisabled = isRuleInstalling || isDisabled;
return (
<EuiButtonEmpty size="s" disabled={isRuleInstalling} onClick={() => installOneRule(ruleId)}>
<EuiButtonEmpty
size="s"
disabled={isInstallButtonDisabled}
onClick={() => installOneRule(ruleId)}
>
{isRuleInstalling ? <EuiLoadingSpinner size="s" /> : i18n.INSTALL_RULE_BUTTON}
</EuiButtonEmpty>
);
@ -106,10 +112,12 @@ export const useAddPrebuiltRulesTableColumns = (): TableColumn[] => {
const hasCRUDPermissions = hasUserCRUDPermission(canUserCRUD);
const [showRelatedIntegrations] = useUiSetting$<boolean>(SHOW_RELATED_INTEGRATIONS_SETTING);
const {
state: { loadingRules },
state: { loadingRules, isRefetching, isUpgradingSecurityPackages },
actions: { installOneRule },
} = useAddPrebuiltRulesTableContext();
const isDisabled = isRefetching || isUpgradingSecurityPackages;
return useMemo(
() => [
RULE_NAME_COLUMN,
@ -135,8 +143,10 @@ export const useAddPrebuiltRulesTableColumns = (): TableColumn[] => {
truncateText: true,
width: '12%',
},
...(hasCRUDPermissions ? [createInstallButtonColumn(installOneRule, loadingRules)] : []),
...(hasCRUDPermissions
? [createInstallButtonColumn(installOneRule, loadingRules, isDisabled)]
: []),
],
[hasCRUDPermissions, installOneRule, loadingRules, showRelatedIntegrations]
[hasCRUDPermissions, installOneRule, loadingRules, isDisabled, showRelatedIntegrations]
);
};

View file

@ -17,7 +17,6 @@ import {
} 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';
@ -36,24 +35,29 @@ const NO_ITEMS_MESSAGE = (
* Table Component for displaying rules that have available updates
*/
export const UpgradePrebuiltRulesTable = React.memo(() => {
const isUpgradingSecurityPackages = useIsUpgradingSecurityPackages();
const upgradeRulesTableContext = useUpgradePrebuiltRulesTableContext();
const {
state: { rules, filteredRules, isFetched, isLoading, isRefetching, selectedRules },
state: {
rules,
filteredRules,
isFetched,
isLoading,
selectedRules,
isRefetching,
isUpgradingSecurityPackages,
},
actions: { selectRules },
} = upgradeRulesTableContext;
const rulesColumns = useUpgradePrebuiltRulesTableColumns();
const isTableEmpty = isFetched && rules.length === 0;
const shouldShowLinearProgress = (isFetched && isRefetching) || isUpgradingSecurityPackages;
const shouldShowLoadingOverlay = !isFetched && isRefetching;
const shouldShowProgress = isUpgradingSecurityPackages || isRefetching;
return (
<>
{shouldShowLinearProgress && (
{shouldShowProgress && (
<EuiProgress
data-test-subj="loadingRulesInfoProgress"
size="xs"
@ -62,7 +66,7 @@ export const UpgradePrebuiltRulesTable = React.memo(() => {
/>
)}
<EuiSkeletonLoading
isLoading={isLoading || shouldShowLoadingOverlay}
isLoading={isLoading}
loadingContent={
<>
<EuiSkeletonTitle />

View file

@ -12,7 +12,7 @@ import { useUpgradePrebuiltRulesTableContext } from './upgrade_prebuilt_rules_ta
export const UpgradePrebuiltRulesTableButtons = () => {
const {
state: { rules, selectedRules, loadingRules },
state: { rules, selectedRules, loadingRules, isRefetching, isUpgradingSecurityPackages },
actions: { upgradeAllRules, upgradeSelectedRules },
} = useUpgradePrebuiltRulesTableContext();
@ -21,12 +21,13 @@ export const UpgradePrebuiltRulesTableButtons = () => {
const shouldDisplayUpgradeSelectedRulesButton = numberOfSelectedRules > 0;
const isRuleUpgrading = loadingRules.length > 0;
const isRequestInProgress = isRuleUpgrading || isRefetching || isUpgradingSecurityPackages;
return (
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false} wrap={true}>
{shouldDisplayUpgradeSelectedRulesButton ? (
<EuiFlexItem grow={false}>
<EuiButton onClick={upgradeSelectedRules} disabled={isRuleUpgrading}>
<EuiButton onClick={upgradeSelectedRules} disabled={isRequestInProgress}>
<>
{i18n.UPDATE_SELECTED_RULES(numberOfSelectedRules)}
{isRuleUpgrading ? <EuiLoadingSpinner size="s" /> : undefined}
@ -39,7 +40,7 @@ export const UpgradePrebuiltRulesTableButtons = () => {
fill
iconType="plusInCircle"
onClick={upgradeAllRules}
disabled={!isRulesAvailableForUpgrade || isRuleUpgrading}
disabled={!isRulesAvailableForUpgrade || isRequestInProgress}
>
{i18n.UPDATE_ALL}
{isRuleUpgrading ? <EuiLoadingSpinner size="s" /> : undefined}

View file

@ -7,6 +7,7 @@
import type { Dispatch, SetStateAction } from 'react';
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
import { useIsUpgradingSecurityPackages } from '../../../../rule_management/logic/use_upgrade_security_packages';
import { useInstalledSecurityJobs } from '../../../../../common/components/ml/hooks/use_installed_security_jobs';
import { useBoolState } from '../../../../../common/hooks/use_bool_state';
import { affectedJobIds } from '../../../../../detections/components/callouts/ml_job_compatibility_callout/affected_job_ids';
@ -53,6 +54,11 @@ export interface UpgradePrebuiltRulesTableState {
* Is true whenever a background refetch is in-flight, which does not include initial loading
*/
isRefetching: boolean;
/**
* Is true when installing security_detection_rules
* package in background
*/
isUpgradingSecurityPackages: boolean;
/**
* List of rule IDs that are currently being upgraded
*/
@ -100,6 +106,8 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
tags: [],
});
const isUpgradingSecurityPackages = useIsUpgradingSecurityPackages();
const {
data: { rules, stats: { tags } } = {
rules: [],
@ -211,11 +219,10 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
isFetched,
isLoading: isLoading && loadingJobs,
isRefetching,
isUpgradingSecurityPackages,
selectedRules,
loadingRules,
lastUpdated: dataUpdatedAt,
legacyJobsInstalled,
isUpgradeModalVisible,
},
actions,
};
@ -228,11 +235,10 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
isLoading,
loadingJobs,
isRefetching,
isUpgradingSecurityPackages,
selectedRules,
loadingRules,
dataUpdatedAt,
legacyJobsInstalled,
isUpgradeModalVisible,
actions,
]);

View file

@ -85,14 +85,20 @@ const INTEGRATIONS_COLUMN: TableColumn = {
const createUpgradeButtonColumn = (
upgradeOneRule: UpgradePrebuiltRulesTableActions['upgradeOneRule'],
loadingRules: RuleSignatureId[]
loadingRules: RuleSignatureId[],
isDisabled: boolean
): TableColumn => ({
field: 'rule_id',
name: '',
render: (ruleId: RuleUpgradeInfoForReview['rule_id']) => {
const isRuleUpgrading = loadingRules.includes(ruleId);
const isUpgradeButtonDisabled = isRuleUpgrading || isDisabled;
return (
<EuiButtonEmpty size="s" disabled={isRuleUpgrading} onClick={() => upgradeOneRule(ruleId)}>
<EuiButtonEmpty
size="s"
disabled={isUpgradeButtonDisabled}
onClick={() => upgradeOneRule(ruleId)}
>
{isRuleUpgrading ? <EuiLoadingSpinner size="s" /> : i18n.UPDATE_RULE_BUTTON}
</EuiButtonEmpty>
);
@ -106,10 +112,12 @@ export const useUpgradePrebuiltRulesTableColumns = (): TableColumn[] => {
const hasCRUDPermissions = hasUserCRUDPermission(canUserCRUD);
const [showRelatedIntegrations] = useUiSetting$<boolean>(SHOW_RELATED_INTEGRATIONS_SETTING);
const {
state: { loadingRules },
state: { loadingRules, isRefetching, isUpgradingSecurityPackages },
actions: { upgradeOneRule },
} = useUpgradePrebuiltRulesTableContext();
const isDisabled = isRefetching || isUpgradingSecurityPackages;
return useMemo(
() => [
RULE_NAME_COLUMN,
@ -136,8 +144,10 @@ export const useUpgradePrebuiltRulesTableColumns = (): TableColumn[] => {
truncateText: true,
width: '12%',
},
...(hasCRUDPermissions ? [createUpgradeButtonColumn(upgradeOneRule, loadingRules)] : []),
...(hasCRUDPermissions
? [createUpgradeButtonColumn(upgradeOneRule, loadingRules, isDisabled)]
: []),
],
[hasCRUDPermissions, loadingRules, showRelatedIntegrations, upgradeOneRule]
[hasCRUDPermissions, loadingRules, isDisabled, showRelatedIntegrations, upgradeOneRule]
);
};

View file

@ -38,7 +38,7 @@ export const getPrebuiltRulesStatusRoute = (router: SecuritySolutionPluginRouter
ruleAssetsClient,
ruleObjectsClient,
});
const { currentRules, installableRules, upgradeableRules } =
const { currentRules, installableRules, upgradeableRules, totalAvailableRules } =
getVersionBuckets(ruleVersionsMap);
const body: GetPrebuiltRulesStatusResponseBody = {
@ -46,6 +46,7 @@ export const getPrebuiltRulesStatusRoute = (router: SecuritySolutionPluginRouter
num_prebuilt_rules_installed: currentRules.length,
num_prebuilt_rules_to_install: installableRules.length,
num_prebuilt_rules_to_upgrade: upgradeableRules.length,
num_prebuilt_rules_total_in_package: totalAvailableRules.length,
},
};

View file

@ -31,14 +31,25 @@ export interface VersionBuckets {
*/
target: PrebuiltRuleAsset;
}>;
/**
* All available rules
* (installed and not installed)
*/
totalAvailableRules: PrebuiltRuleAsset[];
}
export const getVersionBuckets = (ruleVersionsMap: Map<string, RuleVersions>): VersionBuckets => {
const currentRules: RuleResponse[] = [];
const installableRules: PrebuiltRuleAsset[] = [];
const totalAvailableRules: PrebuiltRuleAsset[] = [];
const upgradeableRules: VersionBuckets['upgradeableRules'] = [];
ruleVersionsMap.forEach(({ current, target }) => {
if (target != null) {
// If this rule is available in the package
totalAvailableRules.push(target);
}
if (current != null) {
// If this rule is installed
currentRules.push(current);
@ -62,5 +73,6 @@ export const getVersionBuckets = (ruleVersionsMap: Map<string, RuleVersions>): V
currentRules,
installableRules,
upgradeableRules,
totalAvailableRules,
};
};