diff --git a/x-pack/plugins/cloud_security_posture/public/common/api/create_detection_rule.ts b/x-pack/plugins/cloud_security_posture/public/common/api/create_detection_rule.ts new file mode 100644 index 0000000000000..ef0aa3321f35e --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/common/api/create_detection_rule.ts @@ -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 => { + const res = await http.post(DETECTION_ENGINE_RULES_URL, { + body: JSON.stringify(rule), + }); + + return res as RuleResponse; +}; diff --git a/x-pack/plugins/cloud_security_posture/public/components/take_action.tsx b/x-pack/plugins/cloud_security_posture/public/components/take_action.tsx new file mode 100644 index 0000000000000..57684d02fd157 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/components/take_action.tsx @@ -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; +} +/* + * 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( +
+ + {ruleResponse.name} + {` `} + + + + + + + + + + + + +
+ ), + }); + }; + + const button = ( + setPopoverOpen(!isPopoverOpen)} + > + + + ); + + return ( + + { + closePopover(); + setIsLoading(true); + const ruleResponse = await createRuleFn(http); + setIsLoading(false); + showSuccessToast(ruleResponse); + }} + data-test-subj={CREATE_RULE_ACTION_SUBJ} + > + + , + ]} + /> + + ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts b/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts index 638076818d3f0..a1fa5d985df3c 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts @@ -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'; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/findings_flyout.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/findings_flyout.tsx index f59463e00125f..2c59f360850d8 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/findings_flyout.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/findings_flyout.tsx @@ -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'; @@ -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 = [ { @@ -127,6 +130,9 @@ export const FindingsRuleFlyout = ({ }: FindingFlyoutProps) => { const [tab, setTab] = useState(tabs[0]); + const createMisconfigurationRuleFn = async (http: HttpSetup) => + await createDetectionRuleFromFinding(http, findings); + return ( @@ -160,7 +166,7 @@ export const FindingsRuleFlyout = ({ - + + + + diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/utils/create_detection_rule_from_finding.ts b/x-pack/plugins/cloud_security_posture/public/pages/configurations/utils/create_detection_rule_from_finding.ts new file mode 100644 index 0000000000000..179ac6e27713c --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/utils/create_detection_rule_from_finding.ts @@ -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), + }, + }); +}; diff --git a/x-pack/plugins/cloud_security_posture/tsconfig.json b/x-pack/plugins/cloud_security_posture/tsconfig.json index ae8f7d610002b..a88bbf2bd0995 100755 --- a/x-pack/plugins/cloud_security_posture/tsconfig.json +++ b/x-pack/plugins/cloud_security_posture/tsconfig.json @@ -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/**/*",