diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/rule_tab.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/rule_tab.tsx
index 63822a185fe312..9b7b400a581964 100644
--- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/rule_tab.tsx
+++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/rule_tab.tsx
@@ -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: {rule.description},
},
+ {
+ title: i18n.translate('xpack.csp.findings.findingsFlyout.ruleTab.AlertsTitle', {
+ defaultMessage: 'Alerts',
+ }),
+ description:
+ ruleState === 'unmuted' ? (
+
+ ) : (
+
+ ),
+ },
{
title: i18n.translate('xpack.csp.findings.findingsFlyout.ruleTab.tagsTitle', {
defaultMessage: 'Tags',
diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/utils/create_detection_rule_from_benchmark.ts b/x-pack/plugins/cloud_security_posture/public/pages/configurations/utils/create_detection_rule_from_benchmark.ts
new file mode 100644
index 00000000000000..f445761ed84473
--- /dev/null
+++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/utils/create_detection_rule_from_benchmark.ts
@@ -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,
+ },
+ });
+};
diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_detection_rule_counter.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_detection_rule_counter.tsx
new file mode 100644
index 00000000000000..04b6a4ab835977
--- /dev/null
+++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_detection_rule_counter.tsx
@@ -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 (
+
+ );
+};
diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_flyout.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_flyout.tsx
index 33215bf80d30bc..333f958de6fb1d 100644
--- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_flyout.tsx
+++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_flyout.tsx
@@ -128,10 +128,25 @@ export const RuleFlyout = ({ onClose, rule, refetchRulesStates }: RuleFlyoutProp
);
};
-const getRuleStateSwitch = (
- rule: CspBenchmarkRulesWithStates,
- switchRuleStates: () => Promise
-) => [
+const RuleOverviewTab = ({
+ rule,
+ ruleData,
+ switchRuleStates,
+}: {
+ rule: CspBenchmarkRuleMetadata;
+ ruleData: CspBenchmarkRulesWithStates;
+ switchRuleStates: () => Promise;
+}) => (
+
+
+
+
+
+);
+
+const ruleState = (rule: CspBenchmarkRulesWithStates, switchRuleStates: () => Promise) => [
{
title: (
@@ -172,21 +187,3 @@ const getRuleStateSwitch = (
),
},
];
-
-const RuleOverviewTab = ({
- rule,
- ruleData,
- switchRuleStates,
-}: {
- rule: CspBenchmarkRuleMetadata;
- ruleData: CspBenchmarkRulesWithStates;
- switchRuleStates: () => Promise;
-}) => (
-
-
-
-
-
-);
diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/rule_page.ts b/x-pack/test/cloud_security_posture_functional/page_objects/rule_page.ts
index 8ab9afd818146f..3cf0810c7a03ee 100644
--- a/x-pack/test/cloud_security_posture_functional/page_objects/rule_page.ts
+++ b/x-pack/test/cloud_security_posture_functional/page_objects/rule_page.ts
@@ -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) => {
diff --git a/x-pack/test/cloud_security_posture_functional/pages/rules.ts b/x-pack/test/cloud_security_posture_functional/pages/rules.ts
index 81629da46439c6..b734c391bf27c7 100644
--- a/x-pack/test/cloud_security_posture_functional/pages/rules.ts
+++ b/x-pack/test/cloud_security_posture_functional/pages/rules.ts
@@ -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,9 +71,81 @@ 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 () => {
+ 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();
expect(
(await rule.rulePage.isBulkActionOptionDisabled(RULES_BULK_ACTION_OPTION_ENABLE)) ===
@@ -83,11 +154,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
expect(
(await rule.rulePage.isBulkActionOptionDisabled(RULES_BULK_ACTION_OPTION_DISABLE)) ===
'true'
- ).to.be(true);
+ ).to.be(false);
});
- it('It should disable Enable option when there are all rules selected are already enabled ', async () => {
- await rule.rulePage.clickSelectAllRules();
+ 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)) ===
@@ -96,7 +166,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
expect(
(await rule.rulePage.isBulkActionOptionDisabled(RULES_BULK_ACTION_OPTION_DISABLE)) ===
'true'
- ).to.be(false);
+ ).to.be(true);
});
it('It should disable Disable option when there are all rules selected are already Disabled', async () => {
@@ -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);
- 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();
+ 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 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);
});
});
});