Skip to content

Commit

Permalink
[Cloud Security]Detection Rules counter on Rules Flyout (elastic#176041)
Browse files Browse the repository at this point in the history
## Summary

This PR adds a Detection Rules Counter on Rules Flyout, users are also
able to add Detection Rule on Flyout


https://github.com/elastic/kibana/assets/8703149/a000c1d5-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="https://github.com/elastic/kibana/assets/8703149/aad119d4-75a3-4407-8155-fb68d7d06f38">
  • Loading branch information
animehart authored and fkanout committed Feb 7, 2024
1 parent 37899d3 commit 0be7815
Show file tree
Hide file tree
Showing 6 changed files with 271 additions and 103 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
},
});
};
Original file line number Diff line number Diff line change
@@ -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}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down Expand Up @@ -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>
);
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
Loading

0 comments on commit 0be7815

Please sign in to comment.