Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution][Alerts] adds suppression for missing fields options for security rules #155055

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
fe2ec8d
[Security Solution][Alerts] adds suppression for missing fields optio…
vitaliidm Apr 17, 2023
c5df93c
Merge branch 'main' into alerts_8_8/suppression_missing_fields
vitaliidm Apr 18, 2023
e400d3d
Merge branch 'main' into alerts_8_8/suppression_missing_fields
vitaliidm Apr 18, 2023
6c1242a
POC on BE
vitaliidm Apr 18, 2023
2aa27be
fix multi fields
vitaliidm Apr 19, 2023
9684255
change wording
vitaliidm Apr 19, 2023
0428a0f
rename varaibles
vitaliidm Apr 20, 2023
ef737a2
fix for https://github.com/elastic/kibana/issues/155242
vitaliidm Apr 20, 2023
2ac3648
implementation #2
vitaliidm Apr 20, 2023
dbd253c
Delete partition_buckets_with_null_values.ts
vitaliidm Apr 20, 2023
ec54eb2
small fixes
vitaliidm Apr 21, 2023
b0103af
Merge branch 'alerts_8_8/suppression_missing_fields' of ssh://github.…
vitaliidm Apr 21, 2023
f421f81
i18n updates
vitaliidm Apr 21, 2023
9b354c7
fix TESTs
vitaliidm Apr 21, 2023
f7b9ef5
more fixes
vitaliidm Apr 21, 2023
b2e2610
fix types and tests
vitaliidm Apr 21, 2023
e81af5c
add accordion
vitaliidm Apr 21, 2023
7fe3b20
Merge branch 'main' into alerts_8_8/suppression_missing_fields
vitaliidm Apr 21, 2023
ce4ab69
add funcitonal tests
vitaliidm Apr 21, 2023
50abfa2
Merge branch 'alerts_8_8/suppression_missing_fields' of ssh://github.…
vitaliidm Apr 21, 2023
b63f7b3
tests
vitaliidm Apr 21, 2023
8bd4e0d
Merge branch 'main' into alerts_8_8/suppression_missing_fields
vitaliidm Apr 21, 2023
23d7e6a
CR
vitaliidm Apr 24, 2023
3cc033e
Merge branch 'alerts_8_8/suppression_missing_fields' of ssh://github.…
vitaliidm Apr 24, 2023
57b7f7a
fix bundle size
vitaliidm Apr 24, 2023
d2de48e
fix types
vitaliidm Apr 24, 2023
69899d5
check bundle
vitaliidm Apr 24, 2023
f8927d8
Merge branch 'main' into alerts_8_8/suppression_missing_fields
vitaliidm Apr 24, 2023
c1ccae3
use bulkCreate
vitaliidm Apr 24, 2023
9e0c671
Merge branch 'alerts_8_8/suppression_missing_fields' of ssh://github.…
vitaliidm Apr 24, 2023
7de6620
typos
vitaliidm Apr 24, 2023
571aca2
Merge branch 'main' into alerts_8_8/suppression_missing_fields
vitaliidm Apr 24, 2023
71ebf45
CR
vitaliidm Apr 24, 2023
086606f
additional tests
vitaliidm Apr 25, 2023
51e3e13
Merge branch 'main' into alerts_8_8/suppression_missing_fields
vitaliidm Apr 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,27 @@ import * as t from 'io-ts';
import {
LimitedSizeArray,
PositiveIntegerGreaterThanZero,
enumeration,
} from '@kbn/securitysolution-io-ts-types';

/**
* describes how alerts will be generated for documents with missing suppress by fields
*/
export enum AlertSuppressionMissingFieldsStrategy {
// per each document a separate alert will be created
DoNotSuppress = 'doNotSuppress',
// only alert will be created per suppress by bucket
Suppress = 'suppress',
}

export type AlertSuppressionMissingFields = t.TypeOf<typeof AlertSuppressionMissingFields>;
export const AlertSuppressionMissingFields = enumeration(
'AlertSuppressionMissingFields',
AlertSuppressionMissingFieldsStrategy
);
export const DEFAULT_SUPPRESSION_MISSING_FIELDS_STRATEGY =
AlertSuppressionMissingFieldsStrategy.Suppress;

export const AlertSuppressionGroupBy = LimitedSizeArray({
codec: t.string,
minSize: 1,
Expand Down Expand Up @@ -41,6 +60,7 @@ export const AlertSuppression = t.intersection([
t.exact(
t.partial({
duration: AlertSuppressionDuration,
missing_fields_strategy: AlertSuppressionMissingFields,
marshallmain marked this conversation as resolved.
Show resolved Hide resolved
})
),
]);
Expand All @@ -55,6 +75,7 @@ export const AlertSuppressionCamel = t.intersection([
t.exact(
t.partial({
duration: AlertSuppressionDuration,
missingFieldsStrategy: AlertSuppressionMissingFields,
marshallmain marked this conversation as resolved.
Show resolved Hide resolved
})
),
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
GroupByOptions,
} from '../../../../detections/pages/detection_engine/rules/types';
import type { RuleCreateProps } from '../../../../../common/detection_engine/rule_schema';
import { DEFAULT_SUPPRESSION_MISSING_FIELDS_STRATEGY } from '../../../../../common/detection_engine/rule_schema';
import { stepActionsDefaultValue } from '../../../../detections/components/rules/step_rule_actions';

export const getTimeTypeValue = (time: string): { unit: Unit; value: number } => {
Expand Down Expand Up @@ -447,6 +448,9 @@ export const formatDefineStepData = (defineStepData: DefineStepRule): DefineStep
ruleFields.groupByRadioSelection === GroupByOptions.PerTimePeriod
? ruleFields.groupByDuration
: undefined,
missing_fields_strategy:
ruleFields.suppressionMissingFields ||
DEFAULT_SUPPRESSION_MISSING_FIELDS_STRATEGY,
},
}
: {}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
EuiIcon,
EuiToolTip,
EuiFlexGrid,
EuiBetaBadge,
} from '@elastic/eui';
import { ALERT_RISK_SCORE } from '@kbn/rule-data-utils';

Expand All @@ -37,7 +36,6 @@ import type {
RequiredFieldArray,
Threshold,
} from '../../../../../common/detection_engine/rule_schema';
import { minimumLicenseForSuppression } from '../../../../../common/detection_engine/rule_schema';

import * as i18n from './translations';
import type { BuildQueryBarDescription, BuildThreatDescription, ListItems } from './types';
Expand All @@ -50,8 +48,9 @@ import type {
import { GroupByOptions } from '../../../pages/detection_engine/rules/types';
import { defaultToEmptyTag } from '../../../../common/components/empty_value';
import { ThreatEuiFlexGroup } from './threat_description';
import { TechnicalPreviewBadge } from './technical_preview_badge';
import type { LicenseService } from '../../../../../common/license';

import { AlertSuppressionMissingFieldsStrategy } from '../../../../../common/detection_engine/rule_schema';
const NoteDescriptionContainer = styled(EuiFlexItem)`
height: 105px;
overflow-y: hidden;
Expand Down Expand Up @@ -535,21 +534,7 @@ export const buildAlertSuppressionDescription = (
</EuiFlexGroup>
);

const title = (
<>
{label}
<EuiBetaBadge
label={i18n.ALERT_SUPPRESSION_TECHNICAL_PREVIEW}
style={{ verticalAlign: 'middle', marginLeft: '8px' }}
size="s"
/>
{!license.isAtLeast(minimumLicenseForSuppression) && (
<EuiToolTip position="top" content={i18n.ALERT_SUPPRESSION_INSUFFICIENT_LICENSE}>
<EuiIcon type={'warning'} size="l" color="#BD271E" style={{ marginLeft: '8px' }} />
</EuiToolTip>
)}
</>
);
const title = <TechnicalPreviewBadge label={label} license={license} />;
return [
{
title,
Expand All @@ -569,21 +554,30 @@ export const buildAlertSuppressionWindowDescription = (
? `${value.value}${value.unit}`
: i18n.ALERT_SUPPRESSION_PER_RULE_EXECUTION;

const title = (
<>
{label}
<EuiBetaBadge
label={i18n.ALERT_SUPPRESSION_TECHNICAL_PREVIEW}
style={{ verticalAlign: 'middle', marginLeft: '8px' }}
size="s"
/>
{!license.isAtLeast(minimumLicenseForSuppression) && (
<EuiToolTip position="top" content={i18n.ALERT_SUPPRESSION_INSUFFICIENT_LICENSE}>
<EuiIcon type={'warning'} size="l" color="#BD271E" style={{ marginLeft: '8px' }} />
</EuiToolTip>
)}
</>
);
const title = <TechnicalPreviewBadge label={label} license={license} />;
return [
{
title,
description,
},
];
};

export const buildAlertSuppressionMissingFieldsDescription = (
label: string,
value: AlertSuppressionMissingFieldsStrategy,
license: LicenseService
): ListItems[] => {
if (isEmpty(value)) {
return [];
}

const description =
value === AlertSuppressionMissingFieldsStrategy.Suppress
? i18n.ALERT_SUPPRESSION_SUPPRESS_ON_MISSING_FIELDS
: i18n.ALERT_SUPPRESSION_DO_NOT_SUPPRESS_ON_MISSING_FIELDS;

const title = <TechnicalPreviewBadge label={label} license={license} />;
return [
{
title,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
buildRequiredFieldsDescription,
buildAlertSuppressionDescription,
buildAlertSuppressionWindowDescription,
buildAlertSuppressionMissingFieldsDescription,
} from './helpers';
import { buildMlJobsDescription } from './build_ml_jobs_description';
import { buildActionsDescription } from './actions_description';
Expand Down Expand Up @@ -216,6 +217,13 @@ export const getDescriptionItem = (
} else {
return [];
}
} else if (field === 'suppressionMissingFields') {
if (get('groupByFields', data).length > 0) {
const value = get(field, data);
return buildAlertSuppressionMissingFieldsDescription(label, value, license);
} else {
return [];
}
} else if (field === 'eqlOptions') {
const eqlOptions: EqlOptionsSelected = get(field, data);
return buildEqlOptionsDescription(eqlOptions);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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 from 'react';
import { EuiIcon, EuiToolTip, EuiBetaBadge } from '@elastic/eui';

import type { LicenseService } from '../../../../../common/license';
import { minimumLicenseForSuppression } from '../../../../../common/detection_engine/rule_schema';

import * as i18n from './translations';

interface TechnicalPreviewBadgeProps {
label: string;
license: LicenseService;
}

export const TechnicalPreviewBadge = ({ label, license }: TechnicalPreviewBadgeProps) => (
<>
{label}
<EuiBetaBadge
label={i18n.ALERT_SUPPRESSION_TECHNICAL_PREVIEW}
style={{ verticalAlign: 'middle', marginLeft: '8px' }}
size="s"
/>
{!license.isAtLeast(minimumLicenseForSuppression) && (
<EuiToolTip position="top" content={i18n.ALERT_SUPPRESSION_INSUFFICIENT_LICENSE}>
<EuiIcon type={'warning'} size="l" color="#BD271E" style={{ marginLeft: '8px' }} />
</EuiToolTip>
)}
</>
);
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,17 @@ export const ALERT_SUPPRESSION_PER_RULE_EXECUTION = i18n.translate(
defaultMessage: 'One rule execution',
}
);

export const ALERT_SUPPRESSION_SUPPRESS_ON_MISSING_FIELDS = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDescription.alertSuppressionSuppressOnMissingFieldsDescription',
{
defaultMessage: 'Suppress on missing field value',
}
);

export const ALERT_SUPPRESSION_DO_NOT_SUPPRESS_ON_MISSING_FIELDS = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDescription.alertSuppressionDoNotSuppressOnMissingFieldsDescription',
{
defaultMessage: 'Do not suppress',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import type { EuiButtonGroupOptionProps } from '@elastic/eui';
import {
EuiAccordion,
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
Expand Down Expand Up @@ -88,7 +89,10 @@ import { defaultCustomQuery } from '../../../pages/detection_engine/rules/utils'
import { getIsRulePreviewDisabled } from '../rule_preview/helpers';
import { GroupByFields } from '../group_by_fields';
import { useLicense } from '../../../../common/hooks/use_license';
import { minimumLicenseForSuppression } from '../../../../../common/detection_engine/rule_schema';
import {
minimumLicenseForSuppression,
AlertSuppressionMissingFieldsStrategy,
} from '../../../../../common/detection_engine/rule_schema';
import { DurationInput } from '../duration_input';

const CommonUseField = getUseField({ component: Field });
Expand Down Expand Up @@ -179,6 +183,7 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
'groupByRadioSelection',
'groupByDuration.value',
'groupByDuration.unit',
'suppressionMissingFields',
],
onChange: (data: DefineStepRule) => {
if (onRuleDataChange) {
Expand Down Expand Up @@ -561,6 +566,34 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
[license, groupByFields]
);

const AlertsSuppressionMissingFields = useCallback(
({ suppressionMissingFields }) => (
<EuiRadioGroup
disabled={
!license.isAtLeast(minimumLicenseForSuppression) ||
groupByFields == null ||
groupByFields.length === 0
}
idSelected={suppressionMissingFields.value}
options={[
{
id: AlertSuppressionMissingFieldsStrategy.Suppress,
label: i18n.ALERT_SUPPRESSION_MISSING_FIELDS_SUPPRESS_OPTION,
},
{
id: AlertSuppressionMissingFieldsStrategy.DoNotSuppress,
label: i18n.ALERT_SUPPRESSION_MISSING_FIELDS_DO_NOT_SUPPRESS_OPTION,
},
]}
onChange={(id: string) => {
suppressionMissingFields.setValue(id);
}}
data-test-subj="suppressionMissingFieldsOptions"
/>
),
[license, groupByFields]
);

const dataViewIndexPatternToggleButtonOptions: EuiButtonGroupOptionProps[] = useMemo(
() => [
{
Expand Down Expand Up @@ -868,41 +901,68 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
</>
)}

<RuleTypeEuiFormRow
$isVisible={isQueryRule(ruleType)}
data-test-subj="alertSuppressionInput"
<EuiSpacer size="l" />
<EuiAccordion
data-test-subj="alertSuppressionAccordion"
id="alertSuppressionAccordion"
buttonContent={i18n.ALERT_SUPPRESSION_ACCORDION_BUTTON}
>
<UseField
path="groupByFields"
component={GroupByFields}
componentProps={{
browserFields: termsAggregationFields,
isDisabled:
!license.isAtLeast(minimumLicenseForSuppression) &&
initialState.groupByFields.length === 0,
}}
/>
</RuleTypeEuiFormRow>
<RuleTypeEuiFormRow
$isVisible={isQueryRule(ruleType)}
data-test-subj="alertSuppressionDuration"
>
<UseMultiFields
fields={{
groupByRadioSelection: {
path: 'groupByRadioSelection',
},
groupByDurationValue: {
path: 'groupByDuration.value',
},
groupByDurationUnit: {
path: 'groupByDuration.unit',
},
}}
<EuiSpacer size="l" />

<RuleTypeEuiFormRow
$isVisible={isQueryRule(ruleType)}
data-test-subj="alertSuppressionInput"
>
{GroupByChildren}
</UseMultiFields>
</RuleTypeEuiFormRow>
<UseField
path="groupByFields"
component={GroupByFields}
componentProps={{
browserFields: termsAggregationFields,
isDisabled:
!license.isAtLeast(minimumLicenseForSuppression) &&
initialState.groupByFields.length === 0,
}}
/>
</RuleTypeEuiFormRow>

<RuleTypeEuiFormRow
$isVisible={isQueryRule(ruleType)}
data-test-subj="alertSuppressionDuration"
>
<UseMultiFields
fields={{
groupByRadioSelection: {
path: 'groupByRadioSelection',
},
groupByDurationValue: {
path: 'groupByDuration.value',
},
groupByDurationUnit: {
path: 'groupByDuration.unit',
},
}}
>
{GroupByChildren}
</UseMultiFields>
</RuleTypeEuiFormRow>

<RuleTypeEuiFormRow
$isVisible={isQueryRule(ruleType)}
data-test-subj="alertSuppressionMissingFields"
label={i18n.ALERT_SUPPRESSION_MISSING_FIELDS_FORM_ROW_LABEL}
>
<UseMultiFields
fields={{
suppressionMissingFields: {
path: 'suppressionMissingFields',
},
}}
>
{AlertsSuppressionMissingFields}
</UseMultiFields>
</RuleTypeEuiFormRow>
</EuiAccordion>
<EuiSpacer size="l" />

<RuleTypeEuiFormRow $isVisible={isMlRule(ruleType)} fullWidth>
<>
Expand Down
Loading