Skip to content

Commit

Permalink
[Cloud Security] [Findings] [Misconfigurations] [Alerts] - Create det…
Browse files Browse the repository at this point in the history
…ection rule (#162750)

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
opauloh and kibanamachine authored Aug 9, 2023
1 parent c04f566 commit c0fe4ac
Show file tree
Hide file tree
Showing 6 changed files with 302 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* 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';

const DETECTION_ENGINE_URL = '/api/detection_engine' as const;
const DETECTION_ENGINE_RULES_URL = `${DETECTION_ENGINE_URL}/rules` as const;

interface RuleCreateProps {
type: string;
language: string;
license: string;
author: string[];
filters: any[];
false_positives: any[];
risk_score: number;
risk_score_mapping: any[];
severity: string;
severity_mapping: any[];
threat: any[];
interval: string;
from: string;
to: string;
timestamp_override: string;
timestamp_override_fallback_disabled: boolean;
actions: any[];
enabled: boolean;
alert_suppression: {
group_by: string[];
missing_fields_strategy: string;
};
index: string[];
query: string;
references: string[];
name: string;
description: string;
tags: string[];
}

export interface RuleResponse extends RuleCreateProps {
id: string;
}

export const createDetectionRule = async ({
http,
rule,
}: {
http: HttpSetup;
rule: RuleCreateProps;
}): Promise<RuleResponse> => {
const res = await http.post<RuleCreateProps>(DETECTION_ENGINE_RULES_URL, {
body: JSON.stringify(rule),
});

return res as RuleResponse;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* 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 React, { useState } from 'react';
import {
EuiButton,
EuiContextMenuItem,
EuiContextMenuPanel,
EuiFlexGroup,
EuiFlexItem,
EuiPopover,
EuiText,
useGeneratedHtmlId,
} from '@elastic/eui';
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
import type { HttpSetup } from '@kbn/core/public';
import { FormattedMessage } from '@kbn/i18n-react';
import { CREATE_RULE_ACTION_SUBJ, TAKE_ACTION_SUBJ } from './test_subjects';
import { useKibana } from '../common/hooks/use_kibana';
import type { RuleResponse } from '../common/api/create_detection_rule';

const RULE_PAGE_PATH = '/app/security/rules/id/';

interface TakeActionProps {
createRuleFn: (http: HttpSetup) => Promise<RuleResponse>;
}
/*
* This component is used to create a detection rule from Flyout.
* It accepts a createRuleFn parameter which is used to create a rule in a generic way.
*/
export const TakeAction = ({ createRuleFn }: TakeActionProps) => {
const [isPopoverOpen, setPopoverOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const closePopover = () => {
setPopoverOpen(false);
};

const smallContextMenuPopoverId = useGeneratedHtmlId({
prefix: 'smallContextMenuPopover',
});

const { http, notifications } = useKibana().services;

const showSuccessToast = (ruleResponse: RuleResponse) => {
return notifications.toasts.addSuccess({
toastLifeTimeMs: 10000,
color: 'success',
iconType: '',
text: toMountPoint(
<div>
<EuiText size="m">
<strong>{ruleResponse.name}</strong>
{` `}
<FormattedMessage
id="xpack.csp.flyout.ruleCreatedToastTitle"
defaultMessage="detection rule was created."
/>
</EuiText>
<EuiText size="s">
<FormattedMessage
id="xpack.csp.flyout.ruleCreatedToast"
defaultMessage="Add rule actions to get notified when alerts are generated."
/>
</EuiText>
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiButton size="s" href={http.basePath.prepend(RULE_PAGE_PATH + ruleResponse.id)}>
<FormattedMessage
id="xpack.csp.flyout.ruleCreatedToastViewRuleButton"
defaultMessage="View rule"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</div>
),
});
};

const button = (
<EuiButton
isLoading={isLoading}
fill
iconType="arrowDown"
iconSide="right"
onClick={() => setPopoverOpen(!isPopoverOpen)}
>
<FormattedMessage id="xpack.csp.flyout.takeActionButton" defaultMessage="Take action" />
</EuiButton>
);

return (
<EuiPopover
id={smallContextMenuPopoverId}
button={button}
isOpen={isPopoverOpen}
closePopover={closePopover}
panelPaddingSize="none"
anchorPosition="downLeft"
data-test-subj={TAKE_ACTION_SUBJ}
>
<EuiContextMenuPanel
size="s"
items={[
<EuiContextMenuItem
key="createRule"
onClick={async () => {
closePopover();
setIsLoading(true);
const ruleResponse = await createRuleFn(http);
setIsLoading(false);
showSuccessToast(ruleResponse);
}}
data-test-subj={CREATE_RULE_ACTION_SUBJ}
>
<FormattedMessage
defaultMessage="Create a detection rule"
id="xpack.csp.createDetectionRuleButton"
/>
</EuiContextMenuItem>,
]}
/>
</EuiPopover>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,7 @@ export const NO_VULNERABILITIES_STATUS_TEST_SUBJ = {

export const VULNERABILITIES_CONTAINER_TEST_SUBJ = 'vulnerabilities_container';

export const VULNERABILITIES_CVSS_SCORE_BADGE_SUBJ = 'vuknerabilities_cvss_score_badge';
export const VULNERABILITIES_CVSS_SCORE_BADGE_SUBJ = 'vulnerabilities_cvss_score_badge';

export const TAKE_ACTION_SUBJ = 'csp:take_action';
export const CREATE_RULE_ACTION_SUBJ = 'csp:create_rule';
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ import {
} from '@elastic/eui';
import { assertNever } from '@kbn/std';
import { i18n } from '@kbn/i18n';
import type { HttpSetup } from '@kbn/core/public';
import cisLogoIcon from '../../../assets/icons/cis_logo.svg';
import { CspFinding } from '../../../../common/schemas/csp_finding';
import { CspEvaluationBadge } from '../../../components/csp_evaluation_badge';
import { TakeAction } from '../../../components/take_action';
import { TableTab } from './table_tab';
import { JsonTab } from './json_tab';
import { OverviewTab } from './overview_tab';
Expand All @@ -36,6 +38,7 @@ import type { BenchmarkId } from '../../../../common/types';
import { CISBenchmarkIcon } from '../../../components/cis_benchmark_icon';
import { BenchmarkName } from '../../../../common/types';
import { FINDINGS_FLYOUT } from '../test_subjects';
import { createDetectionRuleFromFinding } from '../utils/create_detection_rule_from_finding';

const tabs = [
{
Expand Down Expand Up @@ -127,6 +130,9 @@ export const FindingsRuleFlyout = ({
}: FindingFlyoutProps) => {
const [tab, setTab] = useState<FindingsTab>(tabs[0]);

const createMisconfigurationRuleFn = async (http: HttpSetup) =>
await createDetectionRuleFromFinding(http, findings);

return (
<EuiFlyout onClose={onClose} data-test-subj={FINDINGS_FLYOUT}>
<EuiFlyoutHeader>
Expand Down Expand Up @@ -160,7 +166,7 @@ export const FindingsRuleFlyout = ({
<FindingsTab tab={tab} findings={findings} />
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup gutterSize="none" justifyContent="flexEnd">
<EuiFlexGroup gutterSize="none" alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiPagination
aria-label={PAGINATION_LABEL}
Expand All @@ -170,6 +176,9 @@ export const FindingsRuleFlyout = ({
compressed
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<TakeAction createRuleFn={createMisconfigurationRuleFn} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* 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 type { CspFinding } from '../../../../common/schemas/csp_finding';
import { LATEST_FINDINGS_INDEX_DEFAULT_NS } from '../../../../common/constants';
import { createDetectionRule } from '../../../common/api/create_detection_rule';

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 ALERT_SUPPRESSION_FIELD = 'resource.id';
const ALERT_TIMESTAMP_FIELD = 'event.ingested';

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 STATIC_RULE_TAGS = ['Elastic', 'Cloud Security'];

const generateMisconfigurationsTags = (finding: CspFinding) => {
return [STATIC_RULE_TAGS]
.concat(finding.rule.tags)
.concat(
finding.rule.benchmark.posture_type ? [finding.rule.benchmark.posture_type.toUpperCase()] : []
)
.flat();
};

const generateMisconfigurationsRuleQuery = (finding: CspFinding) => {
return `
rule.benchmark.rule_number: "${finding.rule.benchmark.rule_number}"
AND rule.benchmark.id: "${finding.rule.benchmark.id}"
AND result.evaluation: "failed"
`;
};

/*
* Creates a detection rule from a CspFinding
*/
export const createDetectionRuleFromFinding = async (http: HttpSetup, finding: CspFinding) => {
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-7200s',
to: 'now',
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: [LATEST_FINDINGS_INDEX_DEFAULT_NS],
query: generateMisconfigurationsRuleQuery(finding),
references: convertReferencesLinksToArray(finding.rule.references),
name: finding.rule.name,
description: finding.rule.rationale,
tags: generateMisconfigurationsTags(finding),
},
});
};
2 changes: 1 addition & 1 deletion x-pack/plugins/cloud_security_posture/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"@kbn/shared-ux-router",
"@kbn/core-saved-objects-server",
"@kbn/share-plugin",
"@kbn/core-http-server",
"@kbn/core-http-server"
],
"exclude": [
"target/**/*",
Expand Down

0 comments on commit c0fe4ac

Please sign in to comment.