Skip to content

Commit

Permalink
[Cloud Security] Added Toast message that pops up when user enables /…
Browse files Browse the repository at this point in the history
… disables rule (#176462)

## Summary
This PR adds a toast message that pops up whenever user enables or
disables rules.
The toast message will show the number of rules that got disabled/enable
and detection rules (if there's any)


https://github.com/elastic/kibana/assets/8703149/6b394fc4-82ca-4afa-a6b4-e504e8a83f0c
  • Loading branch information
animehart authored Feb 12, 2024
1 parent 7f4208a commit 509f9ed
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,45 @@

import { CspBenchmarkRuleMetadata } from '../types';
import {
convertRuleTagsToKQL,
convertRuleTagsToMatchAllKQL,
convertRuleTagsToMatchAnyKQL,
generateBenchmarkRuleTags,
getFindingsDetectionRuleSearchTags,
getFindingsDetectionRuleSearchTagsFromArrayOfRules,
} from './detection_rules';

describe('Detection rules utils', () => {
it('should convert tags to KQL format', () => {
it('should convert tags to KQL format with AND operator', () => {
const inputTags = ['tag1', 'tag2', 'tag3'];

const result = convertRuleTagsToKQL(inputTags);
const result = convertRuleTagsToMatchAllKQL(inputTags);

const expectedKQL = 'alert.attributes.tags:("tag1" AND "tag2" AND "tag3")';
expect(result).toBe(expectedKQL);
});

it('Should convert tags to KQL format', () => {
it('Should convert tags to KQL format with AND Operator (empty array)', () => {
const inputTags = [] as string[];

const result = convertRuleTagsToKQL(inputTags);
const result = convertRuleTagsToMatchAllKQL(inputTags);

const expectedKQL = 'alert.attributes.tags:()';
expect(result).toBe(expectedKQL);
});

it('should convert tags to KQL format with OR Operator', () => {
const inputTags = ['tag1', 'tag2', 'tag3'];

const result = convertRuleTagsToMatchAnyKQL(inputTags);

const expectedKQL = 'alert.attributes.tags:("tag1" OR "tag2" OR "tag3")';
expect(result).toBe(expectedKQL);
});

it('Should convert tags to KQL format with OR Operator (empty array)', () => {
const inputTags = [] as string[];

const result = convertRuleTagsToMatchAnyKQL(inputTags);

const expectedKQL = 'alert.attributes.tags:()';
expect(result).toBe(expectedKQL);
Expand Down Expand Up @@ -63,6 +83,35 @@ describe('Detection rules utils', () => {
expect(result).toEqual(expectedTags);
});

it('Should generate search tags for a CSP benchmark rule given an array of Benchmarks', () => {
const cspBenchmarkRule = [
{
benchmark: {
id: 'cis_gcp',
rule_number: '1.1',
},
},
{
benchmark: {
id: 'cis_gcp',
rule_number: '1.2',
},
},
] as unknown as CspBenchmarkRuleMetadata[];

const result = getFindingsDetectionRuleSearchTagsFromArrayOfRules(cspBenchmarkRule);

const expectedTags = ['CIS GCP 1.1', 'CIS GCP 1.2'];
expect(result).toEqual(expectedTags);
});

it('Should handle undefined benchmark object gracefully given an array of empty benchmark', () => {
const cspBenchmarkRule = [{ benchmark: {} }] as any;
const expectedTags: string[] = [];
const result = getFindingsDetectionRuleSearchTagsFromArrayOfRules(cspBenchmarkRule);
expect(result).toEqual(expectedTags);
});

it('Should generate tags for a CSPM benchmark rule', () => {
const cspBenchmarkRule = {
benchmark: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@ const CSP_RULE_TAG_DATA_SOURCE_PREFIX = 'Data Source: ';

const STATIC_RULE_TAGS = [CSP_RULE_TAG, CSP_RULE_TAG_USE_CASE];

export const convertRuleTagsToKQL = (tags: string[]): string => {
export const convertRuleTagsToMatchAllKQL = (tags: string[]): string => {
const TAGS_FIELD = 'alert.attributes.tags';
return `${TAGS_FIELD}:(${tags.map((tag) => `"${tag}"`).join(' AND ')})`;
return `${TAGS_FIELD}:(${tags.map((tag) => `"${tag}"`).join(` AND `)})`;
};

export const convertRuleTagsToMatchAnyKQL = (tags: string[]): string => {
const TAGS_FIELD = 'alert.attributes.tags';
return `${TAGS_FIELD}:(${tags.map((tag) => `"${tag}"`).join(` OR `)})`;
};

/*
Expand All @@ -42,6 +47,30 @@ export const getFindingsDetectionRuleSearchTags = (
return benchmarkIdTags.concat([benchmarkRuleNumberTag]);
};

export const getFindingsDetectionRuleSearchTagsFromArrayOfRules = (
cspBenchmarkRules: CspBenchmarkRuleMetadata[]
): string[] => {
if (
!cspBenchmarkRules ||
!cspBenchmarkRules.some((rule) => rule.benchmark) ||
!cspBenchmarkRules.some((rule) => rule.benchmark.id)
) {
return [];
}

// we can just take the first benchmark id because we Know that the array will ONLY contain 1 kind of id
const benchmarkIds = cspBenchmarkRules.map((rule) => rule.benchmark.id);
if (benchmarkIds.length === 0) return [];
const benchmarkId = benchmarkIds[0];
const benchmarkRuleNumbers = cspBenchmarkRules.map((rule) => rule.benchmark.rule_number);
if (benchmarkRuleNumbers.length === 0) return [];
const benchmarkTagArray = benchmarkRuleNumbers.map(
(tag) => benchmarkId.replace('_', ' ').toUpperCase() + ' ' + tag
);
// we want the tags to only consist of a format like this CIS AWS 1.1.0
return benchmarkTagArray;
};

export const generateBenchmarkRuleTags = (rule: CspBenchmarkRuleMetadata) => {
return [STATIC_RULE_TAGS]
.concat(getFindingsDetectionRuleSearchTags(rule))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
* 2.0.
*/

import { CoreStart } from '@kbn/core/public';
import { CoreStart, HttpSetup } from '@kbn/core/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useQuery } from '@tanstack/react-query';
import { DETECTION_RULE_RULES_API_CURRENT_VERSION } from '../../../common/constants';
import { RuleResponse } from '../types';
import { DETECTION_ENGINE_RULES_KEY } from '../constants';
import { convertRuleTagsToKQL } from '../../../common/utils/detection_rules';
import {
convertRuleTagsToMatchAllKQL,
convertRuleTagsToMatchAnyKQL,
} from '../../../common/utils/detection_rules';

/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
Expand All @@ -31,20 +34,32 @@ const DETECTION_ENGINE_URL = '/api/detection_engine' as const;
const DETECTION_ENGINE_RULES_URL = `${DETECTION_ENGINE_URL}/rules` as const;
export const DETECTION_ENGINE_RULES_URL_FIND = `${DETECTION_ENGINE_RULES_URL}/_find` as const;

export const useFetchDetectionRulesByTags = (tags: string[]) => {
export const useFetchDetectionRulesByTags = (
tags: string[],
option: { match: 'all' | 'any' } = { match: 'all' }
) => {
const { http } = useKibana<CoreStart>().services;
return useQuery([DETECTION_ENGINE_RULES_KEY, tags, option], () =>
fetchDetectionRulesByTags(tags, option, http)
);
};

export const fetchDetectionRulesByTags = (
tags: string[],
option: { match: 'all' | 'any' } = { match: 'all' },
http: HttpSetup
) => {
const query = {
page: 1,
per_page: 1,
filter: convertRuleTagsToKQL(tags),
filter:
option.match === 'all'
? convertRuleTagsToMatchAllKQL(tags)
: convertRuleTagsToMatchAnyKQL(tags),
};

return useQuery([DETECTION_ENGINE_RULES_KEY, tags], () =>
http.fetch<FetchRulesResponse>(DETECTION_ENGINE_RULES_URL_FIND, {
method: 'GET',
version: DETECTION_RULE_RULES_API_CURRENT_VERSION,
query,
})
);
return http.fetch<FetchRulesResponse>(DETECTION_ENGINE_RULES_URL_FIND, {
method: 'GET',
version: DETECTION_RULE_RULES_API_CURRENT_VERSION,
query,
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,11 @@ export const showCreateDetectionRuleSuccessToast = (

export const showChangeBenchmarkRuleStatesSuccessToast = (
notifications: NotificationsStart,
isBenchmarkRuleMuted: boolean
isBenchmarkRuleMuted: boolean,
data: {
numberOfRules: number;
numberOfDetectionRules: number;
}
) => {
return notifications.toasts.addSuccess({
toastLifeTimeMs: 10000,
Expand All @@ -101,9 +105,23 @@ export const showChangeBenchmarkRuleStatesSuccessToast = (
</strong>
</EuiText>
<FormattedMessage
id="xpack.csp.flyout.ruleEnabledToast"
defaultMessage="Successfully enabled rule"
id="xpack.csp.flyout.ruleEnabledToastRulesCount"
defaultMessage="Successfully enabled {ruleCount, plural, one {# rule} other {# rules}} "
values={{
ruleCount: data.numberOfRules,
}}
/>
{data.numberOfDetectionRules > 0 ? (
<strong>
<FormattedMessage
id="xpack.csp.flyout.ruleEnabledToastDetectionRulesCount"
defaultMessage="and {detectionRuleCount, plural, one {# detection rule} other {# detection rules}}"
values={{
detectionRuleCount: data.numberOfDetectionRules,
}}
/>
</strong>
) : undefined}
</>
) : (
<>
Expand All @@ -115,10 +133,26 @@ export const showChangeBenchmarkRuleStatesSuccessToast = (
/>
</strong>
</EuiText>

<FormattedMessage
id="xpack.csp.flyout.ruleDisabledToast"
defaultMessage="Successfully disabled rule"
id="xpack.csp.flyout.ruleDisabledToastRulesCount"
defaultMessage="Successfully disabled {ruleCount, plural, one {# rule} other {# rules}} "
values={{
ruleCount: data.numberOfRules,
}}
/>

{data.numberOfDetectionRules > 0 ? (
<strong>
<FormattedMessage
id="xpack.csp.flyout.ruleDisabledToastDetectionRulesCount"
defaultMessage="and {detectionRuleCount, plural, one {# detection rule} other {# detection rules}}"
values={{
detectionRuleCount: data.numberOfDetectionRules,
}}
/>
</strong>
) : undefined}
</>
)}
</EuiText>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,20 @@ import {
EuiToolTip,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';

import { FormattedMessage } from '@kbn/i18n-react';
import { useKibana } from '../../common/hooks/use_kibana';
import { getFindingsDetectionRuleSearchTags } from '../../../common/utils/detection_rules';
import { CspBenchmarkRuleMetadata } from '../../../common/types/latest';
import { getRuleList } from '../configurations/findings_flyout/rule_tab';
import { getRemediationList } from '../configurations/findings_flyout/overview_tab';
import * as TEST_SUBJECTS from './test_subjects';
import { useChangeCspRuleState } from './change_csp_rule_state';
import { CspBenchmarkRulesWithStates } from './rules_container';
import { TakeAction } from '../../components/take_action';
import {
showChangeBenchmarkRuleStatesSuccessToast,
TakeAction,
} from '../../components/take_action';
import { useFetchDetectionRulesByTags } from '../../common/api/use_fetch_detection_rules_by_tags';

export const RULES_FLYOUT_SWITCH_BUTTON = 'rule-flyout-switch-button';

Expand Down Expand Up @@ -61,8 +66,11 @@ type RuleTab = typeof tabs[number]['id'];
export const RuleFlyout = ({ onClose, rule, refetchRulesStates }: RuleFlyoutProps) => {
const [tab, setTab] = useState<RuleTab>('overview');
const postRequestChangeRulesStates = useChangeCspRuleState();
const { data: rulesData } = useFetchDetectionRulesByTags(
getFindingsDetectionRuleSearchTags(rule.metadata)
);
const isRuleMuted = rule?.state === 'muted';

const { notifications } = useKibana().services;
const switchRuleStates = async () => {
if (rule.metadata.benchmark.rule_number) {
const rulesObjectRequest = {
Expand All @@ -74,6 +82,10 @@ export const RuleFlyout = ({ onClose, rule, refetchRulesStates }: RuleFlyoutProp
const nextRuleStates = isRuleMuted ? 'unmute' : 'mute';
await postRequestChangeRulesStates(nextRuleStates, [rulesObjectRequest]);
await refetchRulesStates();
await showChangeBenchmarkRuleStatesSuccessToast(notifications, isRuleMuted, {
numberOfRules: 1,
numberOfDetectionRules: rulesData?.total || 0,
});
}
};

Expand Down
Loading

0 comments on commit 509f9ed

Please sign in to comment.