mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Cloud Security]Detection Rules counter on Rules Flyout (#176041)
## Summary This PR adds a Detection Rules Counter on Rules Flyout, users are also able to add Detection Rule on Flyouta000c1d5
-3cec-4631-b255-745547e93c38 When Rule is disabled, the text 'Disabled' will be rendered instead of Detection Rule Counter <img width="1423" alt="Screenshot 2024-02-01 at 8 09 04 PM" src="aad119d4
-75a3-4407-8155-fb68d7d06f38">
This commit is contained in:
parent
1c7d089162
commit
a50f0ae1a6
6 changed files with 276 additions and 108 deletions
|
@ -8,10 +8,16 @@
|
|||
import { EuiBadge, EuiDescriptionList, EuiLink, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { CspFinding } from '../../../../common/schemas/csp_finding';
|
||||
import { RulesDetectionRuleCounter } from '../../rules/rules_detection_rule_counter';
|
||||
import { CisKubernetesIcons, CspFlyoutMarkdown } from './findings_flyout';
|
||||
|
||||
export const getRuleList = (rule: CspFinding['rule'], ruleFlyoutLink?: string) => [
|
||||
export const getRuleList = (
|
||||
rule: CspFinding['rule'],
|
||||
ruleState = 'unmuted',
|
||||
ruleFlyoutLink?: string
|
||||
) => [
|
||||
{
|
||||
title: i18n.translate('xpack.csp.findings.findingsFlyout.ruleTab.nameTitle', {
|
||||
defaultMessage: 'Name',
|
||||
|
@ -35,6 +41,20 @@ export const getRuleList = (rule: CspFinding['rule'], ruleFlyoutLink?: string) =
|
|||
}),
|
||||
description: <CspFlyoutMarkdown>{rule.description}</CspFlyoutMarkdown>,
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.csp.findings.findingsFlyout.ruleTab.AlertsTitle', {
|
||||
defaultMessage: 'Alerts',
|
||||
}),
|
||||
description:
|
||||
ruleState === 'unmuted' ? (
|
||||
<RulesDetectionRuleCounter benchmarkRule={rule} />
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.csp.findings.findingsFlyout.ruleTab.disabledRuleText"
|
||||
defaultMessage="Disabled"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.csp.findings.findingsFlyout.ruleTab.tagsTitle', {
|
||||
defaultMessage: 'Tags',
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* 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 { HttpSetup } from '@kbn/core/public';
|
||||
import { CspBenchmarkRule } from '../../../../common/types/latest';
|
||||
import {
|
||||
FINDINGS_INDEX_PATTERN,
|
||||
LATEST_FINDINGS_RETENTION_POLICY,
|
||||
} from '../../../../common/constants';
|
||||
import { createDetectionRule } from '../../../common/api/create_detection_rule';
|
||||
import { generateBenchmarkRuleTags } from '../../../../common/utils/detection_rules';
|
||||
|
||||
const DEFAULT_RULE_RISK_SCORE = 0;
|
||||
const DEFAULT_RULE_SEVERITY = 'low';
|
||||
const DEFAULT_RULE_ENABLED = true;
|
||||
const DEFAULT_RULE_AUTHOR = 'Elastic';
|
||||
const DEFAULT_RULE_LICENSE = 'Elastic License v2';
|
||||
const DEFAULT_MAX_ALERTS_PER_RULE = 100;
|
||||
const ALERT_SUPPRESSION_FIELD = 'resource.id';
|
||||
const ALERT_TIMESTAMP_FIELD = 'event.ingested';
|
||||
const DEFAULT_INVESTIGATION_FIELDS = {
|
||||
field_names: ['resource.name', 'resource.id', 'resource.type', 'resource.sub_type'],
|
||||
};
|
||||
|
||||
enum AlertSuppressionMissingFieldsStrategy {
|
||||
// per each document a separate alert will be created
|
||||
DoNotSuppress = 'doNotSuppress',
|
||||
// only one alert will be created per suppress by bucket
|
||||
Suppress = 'suppress',
|
||||
}
|
||||
|
||||
const convertReferencesLinksToArray = (input: string | undefined) => {
|
||||
if (!input) {
|
||||
return [];
|
||||
}
|
||||
// Match all URLs in the input string using a regular expression
|
||||
const matches = input.match(/(https?:\/\/\S+)/g);
|
||||
|
||||
if (!matches) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Remove the numbers and new lines
|
||||
return matches.map((link) => link.replace(/^\d+\. /, '').replace(/\n/g, ''));
|
||||
};
|
||||
|
||||
const generateFindingsRuleQuery = (benchmarkRule: CspBenchmarkRule['metadata']) => {
|
||||
const currentTimestamp = new Date().toISOString();
|
||||
|
||||
return `rule.benchmark.rule_number: "${benchmarkRule.benchmark.rule_number}"
|
||||
AND rule.benchmark.id: "${benchmarkRule.benchmark.id}"
|
||||
AND result.evaluation: "failed"
|
||||
AND event.ingested >= "${currentTimestamp}"`;
|
||||
};
|
||||
|
||||
/*
|
||||
* Creates a detection rule from a Benchmark rule
|
||||
*/
|
||||
export const createDetectionRuleFromBenchmark = async (
|
||||
http: HttpSetup,
|
||||
benchmarkRule: CspBenchmarkRule['metadata']
|
||||
) => {
|
||||
return await createDetectionRule({
|
||||
http,
|
||||
rule: {
|
||||
type: 'query',
|
||||
language: 'kuery',
|
||||
license: DEFAULT_RULE_LICENSE,
|
||||
author: [DEFAULT_RULE_AUTHOR],
|
||||
filters: [],
|
||||
false_positives: [],
|
||||
risk_score: DEFAULT_RULE_RISK_SCORE,
|
||||
risk_score_mapping: [],
|
||||
severity: DEFAULT_RULE_SEVERITY,
|
||||
severity_mapping: [],
|
||||
threat: [],
|
||||
interval: '1h',
|
||||
from: `now-${LATEST_FINDINGS_RETENTION_POLICY}`,
|
||||
to: 'now',
|
||||
max_signals: DEFAULT_MAX_ALERTS_PER_RULE,
|
||||
timestamp_override: ALERT_TIMESTAMP_FIELD,
|
||||
timestamp_override_fallback_disabled: false,
|
||||
actions: [],
|
||||
enabled: DEFAULT_RULE_ENABLED,
|
||||
alert_suppression: {
|
||||
group_by: [ALERT_SUPPRESSION_FIELD],
|
||||
missing_fields_strategy: AlertSuppressionMissingFieldsStrategy.Suppress,
|
||||
},
|
||||
index: [FINDINGS_INDEX_PATTERN],
|
||||
query: generateFindingsRuleQuery(benchmarkRule),
|
||||
references: convertReferencesLinksToArray(benchmarkRule.references),
|
||||
name: benchmarkRule.name,
|
||||
description: benchmarkRule.rationale,
|
||||
tags: generateBenchmarkRuleTags(benchmarkRule),
|
||||
investigation_fields: DEFAULT_INVESTIGATION_FIELDS,
|
||||
note: benchmarkRule.remediation,
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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 { HttpSetup } from '@kbn/core/public';
|
||||
import React from 'react';
|
||||
import { CspBenchmarkRule } from '../../../common/types/latest';
|
||||
import { getFindingsDetectionRuleSearchTags } from '../../../common/utils/detection_rules';
|
||||
import { DetectionRuleCounter } from '../../components/detection_rule_counter';
|
||||
import { createDetectionRuleFromBenchmark } from '../configurations/utils/create_detection_rule_from_benchmark';
|
||||
|
||||
export const RulesDetectionRuleCounter = ({
|
||||
benchmarkRule,
|
||||
}: {
|
||||
benchmarkRule: CspBenchmarkRule['metadata'];
|
||||
}) => {
|
||||
const createBenchmarkRuleFn = async (http: HttpSetup) =>
|
||||
await createDetectionRuleFromBenchmark(http, benchmarkRule);
|
||||
|
||||
return (
|
||||
<DetectionRuleCounter
|
||||
tags={getFindingsDetectionRuleSearchTags(benchmarkRule)}
|
||||
createRuleFn={createBenchmarkRuleFn}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -128,10 +128,25 @@ export const RuleFlyout = ({ onClose, rule, refetchRulesStates }: RuleFlyoutProp
|
|||
);
|
||||
};
|
||||
|
||||
const getRuleStateSwitch = (
|
||||
rule: CspBenchmarkRulesWithStates,
|
||||
switchRuleStates: () => Promise<void>
|
||||
) => [
|
||||
const RuleOverviewTab = ({
|
||||
rule,
|
||||
ruleData,
|
||||
switchRuleStates,
|
||||
}: {
|
||||
rule: CspBenchmarkRuleMetadata;
|
||||
ruleData: CspBenchmarkRulesWithStates;
|
||||
switchRuleStates: () => Promise<void>;
|
||||
}) => (
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionList
|
||||
listItems={[...ruleState(ruleData, switchRuleStates), ...getRuleList(rule, ruleData.state)]}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
const ruleState = (rule: CspBenchmarkRulesWithStates, switchRuleStates: () => Promise<void>) => [
|
||||
{
|
||||
title: (
|
||||
<EuiFlexGroup gutterSize="xs" alignItems="center">
|
||||
|
@ -172,21 +187,3 @@ const getRuleStateSwitch = (
|
|||
),
|
||||
},
|
||||
];
|
||||
|
||||
const RuleOverviewTab = ({
|
||||
rule,
|
||||
ruleData,
|
||||
switchRuleStates,
|
||||
}: {
|
||||
rule: CspBenchmarkRuleMetadata;
|
||||
ruleData: CspBenchmarkRulesWithStates;
|
||||
switchRuleStates: () => Promise<void>;
|
||||
}) => (
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionList
|
||||
listItems={[...getRuleStateSwitch(ruleData, switchRuleStates), ...getRuleList(rule)]}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
|
|
@ -181,6 +181,10 @@ export function RulePagePageProvider({ getService, getPageObjects }: FtrProvider
|
|||
const disabledRulesButton = await testSubjects.find('rules-counters-disabled-rules-button');
|
||||
await disabledRulesButton.click();
|
||||
},
|
||||
|
||||
doesElementExist: async (selector: string) => {
|
||||
return await testSubjects.exists(selector);
|
||||
},
|
||||
};
|
||||
|
||||
const navigateToRulePage = async (benchmarkCisId: string, benchmarkCisVersion: string) => {
|
||||
|
|
|
@ -28,8 +28,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
'findings',
|
||||
]);
|
||||
|
||||
// Failing: See https://github.com/elastic/kibana/issues/175905
|
||||
describe.skip('Cloud Posture Rules Page', function () {
|
||||
describe('Cloud Posture Rules Page', function () {
|
||||
this.tags(['cloud_security_posture_rules_page']);
|
||||
let rule: typeof pageObjects.rule;
|
||||
let findings: typeof pageObjects.findings;
|
||||
|
@ -72,20 +71,79 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
await findings.index.remove();
|
||||
});
|
||||
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/175614
|
||||
describe.skip('Rules Page - Bulk Action buttons', () => {
|
||||
it('It should disable both Enable and Disable options when there are no rules selected', async () => {
|
||||
await rule.rulePage.toggleBulkActionButton();
|
||||
expect(
|
||||
(await rule.rulePage.isBulkActionOptionDisabled(RULES_BULK_ACTION_OPTION_ENABLE)) ===
|
||||
'true'
|
||||
).to.be(true);
|
||||
expect(
|
||||
(await rule.rulePage.isBulkActionOptionDisabled(RULES_BULK_ACTION_OPTION_DISABLE)) ===
|
||||
'true'
|
||||
).to.be(true);
|
||||
describe('Rules Page - Rules Counters', () => {
|
||||
it('Shows posture score when there are findings', async () => {
|
||||
const isEmptyStateVisible = await rule.rulePage.getCountersEmptyState();
|
||||
expect(isEmptyStateVisible).to.be(false);
|
||||
|
||||
const postureScoreCounter = await rule.rulePage.getPostureScoreCounter();
|
||||
expect((await postureScoreCounter.getVisibleText()).includes('33%')).to.be(true);
|
||||
});
|
||||
|
||||
it('Clicking the posture score button leads to the dashboard', async () => {
|
||||
await rule.rulePage.clickPostureScoreButton();
|
||||
await pageObjects.common.waitUntilUrlIncludes('cloud_security_posture/dashboard');
|
||||
});
|
||||
|
||||
it('Shows integrations count when there are findings', async () => {
|
||||
const integrationsCounter = await rule.rulePage.getIntegrationsEvaluatedCounter();
|
||||
expect((await integrationsCounter.getVisibleText()).includes('1')).to.be(true);
|
||||
});
|
||||
|
||||
it('Clicking the integrations counter button leads to the integration page', async () => {
|
||||
await rule.rulePage.clickIntegrationsEvaluatedButton();
|
||||
await pageObjects.common.waitUntilUrlIncludes('add-integration/kspm');
|
||||
});
|
||||
|
||||
it('Shows the failed findings counter when there are findings', async () => {
|
||||
const failedFindingsCounter = await rule.rulePage.getFailedFindingsCounter();
|
||||
expect((await failedFindingsCounter.getVisibleText()).includes('2')).to.be(true);
|
||||
});
|
||||
|
||||
it('Clicking the failed findings button leads to the findings page', async () => {
|
||||
await rule.rulePage.clickFailedFindingsButton();
|
||||
await pageObjects.common.waitUntilUrlIncludes(
|
||||
'cloud_security_posture/findings/configurations'
|
||||
);
|
||||
});
|
||||
|
||||
it('Shows the disabled rules count', async () => {
|
||||
const disabledRulesCounter = await rule.rulePage.getDisabledRulesCounter();
|
||||
expect((await disabledRulesCounter.getVisibleText()).includes('0')).to.be(true);
|
||||
|
||||
// disable rule 1.1.1 (k8s findings mock contains a findings from that rule)
|
||||
await rule.rulePage.clickEnableRulesRowSwitchButton(0);
|
||||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
expect((await disabledRulesCounter.getVisibleText()).includes('1')).to.be(true);
|
||||
|
||||
const postureScoreCounter = await rule.rulePage.getPostureScoreCounter();
|
||||
expect((await postureScoreCounter.getVisibleText()).includes('0%')).to.be(true);
|
||||
|
||||
// enable rule back
|
||||
await rule.rulePage.clickEnableRulesRowSwitchButton(0);
|
||||
});
|
||||
|
||||
it('Clicking the disabled rules button shows enables the disabled filter', async () => {
|
||||
await rule.rulePage.clickEnableRulesRowSwitchButton(0);
|
||||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
||||
await rule.rulePage.clickDisabledRulesButton();
|
||||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
expect((await rule.rulePage.getEnableRulesRowSwitchButton()) === 1).to.be(true);
|
||||
});
|
||||
|
||||
it('Shows empty state when there are no findings', async () => {
|
||||
// Ensure there are no findings initially
|
||||
await findings.index.remove();
|
||||
await rule.navigateToRulePage('cis_k8s', '1.0.1');
|
||||
|
||||
const isEmptyStateVisible = await rule.rulePage.getCountersEmptyState();
|
||||
expect(isEmptyStateVisible).to.be(true);
|
||||
await rule.rulePage.clickEnableRulesRowSwitchButton(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Rules Page - Bulk Action buttons', () => {
|
||||
it('It should disable Enable option when there are all rules selected are already enabled ', async () => {
|
||||
await rule.rulePage.clickSelectAllRules();
|
||||
await rule.rulePage.toggleBulkActionButton();
|
||||
|
@ -99,6 +157,18 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
).to.be(false);
|
||||
});
|
||||
|
||||
it('It should disable both Enable and Disable options when there are no rules selected', async () => {
|
||||
await rule.rulePage.toggleBulkActionButton();
|
||||
expect(
|
||||
(await rule.rulePage.isBulkActionOptionDisabled(RULES_BULK_ACTION_OPTION_ENABLE)) ===
|
||||
'true'
|
||||
).to.be(true);
|
||||
expect(
|
||||
(await rule.rulePage.isBulkActionOptionDisabled(RULES_BULK_ACTION_OPTION_DISABLE)) ===
|
||||
'true'
|
||||
).to.be(true);
|
||||
});
|
||||
|
||||
it('It should disable Disable option when there are all rules selected are already Disabled', async () => {
|
||||
await rule.rulePage.clickSelectAllRules();
|
||||
await rule.rulePage.toggleBulkActionButton();
|
||||
|
@ -178,6 +248,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
expect((await rule.rulePage.getEnableSwitchButtonState()) === 'false').to.be(true);
|
||||
});
|
||||
it('Alerts section of Rules Flyout shows Disabled text when Rules are disabled', async () => {
|
||||
await rule.rulePage.clickRulesNames(0);
|
||||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
expect(
|
||||
(await rule.rulePage.doesElementExist(
|
||||
'csp:findings-flyout-create-detection-rule-link'
|
||||
)) === false
|
||||
).to.be(true);
|
||||
});
|
||||
it('Users are able to Enable/Disable Rule from Take Action on Rule Flyout', async () => {
|
||||
await rule.rulePage.clickRulesNames(0);
|
||||
await rule.rulePage.clickTakeActionButton();
|
||||
|
@ -185,78 +264,14 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
expect((await rule.rulePage.getEnableSwitchButtonState()) === 'true').to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Rules Page - Rules Counters', () => {
|
||||
it('Shows posture score when there are findings', async () => {
|
||||
const isEmptyStateVisible = await rule.rulePage.getCountersEmptyState();
|
||||
expect(isEmptyStateVisible).to.be(false);
|
||||
|
||||
const postureScoreCounter = await rule.rulePage.getPostureScoreCounter();
|
||||
expect((await postureScoreCounter.getVisibleText()).includes('33%')).to.be(true);
|
||||
});
|
||||
|
||||
it('Clicking the posture score button leads to the dashboard', async () => {
|
||||
await rule.rulePage.clickPostureScoreButton();
|
||||
await pageObjects.common.waitUntilUrlIncludes('cloud_security_posture/dashboard');
|
||||
});
|
||||
|
||||
it('Shows integrations count when there are findings', async () => {
|
||||
const integrationsCounter = await rule.rulePage.getIntegrationsEvaluatedCounter();
|
||||
expect((await integrationsCounter.getVisibleText()).includes('1')).to.be(true);
|
||||
});
|
||||
|
||||
it('Clicking the integrations counter button leads to the integration page', async () => {
|
||||
await rule.rulePage.clickIntegrationsEvaluatedButton();
|
||||
await pageObjects.common.waitUntilUrlIncludes(
|
||||
'cloud_security_posture/add-integration/kspm'
|
||||
);
|
||||
});
|
||||
|
||||
it('Shows the failed findings counter when there are findings', async () => {
|
||||
const failedFindingsCounter = await rule.rulePage.getFailedFindingsCounter();
|
||||
expect((await failedFindingsCounter.getVisibleText()).includes('2')).to.be(true);
|
||||
});
|
||||
|
||||
it('Clicking the failed findings button leads to the findings page', async () => {
|
||||
await rule.rulePage.clickFailedFindingsButton();
|
||||
await pageObjects.common.waitUntilUrlIncludes(
|
||||
'cloud_security_posture/findings/configurations'
|
||||
);
|
||||
});
|
||||
|
||||
it('Shows the disabled rules count', async () => {
|
||||
const disabledRulesCounter = await rule.rulePage.getDisabledRulesCounter();
|
||||
expect((await disabledRulesCounter.getVisibleText()).includes('0')).to.be(true);
|
||||
|
||||
// disable rule 1.1.1 (k8s findings mock contains a findings from that rule)
|
||||
await rule.rulePage.clickEnableRulesRowSwitchButton(0);
|
||||
it('Alerts section of Rules Flyout shows Detection Rule Counter component when Rules are enabled', async () => {
|
||||
await rule.rulePage.clickRulesNames(0);
|
||||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
expect((await disabledRulesCounter.getVisibleText()).includes('1')).to.be(true);
|
||||
|
||||
const postureScoreCounter = await rule.rulePage.getPostureScoreCounter();
|
||||
expect((await postureScoreCounter.getVisibleText()).includes('0%')).to.be(true);
|
||||
|
||||
// enable rule back
|
||||
await rule.rulePage.clickEnableRulesRowSwitchButton(0);
|
||||
});
|
||||
|
||||
it('Clicking the disabled rules button shows enables the disabled filter', async () => {
|
||||
await rule.rulePage.clickEnableRulesRowSwitchButton(0);
|
||||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
||||
await rule.rulePage.clickDisabledRulesButton();
|
||||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
expect((await rule.rulePage.getEnableRulesRowSwitchButton()) === 1).to.be(true);
|
||||
});
|
||||
|
||||
it('Shows empty state when there are no findings', async () => {
|
||||
// Ensure there are no findings initially
|
||||
await findings.index.remove();
|
||||
await rule.navigateToRulePage('cis_k8s', '1.0.1');
|
||||
|
||||
const isEmptyStateVisible = await rule.rulePage.getCountersEmptyState();
|
||||
expect(isEmptyStateVisible).to.be(true);
|
||||
expect(
|
||||
(await rule.rulePage.doesElementExist(
|
||||
'csp:findings-flyout-create-detection-rule-link'
|
||||
)) === true
|
||||
).to.be(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue