mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Security Solution][Detection Engine] adds alert suppression for New Terms rule type (#178294)
## Summary - addresses https://github.com/elastic/security-team/issues/8824 - adds alert suppression for new terms rule type - fixes `getOpenAlerts` test function, which returned closed alerts as well ### UI <img width="2294" alt="Screenshot 2024-04-02 at 12 53 26" src="8398fba4
-a06c-464b-87ef-1c5d5a18e37f"> <img width="1651" alt="Screenshot 2024-04-02 at 12 53 46" src="971ec0da
-c1d9-4c96-a4af-7cc8dfae52a4"> ### Checklist - [x] Functional changes are hidden behind a feature flag Feature flag `alertSuppressionForNewTermsRuleEnabled` - [x] Functional changes are covered with a test plan and automated tests. Test plan: https://github.com/elastic/security-team/pull/9045 - [x] Stability of new and changed tests is verified using the [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner). Cypress ESS: https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5547 Cypress Serverless: https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5548 FTR ESS: https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5596 FTR Serverless: https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5597 - [ ] Comprehensive manual testing is done by two engineers: the PR author and one of the PR reviewers. Changes are tested in both ESS and Serverless. - [x] Mapping changes are accompanied by a technical design document. It can be a GitHub issue or an RFC explaining the changes. The design document is shared with and approved by the appropriate teams and individual stakeholders. Existing AlertSuppression schema field is used for New terms rule, the one that used for Query and IM rules. ```yml alert_suppression: $ref: './common_attributes.schema.yaml#/components/schemas/AlertSuppression' ``` where ```yml AlertSuppression: type: object properties: group_by: $ref: '#/components/schemas/AlertSuppressionGroupBy' duration: $ref: '#/components/schemas/AlertSuppressionDuration' missing_fields_strategy: $ref: '#/components/schemas/AlertSuppressionMissingFieldsStrategy' required: - group_by ``` - [x] Functional changes are communicated to the Docs team. A ticket or PR is opened in https://github.com/elastic/security-docs. The following information is included: any feature flags used, affected environments (Serverless, ESS, or both). https://github.com/elastic/security-docs/issues/5030
This commit is contained in:
parent
42e92d8749
commit
52cfdd6fc2
65 changed files with 4079 additions and 503 deletions
|
@ -6811,6 +6811,52 @@ Object {
|
|||
},
|
||||
Object {
|
||||
"properties": Object {
|
||||
"alertSuppression": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"duration": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"unit": Object {
|
||||
"enum": Array [
|
||||
"s",
|
||||
"m",
|
||||
"h",
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
"value": Object {
|
||||
"minimum": 1,
|
||||
"type": "integer",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"value",
|
||||
"unit",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"groupBy": Object {
|
||||
"items": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"maxItems": 3,
|
||||
"minItems": 1,
|
||||
"type": "array",
|
||||
},
|
||||
"missingFieldsStrategy": Object {
|
||||
"enum": Array [
|
||||
"doNotSuppress",
|
||||
"suppress",
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"groupBy",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"dataViewId": Object {
|
||||
"type": "string",
|
||||
},
|
||||
|
|
|
@ -361,7 +361,8 @@ export const createPersistenceRuleTypeWrapper: CreatePersistenceRuleTypeWrapper
|
|||
suppressionWindow,
|
||||
enrichAlerts,
|
||||
currentTimeOverride,
|
||||
isRuleExecutionOnly
|
||||
isRuleExecutionOnly,
|
||||
maxAlerts
|
||||
) => {
|
||||
const ruleDataClientWriter = await ruleDataClient.getWriter({
|
||||
namespace: options.spaceId,
|
||||
|
@ -376,6 +377,8 @@ export const createPersistenceRuleTypeWrapper: CreatePersistenceRuleTypeWrapper
|
|||
const writeAlerts =
|
||||
ruleDataClient.isWriteEnabled() && options.services.shouldWriteAlerts();
|
||||
|
||||
let alertsWereTruncated = false;
|
||||
|
||||
if (writeAlerts && alerts.length > 0) {
|
||||
const suppressionWindowStart = dateMath.parse(suppressionWindow, {
|
||||
forceNow: currentTimeOverride,
|
||||
|
@ -392,7 +395,12 @@ export const createPersistenceRuleTypeWrapper: CreatePersistenceRuleTypeWrapper
|
|||
});
|
||||
|
||||
if (filteredDuplicates.length === 0) {
|
||||
return { createdAlerts: [], errors: {}, suppressedAlerts: [] };
|
||||
return {
|
||||
createdAlerts: [],
|
||||
errors: {},
|
||||
suppressedAlerts: [],
|
||||
alertsWereTruncated,
|
||||
};
|
||||
}
|
||||
|
||||
const suppressionAlertSearchRequest = {
|
||||
|
@ -473,7 +481,12 @@ export const createPersistenceRuleTypeWrapper: CreatePersistenceRuleTypeWrapper
|
|||
});
|
||||
|
||||
if (nonSuppressedAlerts.length === 0) {
|
||||
return { createdAlerts: [], errors: {}, suppressedAlerts: [] };
|
||||
return {
|
||||
createdAlerts: [],
|
||||
errors: {},
|
||||
suppressedAlerts: [],
|
||||
alertsWereTruncated,
|
||||
};
|
||||
}
|
||||
|
||||
const { alertCandidates, suppressedAlerts: suppressedInMemoryAlerts } =
|
||||
|
@ -535,6 +548,11 @@ export const createPersistenceRuleTypeWrapper: CreatePersistenceRuleTypeWrapper
|
|||
}
|
||||
}
|
||||
|
||||
if (maxAlerts && enrichedAlerts.length > maxAlerts) {
|
||||
enrichedAlerts.length = maxAlerts;
|
||||
alertsWereTruncated = true;
|
||||
}
|
||||
|
||||
const augmentedAlerts = augmentAlerts({
|
||||
alerts: enrichedAlerts,
|
||||
options,
|
||||
|
@ -548,7 +566,12 @@ export const createPersistenceRuleTypeWrapper: CreatePersistenceRuleTypeWrapper
|
|||
});
|
||||
|
||||
if (bulkResponse == null) {
|
||||
return { createdAlerts: [], errors: {}, suppressedAlerts: [] };
|
||||
return {
|
||||
createdAlerts: [],
|
||||
errors: {},
|
||||
suppressedAlerts: [],
|
||||
alertsWereTruncated: false,
|
||||
};
|
||||
}
|
||||
|
||||
const createdAlerts = augmentedAlerts
|
||||
|
@ -594,10 +617,16 @@ export const createPersistenceRuleTypeWrapper: CreatePersistenceRuleTypeWrapper
|
|||
createdAlerts,
|
||||
suppressedAlerts: [...duplicateAlerts, ...suppressedInMemoryAlerts],
|
||||
errors: errorAggregator(bulkResponse.body, [409]),
|
||||
alertsWereTruncated,
|
||||
};
|
||||
} else {
|
||||
logger.debug('Writing is disabled.');
|
||||
return { createdAlerts: [], errors: {}, suppressedAlerts: [] };
|
||||
return {
|
||||
createdAlerts: [],
|
||||
errors: {},
|
||||
suppressedAlerts: [],
|
||||
alertsWereTruncated: false,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -52,11 +52,11 @@ export type SuppressedAlertService = <T extends SuppressionFieldsLatest>(
|
|||
params: { spaceId: string }
|
||||
) => Promise<Array<{ _id: string; _source: T }>>,
|
||||
currentTimeOverride?: Date,
|
||||
isRuleExecutionOnly?: boolean
|
||||
isRuleExecutionOnly?: boolean,
|
||||
maxAlerts?: number
|
||||
) => Promise<SuppressedAlertServiceResult<T>>;
|
||||
|
||||
export interface SuppressedAlertServiceResult<T>
|
||||
extends Omit<PersistenceAlertServiceResult<T>, 'alertsWereTruncated'> {
|
||||
export interface SuppressedAlertServiceResult<T> extends PersistenceAlertServiceResult<T> {
|
||||
suppressedAlerts: Array<{ _id: string; _source: T }>;
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import { getListArrayMock } from '../../../../detection_engine/schemas/types/lis
|
|||
import {
|
||||
getCreateEsqlRulesSchemaMock,
|
||||
getCreateMachineLearningRulesSchemaMock,
|
||||
getCreateNewTermsRulesSchemaMock,
|
||||
getCreateRulesSchemaMock,
|
||||
getCreateRulesSchemaMockWithDataView,
|
||||
getCreateSavedQueryRulesSchemaMock,
|
||||
|
@ -1267,6 +1268,7 @@ describe('rules schema', () => {
|
|||
{ ruleType: 'threat_match', ruleMock: getCreateThreatMatchRulesSchemaMock() },
|
||||
{ ruleType: 'query', ruleMock: getCreateRulesSchemaMock() },
|
||||
{ ruleType: 'saved_query', ruleMock: getCreateSavedQueryRulesSchemaMock() },
|
||||
{ ruleType: 'new_terms', ruleMock: getCreateNewTermsRulesSchemaMock() },
|
||||
];
|
||||
|
||||
cases.forEach(({ ruleType, ruleMock }) => {
|
||||
|
|
|
@ -504,6 +504,7 @@ export const NewTermsRuleOptionalFields = z.object({
|
|||
index: IndexPatternArray.optional(),
|
||||
data_view_id: DataViewId.optional(),
|
||||
filters: RuleFilterArray.optional(),
|
||||
alert_suppression: AlertSuppression.optional(),
|
||||
});
|
||||
|
||||
export type NewTermsRuleDefaultableFields = z.infer<typeof NewTermsRuleDefaultableFields>;
|
||||
|
|
|
@ -737,6 +737,8 @@ components:
|
|||
$ref: './common_attributes.schema.yaml#/components/schemas/DataViewId'
|
||||
filters:
|
||||
$ref: './common_attributes.schema.yaml#/components/schemas/RuleFilterArray'
|
||||
alert_suppression:
|
||||
$ref: './common_attributes.schema.yaml#/components/schemas/AlertSuppression'
|
||||
|
||||
NewTermsRuleDefaultableFields:
|
||||
type: object
|
||||
|
|
|
@ -43,5 +43,6 @@ export const SUPPRESSIBLE_ALERT_RULES: Type[] = [
|
|||
'threshold',
|
||||
'saved_query',
|
||||
'query',
|
||||
'new_terms',
|
||||
'threat_match',
|
||||
];
|
||||
|
|
|
@ -231,11 +231,11 @@ describe('Alert Suppression Rules', () => {
|
|||
expect(isSuppressibleAlertRule('saved_query')).toBe(true);
|
||||
expect(isSuppressibleAlertRule('query')).toBe(true);
|
||||
expect(isSuppressibleAlertRule('threat_match')).toBe(true);
|
||||
expect(isSuppressibleAlertRule('new_terms')).toBe(true);
|
||||
|
||||
// Rule types that don't support alert suppression:
|
||||
expect(isSuppressibleAlertRule('eql')).toBe(false);
|
||||
expect(isSuppressibleAlertRule('machine_learning')).toBe(false);
|
||||
expect(isSuppressibleAlertRule('new_terms')).toBe(false);
|
||||
expect(isSuppressibleAlertRule('esql')).toBe(false);
|
||||
});
|
||||
|
||||
|
@ -253,11 +253,11 @@ describe('Alert Suppression Rules', () => {
|
|||
expect(isSuppressionRuleConfiguredWithDuration('saved_query')).toBe(true);
|
||||
expect(isSuppressionRuleConfiguredWithDuration('query')).toBe(true);
|
||||
expect(isSuppressionRuleConfiguredWithDuration('threat_match')).toBe(true);
|
||||
expect(isSuppressionRuleConfiguredWithDuration('new_terms')).toBe(true);
|
||||
|
||||
// Rule types that don't support alert suppression:
|
||||
expect(isSuppressionRuleConfiguredWithDuration('eql')).toBe(false);
|
||||
expect(isSuppressionRuleConfiguredWithDuration('machine_learning')).toBe(false);
|
||||
expect(isSuppressionRuleConfiguredWithDuration('new_terms')).toBe(false);
|
||||
expect(isSuppressionRuleConfiguredWithDuration('esql')).toBe(false);
|
||||
});
|
||||
|
||||
|
@ -274,11 +274,11 @@ describe('Alert Suppression Rules', () => {
|
|||
expect(isSuppressionRuleConfiguredWithGroupBy('saved_query')).toBe(true);
|
||||
expect(isSuppressionRuleConfiguredWithGroupBy('query')).toBe(true);
|
||||
expect(isSuppressionRuleConfiguredWithGroupBy('threat_match')).toBe(true);
|
||||
expect(isSuppressionRuleConfiguredWithGroupBy('new_terms')).toBe(true);
|
||||
|
||||
// Rule types that don't support alert suppression:
|
||||
expect(isSuppressionRuleConfiguredWithGroupBy('eql')).toBe(false);
|
||||
expect(isSuppressionRuleConfiguredWithGroupBy('machine_learning')).toBe(false);
|
||||
expect(isSuppressionRuleConfiguredWithGroupBy('new_terms')).toBe(false);
|
||||
expect(isSuppressionRuleConfiguredWithGroupBy('esql')).toBe(false);
|
||||
});
|
||||
|
||||
|
@ -300,11 +300,11 @@ describe('Alert Suppression Rules', () => {
|
|||
expect(isSuppressionRuleConfiguredWithMissingFields('saved_query')).toBe(true);
|
||||
expect(isSuppressionRuleConfiguredWithMissingFields('query')).toBe(true);
|
||||
expect(isSuppressionRuleConfiguredWithMissingFields('threat_match')).toBe(true);
|
||||
expect(isSuppressionRuleConfiguredWithMissingFields('new_terms')).toBe(true);
|
||||
|
||||
// Rule types that don't support alert suppression:
|
||||
expect(isSuppressionRuleConfiguredWithMissingFields('eql')).toBe(false);
|
||||
expect(isSuppressionRuleConfiguredWithMissingFields('machine_learning')).toBe(false);
|
||||
expect(isSuppressionRuleConfiguredWithMissingFields('new_terms')).toBe(false);
|
||||
expect(isSuppressionRuleConfiguredWithMissingFields('esql')).toBe(false);
|
||||
});
|
||||
|
||||
|
|
|
@ -170,6 +170,11 @@ export const allowedExperimentalValues = Object.freeze({
|
|||
*/
|
||||
riskEnginePrivilegesRouteEnabled: true,
|
||||
|
||||
/**
|
||||
* Enables alerts suppression for new terms rules
|
||||
*/
|
||||
alertSuppressionForNewTermsRuleEnabled: false,
|
||||
|
||||
/**
|
||||
* Enables experimental Experimental S1 integration data to be available in Analyzer
|
||||
*/
|
||||
|
|
|
@ -575,7 +575,7 @@ describe('description_step', () => {
|
|||
});
|
||||
|
||||
describe('alert suppression', () => {
|
||||
const ruleTypesWithoutSuppression: Type[] = ['eql', 'esql', 'machine_learning', 'new_terms'];
|
||||
const ruleTypesWithoutSuppression: Type[] = ['eql', 'esql', 'machine_learning'];
|
||||
const suppressionFields = {
|
||||
groupByDuration: {
|
||||
unit: 'm',
|
||||
|
|
|
@ -19,9 +19,9 @@ import {
|
|||
import {
|
||||
isEsqlRule,
|
||||
isNewTermsRule,
|
||||
isQueryRule,
|
||||
isThreatMatchRule,
|
||||
isThresholdRule,
|
||||
isSuppressionRuleConfiguredWithGroupBy,
|
||||
} from '../../../../../common/detection_engine/utils';
|
||||
import { MAX_NUMBER_OF_NEW_TERMS_FIELDS } from '../../../../../common/constants';
|
||||
import { isMlRule } from '../../../../../common/machine_learning/helpers';
|
||||
|
@ -574,79 +574,6 @@ export const schema: FormSchema<DefineStepRule> = {
|
|||
},
|
||||
],
|
||||
},
|
||||
groupByFields: {
|
||||
type: FIELD_TYPES.COMBO_BOX,
|
||||
label: i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByFieldsLabel',
|
||||
{
|
||||
defaultMessage: 'Suppress alerts by',
|
||||
}
|
||||
),
|
||||
labelAppend: (
|
||||
<EuiText color="subdued" size="xs">
|
||||
{i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByFieldsLabelAppend',
|
||||
{
|
||||
defaultMessage: 'Optional (Technical Preview)',
|
||||
}
|
||||
)}
|
||||
</EuiText>
|
||||
),
|
||||
helpText: i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldGroupByFieldHelpText',
|
||||
{
|
||||
defaultMessage: 'Select field(s) to use for suppressing extra alerts',
|
||||
}
|
||||
),
|
||||
validations: [
|
||||
{
|
||||
validator: (
|
||||
...args: Parameters<ValidationFunc>
|
||||
): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => {
|
||||
const [{ formData }] = args;
|
||||
const needsValidation =
|
||||
isQueryRule(formData.ruleType) || isThreatMatchRule(formData.ruleType);
|
||||
if (!needsValidation) {
|
||||
return;
|
||||
}
|
||||
return fieldValidators.maxLengthField({
|
||||
length: 3,
|
||||
message: i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.validations.stepDefineRule.groupByFieldsMax',
|
||||
{
|
||||
defaultMessage: 'Number of grouping fields must be at most 3',
|
||||
}
|
||||
),
|
||||
})(...args);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
groupByRadioSelection: {},
|
||||
groupByDuration: {
|
||||
label: i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByDurationValueLabel',
|
||||
{
|
||||
defaultMessage: 'Suppress alerts for',
|
||||
}
|
||||
),
|
||||
helpText: i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldGroupByDurationValueHelpText',
|
||||
{
|
||||
defaultMessage: 'Suppress alerts for',
|
||||
}
|
||||
),
|
||||
value: {},
|
||||
unit: {},
|
||||
},
|
||||
suppressionMissingFields: {
|
||||
label: i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.suppressionMissingFieldsLabel',
|
||||
{
|
||||
defaultMessage: 'If a suppression field is missing',
|
||||
}
|
||||
),
|
||||
},
|
||||
newTermsFields: {
|
||||
type: FIELD_TYPES.COMBO_BOX,
|
||||
label: i18n.translate(
|
||||
|
@ -718,6 +645,79 @@ export const schema: FormSchema<DefineStepRule> = {
|
|||
}
|
||||
),
|
||||
},
|
||||
groupByFields: {
|
||||
type: FIELD_TYPES.COMBO_BOX,
|
||||
label: i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByFieldsLabel',
|
||||
{
|
||||
defaultMessage: 'Suppress alerts by',
|
||||
}
|
||||
),
|
||||
labelAppend: (
|
||||
<EuiText color="subdued" size="xs">
|
||||
{i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByFieldsLabelAppend',
|
||||
{
|
||||
defaultMessage: 'Optional (Technical Preview)',
|
||||
}
|
||||
)}
|
||||
</EuiText>
|
||||
),
|
||||
helpText: i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldGroupByFieldHelpText',
|
||||
{
|
||||
defaultMessage: 'Select field(s) to use for suppressing extra alerts',
|
||||
}
|
||||
),
|
||||
validations: [
|
||||
{
|
||||
validator: (
|
||||
...args: Parameters<ValidationFunc>
|
||||
): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => {
|
||||
const [{ formData }] = args;
|
||||
const needsValidation = isSuppressionRuleConfiguredWithGroupBy(formData.ruleType);
|
||||
|
||||
if (!needsValidation) {
|
||||
return;
|
||||
}
|
||||
return fieldValidators.maxLengthField({
|
||||
length: 3,
|
||||
message: i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.validations.stepDefineRule.groupByFieldsMax',
|
||||
{
|
||||
defaultMessage: 'Number of grouping fields must be at most 3',
|
||||
}
|
||||
),
|
||||
})(...args);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
groupByRadioSelection: {},
|
||||
groupByDuration: {
|
||||
label: i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByDurationValueLabel',
|
||||
{
|
||||
defaultMessage: 'Suppress alerts for',
|
||||
}
|
||||
),
|
||||
helpText: i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldGroupByDurationValueHelpText',
|
||||
{
|
||||
defaultMessage: 'Suppress alerts for',
|
||||
}
|
||||
),
|
||||
value: {},
|
||||
unit: {},
|
||||
},
|
||||
suppressionMissingFields: {
|
||||
label: i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.suppressionMissingFieldsLabel',
|
||||
{
|
||||
defaultMessage: 'If a suppression field is missing',
|
||||
}
|
||||
),
|
||||
},
|
||||
shouldLoadQueryDynamically: {
|
||||
type: FIELD_TYPES.CHECKBOX,
|
||||
defaultValue: false,
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
import { useCallback } from 'react';
|
||||
import type { DefineStepRule } from '../../../../detections/pages/detection_engine/rules/types';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
import { isNewTermsRule } from '../../../../../common/detection_engine/utils';
|
||||
|
||||
/**
|
||||
* transforms DefineStepRule fields according to experimental feature flags
|
||||
|
@ -14,9 +16,31 @@ import type { DefineStepRule } from '../../../../detections/pages/detection_engi
|
|||
export const useExperimentalFeatureFieldsTransform = <T extends Partial<DefineStepRule>>(): ((
|
||||
fields: T
|
||||
) => T) => {
|
||||
const transformer = useCallback((fields: T) => {
|
||||
return fields;
|
||||
}, []);
|
||||
const isAlertSuppressionForNewTermsRuleEnabled = useIsExperimentalFeatureEnabled(
|
||||
'alertSuppressionForNewTermsRuleEnabled'
|
||||
);
|
||||
|
||||
const transformer = useCallback(
|
||||
(fields: T) => {
|
||||
const isNewTermsSuppressionDisabled = isNewTermsRule(fields.ruleType)
|
||||
? !isAlertSuppressionForNewTermsRuleEnabled
|
||||
: false;
|
||||
|
||||
// reset any alert suppression values hidden behind feature flag
|
||||
if (isNewTermsSuppressionDisabled) {
|
||||
return {
|
||||
...fields,
|
||||
groupByFields: [],
|
||||
groupByRadioSelection: undefined,
|
||||
groupByDuration: undefined,
|
||||
suppressionMissingFields: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return fields;
|
||||
},
|
||||
[isAlertSuppressionForNewTermsRuleEnabled]
|
||||
);
|
||||
|
||||
return transformer;
|
||||
};
|
||||
|
|
|
@ -490,6 +490,7 @@ export const formatDefineStepData = (defineStepData: DefineStepRule): DefineStep
|
|||
query: ruleFields.queryBar?.query?.query as string,
|
||||
new_terms_fields: ruleFields.newTermsFields,
|
||||
history_window_start: `now-${ruleFields.historyWindowSize}`,
|
||||
...alertSuppressionFields,
|
||||
}
|
||||
: isEsqlFields(ruleFields) && !('index' in ruleFields)
|
||||
? {
|
||||
|
|
|
@ -645,6 +645,28 @@ const prepareDefinitionSectionListItems = (
|
|||
});
|
||||
}
|
||||
|
||||
if ('new_terms_fields' in rule && rule.new_terms_fields && rule.new_terms_fields.length > 0) {
|
||||
definitionSectionListItems.push({
|
||||
title: (
|
||||
<span data-test-subj="newTermsFieldsPropertyTitle">
|
||||
{i18n.NEW_TERMS_FIELDS_FIELD_LABEL}
|
||||
</span>
|
||||
),
|
||||
description: <NewTermsFields newTermsFields={rule.new_terms_fields} />,
|
||||
});
|
||||
}
|
||||
|
||||
if ('history_window_start' in rule) {
|
||||
definitionSectionListItems.push({
|
||||
title: (
|
||||
<span data-test-subj="newTermsWindowSizePropertyTitle">
|
||||
{i18n.HISTORY_WINDOW_SIZE_FIELD_LABEL}
|
||||
</span>
|
||||
),
|
||||
description: <HistoryWindowSize historyWindowStart={rule.history_window_start} />,
|
||||
});
|
||||
}
|
||||
|
||||
if (isSuppressionEnabled && 'alert_suppression' in rule && rule.alert_suppression) {
|
||||
if ('group_by' in rule.alert_suppression) {
|
||||
definitionSectionListItems.push({
|
||||
|
@ -682,28 +704,6 @@ const prepareDefinitionSectionListItems = (
|
|||
}
|
||||
}
|
||||
|
||||
if ('new_terms_fields' in rule && rule.new_terms_fields && rule.new_terms_fields.length > 0) {
|
||||
definitionSectionListItems.push({
|
||||
title: (
|
||||
<span data-test-subj="newTermsFieldsPropertyTitle">
|
||||
{i18n.NEW_TERMS_FIELDS_FIELD_LABEL}
|
||||
</span>
|
||||
),
|
||||
description: <NewTermsFields newTermsFields={rule.new_terms_fields} />,
|
||||
});
|
||||
}
|
||||
|
||||
if ('history_window_start' in rule) {
|
||||
definitionSectionListItems.push({
|
||||
title: (
|
||||
<span data-test-subj="newTermsWindowSizePropertyTitle">
|
||||
{i18n.HISTORY_WINDOW_SIZE_FIELD_LABEL}
|
||||
</span>
|
||||
),
|
||||
description: <HistoryWindowSize historyWindowStart={rule.history_window_start} />,
|
||||
});
|
||||
}
|
||||
|
||||
return definitionSectionListItems;
|
||||
};
|
||||
|
||||
|
|
|
@ -7,8 +7,28 @@
|
|||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import type { Type } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import { useAlertSuppression } from './use_alert_suppression';
|
||||
import * as useIsExperimentalFeatureEnabledMock from '../../../common/hooks/use_experimental_features';
|
||||
|
||||
jest
|
||||
.spyOn(useIsExperimentalFeatureEnabledMock, 'useIsExperimentalFeatureEnabled')
|
||||
.mockReturnValue(false);
|
||||
|
||||
describe('useAlertSuppression', () => {
|
||||
it('should return isSuppressionEnabled false if rule Type exists in SUPPRESSIBLE_ALERT_RULES and Feature Flag is disabled', () => {
|
||||
const { result } = renderHook(() => useAlertSuppression('new_terms'));
|
||||
|
||||
expect(result.current.isSuppressionEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should return isSuppressionEnabled true if rule Type exists in SUPPRESSIBLE_ALERT_RULES and Feature Flag is enabled', () => {
|
||||
jest
|
||||
.spyOn(useIsExperimentalFeatureEnabledMock, 'useIsExperimentalFeatureEnabled')
|
||||
.mockImplementationOnce(() => true);
|
||||
const { result } = renderHook(() => useAlertSuppression('new_terms'));
|
||||
|
||||
expect(result.current.isSuppressionEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should return the correct isSuppressionEnabled value fot threat_match rule type', () => {
|
||||
const { result } = renderHook(() => useAlertSuppression('threat_match'));
|
||||
|
||||
|
|
|
@ -7,17 +7,29 @@
|
|||
import { useCallback } from 'react';
|
||||
import type { Type } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import { isSuppressibleAlertRule } from '../../../../common/detection_engine/utils';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
|
||||
|
||||
export interface UseAlertSuppressionReturn {
|
||||
isSuppressionEnabled: boolean;
|
||||
}
|
||||
|
||||
export const useAlertSuppression = (ruleType: Type | undefined): UseAlertSuppressionReturn => {
|
||||
const isAlertSuppressionForNewTermsRuleEnabled = useIsExperimentalFeatureEnabled(
|
||||
'alertSuppressionForNewTermsRuleEnabled'
|
||||
);
|
||||
|
||||
const isSuppressionEnabledForRuleType = useCallback(() => {
|
||||
if (!ruleType) return false;
|
||||
if (!ruleType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove this condition when the Feature Flag for enabling Suppression in the New terms rule is removed.
|
||||
if (ruleType === 'new_terms') {
|
||||
return isSuppressibleAlertRule(ruleType) && isAlertSuppressionForNewTermsRuleEnabled;
|
||||
}
|
||||
|
||||
return isSuppressibleAlertRule(ruleType);
|
||||
}, [ruleType]);
|
||||
}, [ruleType, isAlertSuppressionForNewTermsRuleEnabled]);
|
||||
|
||||
return {
|
||||
isSuppressionEnabled: isSuppressionEnabledForRuleType(),
|
||||
|
|
|
@ -194,6 +194,27 @@ describe('rule_converters', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('should accept new_terms alerts suppression params', () => {
|
||||
const patchParams = {
|
||||
alert_suppression: {
|
||||
group_by: ['agent.name'],
|
||||
duration: { value: 4, unit: 'h' as const },
|
||||
missing_fields_strategy: 'suppress' as const,
|
||||
},
|
||||
};
|
||||
const rule = getNewTermsRuleParams();
|
||||
const patchedParams = patchTypeSpecificSnakeToCamel(patchParams, rule);
|
||||
expect(patchedParams).toEqual(
|
||||
expect.objectContaining({
|
||||
alertSuppression: {
|
||||
groupBy: ['agent.name'],
|
||||
missingFieldsStrategy: 'suppress',
|
||||
duration: { value: 4, unit: 'h' },
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should accept machine learning params when existing rule type is machine learning', () => {
|
||||
const patchParams = {
|
||||
anomaly_threshold: 5,
|
||||
|
|
|
@ -199,6 +199,7 @@ export const typeSpecificSnakeToCamel = (
|
|||
filters: params.filters,
|
||||
language: params.language ?? 'kuery',
|
||||
dataViewId: params.data_view_id,
|
||||
alertSuppression: convertAlertSuppressionToCamel(params.alert_suppression),
|
||||
};
|
||||
}
|
||||
default: {
|
||||
|
@ -345,6 +346,8 @@ const patchNewTermsParams = (
|
|||
filters: params.filters ?? existingRule.filters,
|
||||
newTermsFields: params.new_terms_fields ?? existingRule.newTermsFields,
|
||||
historyWindowStart: params.history_window_start ?? existingRule.historyWindowStart,
|
||||
alertSuppression:
|
||||
convertAlertSuppressionToCamel(params.alert_suppression) ?? existingRule.alertSuppression,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -643,6 +646,7 @@ export const typeSpecificCamelToSnake = (
|
|||
filters: params.filters,
|
||||
language: params.language,
|
||||
data_view_id: params.dataViewId,
|
||||
alert_suppression: convertAlertSuppressionToSnake(params.alertSuppression),
|
||||
};
|
||||
}
|
||||
default: {
|
||||
|
|
|
@ -268,6 +268,7 @@ export const NewTermsSpecificRuleParams = z.object({
|
|||
filters: RuleFilterArray.optional(),
|
||||
language: KqlQueryLanguage,
|
||||
dataViewId: DataViewId.optional(),
|
||||
alertSuppression: AlertSuppressionCamel.optional(),
|
||||
});
|
||||
|
||||
export type NewTermsRuleParams = BaseRuleParams & NewTermsSpecificRuleParams;
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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 { SuppressedAlertService } from '@kbn/rule-registry-plugin/server';
|
||||
|
||||
import type { SuppressionFieldsLatest } from '@kbn/rule-registry-plugin/common/schemas';
|
||||
import type {
|
||||
SearchAfterAndBulkCreateParams,
|
||||
SearchAfterAndBulkCreateReturnType,
|
||||
WrapSuppressedHits,
|
||||
} from '../types';
|
||||
|
||||
import type { AlertSuppressionCamel } from '../../../../../common/api/detection_engine/model/rule_schema';
|
||||
import { DEFAULT_SUPPRESSION_MISSING_FIELDS_STRATEGY } from '../../../../../common/detection_engine/constants';
|
||||
import { AlertSuppressionMissingFieldsStrategyEnum } from '../../../../../common/api/detection_engine/model/rule_schema';
|
||||
|
||||
import { executeBulkCreateAlerts } from '../utils/bulk_create_suppressed_alerts_in_memory';
|
||||
import type {
|
||||
BaseFieldsLatest,
|
||||
WrappedFieldsLatest,
|
||||
NewTermsFieldsLatest,
|
||||
} from '../../../../../common/api/detection_engine/model/alerts';
|
||||
import { partitionMissingFieldsEvents } from '../utils/partition_missing_fields_events';
|
||||
import type { EventsAndTerms } from './types';
|
||||
import type { ExperimentalFeatures } from '../../../../../common';
|
||||
|
||||
interface SearchAfterAndBulkCreateSuppressedAlertsParams extends SearchAfterAndBulkCreateParams {
|
||||
wrapSuppressedHits: WrapSuppressedHits;
|
||||
alertTimestampOverride: Date | undefined;
|
||||
alertWithSuppression: SuppressedAlertService;
|
||||
alertSuppression?: AlertSuppressionCamel;
|
||||
}
|
||||
export interface BulkCreateSuppressedAlertsParams
|
||||
extends Pick<
|
||||
SearchAfterAndBulkCreateSuppressedAlertsParams,
|
||||
| 'bulkCreate'
|
||||
| 'services'
|
||||
| 'ruleExecutionLogger'
|
||||
| 'tuple'
|
||||
| 'alertSuppression'
|
||||
| 'alertWithSuppression'
|
||||
| 'alertTimestampOverride'
|
||||
> {
|
||||
wrapHits: (
|
||||
events: EventsAndTerms[]
|
||||
) => Array<WrappedFieldsLatest<BaseFieldsLatest & NewTermsFieldsLatest>>;
|
||||
wrapSuppressedHits: (
|
||||
events: EventsAndTerms[]
|
||||
) => Array<
|
||||
WrappedFieldsLatest<BaseFieldsLatest & SuppressionFieldsLatest & NewTermsFieldsLatest>
|
||||
>;
|
||||
eventsAndTerms: EventsAndTerms[];
|
||||
toReturn: SearchAfterAndBulkCreateReturnType;
|
||||
experimentalFeatures: ExperimentalFeatures | undefined;
|
||||
}
|
||||
/**
|
||||
* wraps, bulk create and suppress alerts in memory, also takes care of missing fields logic.
|
||||
* If parameter alertSuppression.missingFieldsStrategy configured not to be suppressed, regular alerts will be created for such events without suppression
|
||||
* This function is similar to x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/bulk_create_suppressed_alerts_in_memory.ts, but
|
||||
* it operates with new terms specific eventsAndTerms{@link EventsAndTerms} parameter property, instead of regular events as common utility
|
||||
*/
|
||||
export const bulkCreateSuppressedNewTermsAlertsInMemory = async ({
|
||||
eventsAndTerms,
|
||||
wrapHits,
|
||||
wrapSuppressedHits,
|
||||
toReturn,
|
||||
bulkCreate,
|
||||
services,
|
||||
ruleExecutionLogger,
|
||||
tuple,
|
||||
alertSuppression,
|
||||
alertWithSuppression,
|
||||
alertTimestampOverride,
|
||||
experimentalFeatures,
|
||||
}: BulkCreateSuppressedAlertsParams) => {
|
||||
const suppressOnMissingFields =
|
||||
(alertSuppression?.missingFieldsStrategy ?? DEFAULT_SUPPRESSION_MISSING_FIELDS_STRATEGY) ===
|
||||
AlertSuppressionMissingFieldsStrategyEnum.suppress;
|
||||
|
||||
let suppressibleEvents = eventsAndTerms;
|
||||
let unsuppressibleWrappedDocs: Array<WrappedFieldsLatest<BaseFieldsLatest>> = [];
|
||||
|
||||
if (!suppressOnMissingFields) {
|
||||
const partitionedEvents = partitionMissingFieldsEvents(
|
||||
eventsAndTerms,
|
||||
alertSuppression?.groupBy || [],
|
||||
['event']
|
||||
);
|
||||
|
||||
unsuppressibleWrappedDocs = wrapHits(partitionedEvents[1]);
|
||||
|
||||
suppressibleEvents = partitionedEvents[0];
|
||||
}
|
||||
|
||||
const suppressibleWrappedDocs = wrapSuppressedHits(suppressibleEvents);
|
||||
|
||||
return executeBulkCreateAlerts({
|
||||
suppressibleWrappedDocs,
|
||||
unsuppressibleWrappedDocs,
|
||||
toReturn,
|
||||
bulkCreate,
|
||||
services,
|
||||
ruleExecutionLogger,
|
||||
tuple,
|
||||
alertSuppression,
|
||||
alertWithSuppression,
|
||||
alertTimestampOverride,
|
||||
experimentalFeatures,
|
||||
});
|
||||
};
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isObject } from 'lodash';
|
||||
import { isObject, chunk } from 'lodash';
|
||||
|
||||
import { NEW_TERMS_RULE_TYPE_ID } from '@kbn/securitysolution-rules';
|
||||
import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common';
|
||||
|
@ -16,13 +16,16 @@ import type { CreateRuleOptions, SecurityAlertType } from '../types';
|
|||
import { singleSearchAfter } from '../utils/single_search_after';
|
||||
import { getFilter } from '../utils/get_filter';
|
||||
import { wrapNewTermsAlerts } from './wrap_new_terms_alerts';
|
||||
import type { EventsAndTerms } from './wrap_new_terms_alerts';
|
||||
import { wrapSuppressedNewTermsAlerts } from './wrap_suppressed_new_terms_alerts';
|
||||
import { bulkCreateSuppressedNewTermsAlertsInMemory } from './bulk_create_suppressed_alerts_in_memory';
|
||||
import type { EventsAndTerms } from './types';
|
||||
import type {
|
||||
RecentTermsAggResult,
|
||||
DocFetchAggResult,
|
||||
NewTermsAggResult,
|
||||
CreateAlertsHook,
|
||||
} from './build_new_terms_aggregation';
|
||||
import type { NewTermsFieldsLatest } from '../../../../../common/api/detection_engine/model/alerts';
|
||||
import {
|
||||
buildRecentTermsAgg,
|
||||
buildNewTermsAgg,
|
||||
|
@ -35,14 +38,17 @@ import {
|
|||
createSearchAfterReturnType,
|
||||
getUnprocessedExceptionsWarnings,
|
||||
getMaxSignalsWarning,
|
||||
getSuppressionMaxSignalsWarning,
|
||||
} from '../utils/utils';
|
||||
import { createEnrichEventsFunction } from '../utils/enrichments';
|
||||
import { getIsAlertSuppressionActive } from '../utils/get_is_alert_suppression_active';
|
||||
import { multiTermsComposite } from './multi_terms_composite';
|
||||
import type { GenericBulkCreateResponse } from '../utils/bulk_create_with_suppression';
|
||||
|
||||
export const createNewTermsAlertType = (
|
||||
createOptions: CreateRuleOptions
|
||||
): SecurityAlertType<NewTermsRuleParams, {}, {}, 'default'> => {
|
||||
const { logger } = createOptions;
|
||||
const { logger, licensing } = createOptions;
|
||||
return {
|
||||
id: NEW_TERMS_RULE_TYPE_ID,
|
||||
name: 'New Terms Rule',
|
||||
|
@ -104,6 +110,8 @@ export const createNewTermsAlertType = (
|
|||
alertTimestampOverride,
|
||||
publicBaseUrl,
|
||||
inputIndexFields,
|
||||
experimentalFeatures,
|
||||
alertWithSuppression,
|
||||
},
|
||||
services,
|
||||
params,
|
||||
|
@ -137,6 +145,11 @@ export const createNewTermsAlertType = (
|
|||
name: 'historyWindowStart',
|
||||
});
|
||||
|
||||
const isAlertSuppressionActive = await getIsAlertSuppressionActive({
|
||||
alertSuppression: params.alertSuppression,
|
||||
licensing,
|
||||
isFeatureDisabled: !experimentalFeatures?.alertSuppressionForNewTermsRuleEnabled,
|
||||
});
|
||||
let afterKey;
|
||||
|
||||
const result = createSearchAfterReturnType();
|
||||
|
@ -191,6 +204,32 @@ export const createNewTermsAlertType = (
|
|||
const bucketsForField = searchResultWithAggs.aggregations.new_terms.buckets;
|
||||
|
||||
const createAlertsHook: CreateAlertsHook = async (aggResult) => {
|
||||
const wrapHits = (eventsAndTerms: EventsAndTerms[]) =>
|
||||
wrapNewTermsAlerts({
|
||||
eventsAndTerms,
|
||||
spaceId,
|
||||
completeRule,
|
||||
mergeStrategy,
|
||||
indicesToQuery: inputIndex,
|
||||
alertTimestampOverride,
|
||||
ruleExecutionLogger,
|
||||
publicBaseUrl,
|
||||
});
|
||||
|
||||
const wrapSuppressedHits = (eventsAndTerms: EventsAndTerms[]) =>
|
||||
wrapSuppressedNewTermsAlerts({
|
||||
eventsAndTerms,
|
||||
spaceId,
|
||||
completeRule,
|
||||
mergeStrategy,
|
||||
indicesToQuery: inputIndex,
|
||||
alertTimestampOverride,
|
||||
ruleExecutionLogger,
|
||||
publicBaseUrl,
|
||||
primaryTimestamp,
|
||||
secondaryTimestamp,
|
||||
});
|
||||
|
||||
const eventsAndTerms: EventsAndTerms[] = (
|
||||
aggResult?.aggregations?.new_terms.buckets ?? []
|
||||
).map((bucket) => {
|
||||
|
@ -201,27 +240,61 @@ export const createNewTermsAlertType = (
|
|||
};
|
||||
});
|
||||
|
||||
const wrappedAlerts = wrapNewTermsAlerts({
|
||||
eventsAndTerms,
|
||||
spaceId,
|
||||
completeRule,
|
||||
mergeStrategy,
|
||||
indicesToQuery: inputIndex,
|
||||
alertTimestampOverride,
|
||||
ruleExecutionLogger,
|
||||
publicBaseUrl,
|
||||
});
|
||||
let bulkCreateResult: Omit<
|
||||
GenericBulkCreateResponse<NewTermsFieldsLatest>,
|
||||
'suppressedItemsCount'
|
||||
> = {
|
||||
errors: [],
|
||||
success: true,
|
||||
enrichmentDuration: '0',
|
||||
bulkCreateDuration: '0',
|
||||
createdItemsCount: 0,
|
||||
createdItems: [],
|
||||
alertsWereTruncated: false,
|
||||
};
|
||||
|
||||
const bulkCreateResult = await bulkCreate(
|
||||
wrappedAlerts,
|
||||
params.maxSignals - result.createdSignalsCount,
|
||||
createEnrichEventsFunction({
|
||||
services,
|
||||
logger: ruleExecutionLogger,
|
||||
})
|
||||
);
|
||||
// wrap and create alerts by chunks
|
||||
// large number of matches, processed in possibly 10,000 size of events and terms
|
||||
// can significantly affect Kibana performance
|
||||
const eventAndTermsChunks = chunk(eventsAndTerms, 5 * params.maxSignals);
|
||||
|
||||
addToSearchAfterReturn({ current: result, next: bulkCreateResult });
|
||||
for (let i = 0; i < eventAndTermsChunks.length; i++) {
|
||||
const eventAndTermsChunk = eventAndTermsChunks[i];
|
||||
|
||||
if (isAlertSuppressionActive) {
|
||||
bulkCreateResult = await bulkCreateSuppressedNewTermsAlertsInMemory({
|
||||
eventsAndTerms: eventAndTermsChunk,
|
||||
toReturn: result,
|
||||
wrapHits,
|
||||
bulkCreate,
|
||||
services,
|
||||
ruleExecutionLogger,
|
||||
tuple,
|
||||
alertSuppression: params.alertSuppression,
|
||||
wrapSuppressedHits,
|
||||
alertTimestampOverride,
|
||||
alertWithSuppression,
|
||||
experimentalFeatures,
|
||||
});
|
||||
} else {
|
||||
const wrappedAlerts = wrapHits(eventAndTermsChunk);
|
||||
|
||||
bulkCreateResult = await bulkCreate(
|
||||
wrappedAlerts,
|
||||
params.maxSignals - result.createdSignalsCount,
|
||||
createEnrichEventsFunction({
|
||||
services,
|
||||
logger: ruleExecutionLogger,
|
||||
})
|
||||
);
|
||||
|
||||
addToSearchAfterReturn({ current: result, next: bulkCreateResult });
|
||||
}
|
||||
|
||||
if (bulkCreateResult.alertsWereTruncated) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return bulkCreateResult;
|
||||
};
|
||||
|
@ -229,7 +302,7 @@ export const createNewTermsAlertType = (
|
|||
// separate route for multiple new terms
|
||||
// it uses paging through composite aggregation
|
||||
if (params.newTermsFields.length > 1) {
|
||||
await multiTermsComposite({
|
||||
const bulkCreateResult = await multiTermsComposite({
|
||||
filterArgs,
|
||||
buckets: bucketsForField,
|
||||
params,
|
||||
|
@ -241,7 +314,12 @@ export const createNewTermsAlertType = (
|
|||
runOpts: execOptions.runOpts,
|
||||
afterKey,
|
||||
createAlertsHook,
|
||||
isAlertSuppressionActive,
|
||||
});
|
||||
|
||||
if (bulkCreateResult?.alertsWereTruncated) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// PHASE 2: Take the page of results from Phase 1 and determine if each term exists in the history window.
|
||||
// The aggregation filters out buckets for terms that exist prior to `tuple.from`, so the buckets in the
|
||||
|
@ -326,7 +404,11 @@ export const createNewTermsAlertType = (
|
|||
const bulkCreateResult = await createAlertsHook(docFetchResultWithAggs);
|
||||
|
||||
if (bulkCreateResult.alertsWereTruncated) {
|
||||
result.warningMessages.push(getMaxSignalsWarning());
|
||||
result.warningMessages.push(
|
||||
isAlertSuppressionActive
|
||||
? getSuppressionMaxSignalsWarning()
|
||||
: getMaxSignalsWarning()
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,8 +21,9 @@ import type {
|
|||
CompositeNewTermsAggResult,
|
||||
CreateAlertsHook,
|
||||
} from './build_new_terms_aggregation';
|
||||
|
||||
import { getMaxSignalsWarning } from '../utils/utils';
|
||||
import type { NewTermsFieldsLatest } from '../../../../../common/api/detection_engine/model/alerts';
|
||||
import { getMaxSignalsWarning, getSuppressionMaxSignalsWarning } from '../utils/utils';
|
||||
import type { GenericBulkCreateResponse } from '../utils/bulk_create_with_suppression';
|
||||
|
||||
import type { RuleServices, SearchAfterAndBulkCreateReturnType, RunOpts } from '../types';
|
||||
|
||||
|
@ -47,6 +48,7 @@ interface MultiTermsCompositeArgsBase {
|
|||
runOpts: RunOpts<NewTermsRuleParams>;
|
||||
afterKey: Record<string, string | number | null> | undefined;
|
||||
createAlertsHook: CreateAlertsHook;
|
||||
isAlertSuppressionActive: boolean;
|
||||
}
|
||||
|
||||
interface MultiTermsCompositeArgs extends MultiTermsCompositeArgsBase {
|
||||
|
@ -72,7 +74,10 @@ const multiTermsCompositeNonRetryable = async ({
|
|||
afterKey,
|
||||
createAlertsHook,
|
||||
batchSize,
|
||||
}: MultiTermsCompositeArgs) => {
|
||||
isAlertSuppressionActive,
|
||||
}: MultiTermsCompositeArgs): Promise<
|
||||
Omit<GenericBulkCreateResponse<NewTermsFieldsLatest>, 'suppressedItemsCount'> | undefined
|
||||
> => {
|
||||
const {
|
||||
ruleExecutionLogger,
|
||||
tuple,
|
||||
|
@ -183,15 +188,15 @@ const multiTermsCompositeNonRetryable = async ({
|
|||
const bulkCreateResult = await createAlertsHook(docFetchResultWithAggs);
|
||||
|
||||
if (bulkCreateResult.alertsWereTruncated) {
|
||||
result.warningMessages.push(getMaxSignalsWarning());
|
||||
return result;
|
||||
result.warningMessages.push(
|
||||
isAlertSuppressionActive ? getSuppressionMaxSignalsWarning() : getMaxSignalsWarning()
|
||||
);
|
||||
return bulkCreateResult;
|
||||
}
|
||||
}
|
||||
|
||||
internalAfterKey = batch[batch.length - 1]?.key;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -199,10 +204,14 @@ const multiTermsCompositeNonRetryable = async ({
|
|||
* We will try to reduce it in twice per each request, three times, up until 125
|
||||
* Per ES documentation, max_clause_count min value is 1,000 - so with 125 we should be able execute query below max_clause_count value
|
||||
*/
|
||||
export const multiTermsComposite = async (args: MultiTermsCompositeArgsBase): Promise<void> => {
|
||||
export const multiTermsComposite = async (
|
||||
args: MultiTermsCompositeArgsBase
|
||||
): Promise<
|
||||
Omit<GenericBulkCreateResponse<NewTermsFieldsLatest>, 'suppressedItemsCount'> | undefined
|
||||
> => {
|
||||
let retryBatchSize = BATCH_SIZE;
|
||||
const ruleExecutionLogger = args.runOpts.ruleExecutionLogger;
|
||||
await pRetry(
|
||||
return pRetry(
|
||||
async (retryCount) => {
|
||||
try {
|
||||
const res = await multiTermsCompositeNonRetryable({ ...args, batchSize: retryBatchSize });
|
||||
|
@ -218,7 +227,7 @@ export const multiTermsComposite = async (args: MultiTermsCompositeArgsBase): Pr
|
|||
].some((errMessage) => e.message.includes(errMessage))
|
||||
) {
|
||||
args.result.errors.push(e.message);
|
||||
return args.result;
|
||||
return;
|
||||
}
|
||||
|
||||
retryBatchSize = retryBatchSize / 2;
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 * as estypes from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { SignalSource } from '../types';
|
||||
|
||||
export interface EventsAndTerms {
|
||||
event: estypes.SearchHit<SignalSource>;
|
||||
newTerms: Array<string | number | null>;
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* 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 { cloneDeep } from 'lodash';
|
||||
|
||||
import {
|
||||
ALERT_URL,
|
||||
ALERT_UUID,
|
||||
ALERT_SUPPRESSION_DOCS_COUNT,
|
||||
ALERT_INSTANCE_ID,
|
||||
ALERT_SUPPRESSION_TERMS,
|
||||
ALERT_SUPPRESSION_START,
|
||||
ALERT_SUPPRESSION_END,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { ALERT_NEW_TERMS } from '../../../../../common/field_maps/field_names';
|
||||
import { getCompleteRuleMock, getNewTermsRuleParams } from '../../rule_schema/mocks';
|
||||
import { ruleExecutionLogMock } from '../../rule_monitoring/mocks';
|
||||
import { sampleDocNoSortIdWithTimestamp } from '../__mocks__/es_results';
|
||||
import { wrapSuppressedNewTermsAlerts } from './wrap_suppressed_new_terms_alerts';
|
||||
|
||||
const ruleExecutionLogger = ruleExecutionLogMock.forExecutors.create();
|
||||
|
||||
const docId = 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71';
|
||||
const publicBaseUrl = 'http://somekibanabaseurl.com';
|
||||
|
||||
const alertSuppression = {
|
||||
groupBy: ['source.ip'],
|
||||
};
|
||||
|
||||
const completeRule = getCompleteRuleMock(getNewTermsRuleParams());
|
||||
completeRule.ruleParams.alertSuppression = alertSuppression;
|
||||
|
||||
describe('wrapSuppressedNewTermsAlerts', () => {
|
||||
test('should create an alert with the correct _id from a document and suppression fields', () => {
|
||||
const doc = sampleDocNoSortIdWithTimestamp(docId);
|
||||
const alerts = wrapSuppressedNewTermsAlerts({
|
||||
eventsAndTerms: [{ event: doc, newTerms: ['127.0.0.1'] }],
|
||||
spaceId: 'default',
|
||||
mergeStrategy: 'missingFields',
|
||||
completeRule,
|
||||
indicesToQuery: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
|
||||
alertTimestampOverride: undefined,
|
||||
ruleExecutionLogger,
|
||||
publicBaseUrl,
|
||||
primaryTimestamp: '@timestamp',
|
||||
});
|
||||
|
||||
expect(alerts[0]._id).toEqual('3b67aa2ebdc628afc98febc65082d2d83a116d79');
|
||||
expect(alerts[0]._source[ALERT_UUID]).toEqual('3b67aa2ebdc628afc98febc65082d2d83a116d79');
|
||||
expect(alerts[0]._source[ALERT_NEW_TERMS]).toEqual(['127.0.0.1']);
|
||||
expect(alerts[0]._source[ALERT_URL]).toContain(
|
||||
'http://somekibanabaseurl.com/app/security/alerts/redirect/3b67aa2ebdc628afc98febc65082d2d83a116d79?index=.alerts-security.alerts-default'
|
||||
);
|
||||
expect(alerts[0]._source[ALERT_SUPPRESSION_DOCS_COUNT]).toEqual(0);
|
||||
expect(alerts[0]._source[ALERT_INSTANCE_ID]).toEqual(
|
||||
'1bf77f90e72d76d9335ad0ce356340a3d9833f96'
|
||||
);
|
||||
expect(alerts[0]._source[ALERT_SUPPRESSION_TERMS]).toEqual([
|
||||
{ field: 'source.ip', value: ['127.0.0.1'] },
|
||||
]);
|
||||
expect(alerts[0]._source[ALERT_SUPPRESSION_START]).toBeDefined();
|
||||
expect(alerts[0]._source[ALERT_SUPPRESSION_END]).toBeDefined();
|
||||
});
|
||||
|
||||
test('should create an alert with a different _id if suppression field is different', () => {
|
||||
const completeRuleCloned = cloneDeep(completeRule);
|
||||
completeRuleCloned.ruleParams.alertSuppression = {
|
||||
groupBy: ['someKey'],
|
||||
};
|
||||
const doc = sampleDocNoSortIdWithTimestamp(docId);
|
||||
const alerts = wrapSuppressedNewTermsAlerts({
|
||||
eventsAndTerms: [{ event: doc, newTerms: ['127.0.0.1'] }],
|
||||
spaceId: 'default',
|
||||
mergeStrategy: 'missingFields',
|
||||
completeRule: completeRuleCloned,
|
||||
indicesToQuery: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
|
||||
alertTimestampOverride: undefined,
|
||||
ruleExecutionLogger,
|
||||
publicBaseUrl,
|
||||
primaryTimestamp: '@timestamp',
|
||||
});
|
||||
|
||||
expect(alerts[0]._id).toEqual('3e0436a03b735af12d6e5358cb36d2c3b39425a8');
|
||||
expect(alerts[0]._source[ALERT_UUID]).toEqual('3e0436a03b735af12d6e5358cb36d2c3b39425a8');
|
||||
expect(alerts[0]._source[ALERT_URL]).toContain(
|
||||
'http://somekibanabaseurl.com/app/security/alerts/redirect/3e0436a03b735af12d6e5358cb36d2c3b39425a8?index=.alerts-security.alerts-default'
|
||||
);
|
||||
expect(alerts[0]._source[ALERT_SUPPRESSION_DOCS_COUNT]).toEqual(0);
|
||||
expect(alerts[0]._source[ALERT_INSTANCE_ID]).toEqual(
|
||||
'01e43acf431fd232bbe230ac523a5d5d1e8a2787'
|
||||
);
|
||||
expect(alerts[0]._source[ALERT_SUPPRESSION_TERMS]).toEqual([
|
||||
{ field: 'someKey', value: ['someValue'] },
|
||||
]);
|
||||
});
|
||||
|
||||
test('should create an alert with a different _id if the space is different', () => {
|
||||
const doc = sampleDocNoSortIdWithTimestamp(docId);
|
||||
const alerts = wrapSuppressedNewTermsAlerts({
|
||||
eventsAndTerms: [{ event: doc, newTerms: ['127.0.0.1'] }],
|
||||
spaceId: 'otherSpace',
|
||||
mergeStrategy: 'missingFields',
|
||||
completeRule,
|
||||
indicesToQuery: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
|
||||
alertTimestampOverride: undefined,
|
||||
ruleExecutionLogger,
|
||||
publicBaseUrl,
|
||||
primaryTimestamp: '@timestamp',
|
||||
});
|
||||
|
||||
expect(alerts[0]._id).toEqual('f8a029df9c99e245dc83977153a0612178f3d2e8');
|
||||
expect(alerts[0]._source[ALERT_UUID]).toEqual('f8a029df9c99e245dc83977153a0612178f3d2e8');
|
||||
expect(alerts[0]._source[ALERT_URL]).toContain(
|
||||
'http://somekibanabaseurl.com/s/otherSpace/app/security/alerts/redirect/f8a029df9c99e245dc83977153a0612178f3d2e8?index=.alerts-security.alerts-otherSpace'
|
||||
);
|
||||
});
|
||||
|
||||
test('should create an alert with a different _id if the newTerms array is different', () => {
|
||||
const doc = sampleDocNoSortIdWithTimestamp(docId);
|
||||
const alerts = wrapSuppressedNewTermsAlerts({
|
||||
eventsAndTerms: [{ event: doc, newTerms: ['127.0.0.2'] }],
|
||||
spaceId: 'otherSpace',
|
||||
mergeStrategy: 'missingFields',
|
||||
completeRule,
|
||||
indicesToQuery: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
|
||||
alertTimestampOverride: undefined,
|
||||
ruleExecutionLogger,
|
||||
publicBaseUrl,
|
||||
primaryTimestamp: '@timestamp',
|
||||
});
|
||||
|
||||
expect(alerts[0]._id).toEqual('cb8684ec72592346d32839b1838e4f4080dc052e');
|
||||
expect(alerts[0]._source[ALERT_UUID]).toEqual('cb8684ec72592346d32839b1838e4f4080dc052e');
|
||||
expect(alerts[0]._source[ALERT_NEW_TERMS]).toEqual(['127.0.0.2']);
|
||||
expect(alerts[0]._source[ALERT_URL]).toContain(
|
||||
'http://somekibanabaseurl.com/s/otherSpace/app/security/alerts/redirect/cb8684ec72592346d32839b1838e4f4080dc052e?index=.alerts-security.alerts-otherSpace'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* 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 * as estypes from '@elastic/elasticsearch/lib/api/types';
|
||||
import objectHash from 'object-hash';
|
||||
import { TIMESTAMP } from '@kbn/rule-data-utils';
|
||||
import type { SuppressionFieldsLatest } from '@kbn/rule-registry-plugin/common/schemas';
|
||||
import type {
|
||||
BaseFieldsLatest,
|
||||
NewTermsFieldsLatest,
|
||||
WrappedFieldsLatest,
|
||||
} from '../../../../../common/api/detection_engine/model/alerts';
|
||||
import { ALERT_NEW_TERMS } from '../../../../../common/field_maps/field_names';
|
||||
import type { ConfigType } from '../../../../config';
|
||||
import type { CompleteRule, NewTermsRuleParams } from '../../rule_schema';
|
||||
import { buildReasonMessageForNewTermsAlert } from '../utils/reason_formatters';
|
||||
import { getSuppressionAlertFields, getSuppressionTerms } from '../utils';
|
||||
import type { SignalSource } from '../types';
|
||||
import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring';
|
||||
import { buildBulkBody } from '../factories/utils/build_bulk_body';
|
||||
|
||||
export interface EventsAndTerms {
|
||||
event: estypes.SearchHit<SignalSource>;
|
||||
newTerms: Array<string | number | null>;
|
||||
}
|
||||
|
||||
export const wrapSuppressedNewTermsAlerts = ({
|
||||
eventsAndTerms,
|
||||
spaceId,
|
||||
completeRule,
|
||||
mergeStrategy,
|
||||
indicesToQuery,
|
||||
alertTimestampOverride,
|
||||
ruleExecutionLogger,
|
||||
publicBaseUrl,
|
||||
primaryTimestamp,
|
||||
secondaryTimestamp,
|
||||
}: {
|
||||
eventsAndTerms: EventsAndTerms[];
|
||||
spaceId: string | null | undefined;
|
||||
completeRule: CompleteRule<NewTermsRuleParams>;
|
||||
mergeStrategy: ConfigType['alertMergeStrategy'];
|
||||
indicesToQuery: string[];
|
||||
alertTimestampOverride: Date | undefined;
|
||||
ruleExecutionLogger: IRuleExecutionLogForExecutors;
|
||||
publicBaseUrl: string | undefined;
|
||||
primaryTimestamp: string;
|
||||
secondaryTimestamp?: string;
|
||||
}): Array<WrappedFieldsLatest<NewTermsFieldsLatest & SuppressionFieldsLatest>> => {
|
||||
return eventsAndTerms.map((eventAndTerms) => {
|
||||
const event = eventAndTerms.event;
|
||||
|
||||
const suppressionTerms = getSuppressionTerms({
|
||||
alertSuppression: completeRule?.ruleParams?.alertSuppression,
|
||||
fields: event.fields,
|
||||
});
|
||||
|
||||
const instanceId = objectHash([suppressionTerms, completeRule.alertId, spaceId]);
|
||||
|
||||
const id = objectHash([
|
||||
eventAndTerms.event._index,
|
||||
eventAndTerms.event._id,
|
||||
String(eventAndTerms.event._version),
|
||||
`${spaceId}:${completeRule.alertId}`,
|
||||
eventAndTerms.newTerms,
|
||||
suppressionTerms,
|
||||
]);
|
||||
|
||||
const baseAlert: BaseFieldsLatest = buildBulkBody(
|
||||
spaceId,
|
||||
completeRule,
|
||||
event,
|
||||
mergeStrategy,
|
||||
[],
|
||||
true,
|
||||
buildReasonMessageForNewTermsAlert,
|
||||
indicesToQuery,
|
||||
alertTimestampOverride,
|
||||
ruleExecutionLogger,
|
||||
id,
|
||||
publicBaseUrl
|
||||
);
|
||||
|
||||
return {
|
||||
_id: id,
|
||||
_index: '',
|
||||
_source: {
|
||||
...baseAlert,
|
||||
[ALERT_NEW_TERMS]: eventAndTerms.newTerms,
|
||||
...getSuppressionAlertFields({
|
||||
primaryTimestamp,
|
||||
secondaryTimestamp,
|
||||
fields: event.fields,
|
||||
suppressionTerms,
|
||||
fallbackTimestamp: baseAlert[TIMESTAMP],
|
||||
instanceId,
|
||||
}),
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* 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 { SuppressedAlertService } from '@kbn/rule-registry-plugin/server';
|
||||
|
||||
import type { SuppressionFieldsLatest } from '@kbn/rule-registry-plugin/common/schemas';
|
||||
import type {
|
||||
SearchAfterAndBulkCreateParams,
|
||||
SearchAfterAndBulkCreateReturnType,
|
||||
WrapSuppressedHits,
|
||||
SignalSourceHit,
|
||||
} from '../types';
|
||||
import { MAX_SIGNALS_SUPPRESSION_MULTIPLIER } from '../constants';
|
||||
import { addToSearchAfterReturn } from './utils';
|
||||
import type { AlertSuppressionCamel } from '../../../../../common/api/detection_engine/model/rule_schema';
|
||||
import { DEFAULT_SUPPRESSION_MISSING_FIELDS_STRATEGY } from '../../../../../common/detection_engine/constants';
|
||||
import { partitionMissingFieldsEvents } from './partition_missing_fields_events';
|
||||
import { AlertSuppressionMissingFieldsStrategyEnum } from '../../../../../common/api/detection_engine/model/rule_schema';
|
||||
import { createEnrichEventsFunction } from './enrichments';
|
||||
import { bulkCreateWithSuppression } from './bulk_create_with_suppression';
|
||||
import type { ExperimentalFeatures } from '../../../../../common';
|
||||
|
||||
import type {
|
||||
BaseFieldsLatest,
|
||||
WrappedFieldsLatest,
|
||||
} from '../../../../../common/api/detection_engine/model/alerts';
|
||||
|
||||
interface SearchAfterAndBulkCreateSuppressedAlertsParams extends SearchAfterAndBulkCreateParams {
|
||||
wrapSuppressedHits: WrapSuppressedHits;
|
||||
alertTimestampOverride: Date | undefined;
|
||||
alertWithSuppression: SuppressedAlertService;
|
||||
alertSuppression?: AlertSuppressionCamel;
|
||||
}
|
||||
export interface BulkCreateSuppressedAlertsParams
|
||||
extends Pick<
|
||||
SearchAfterAndBulkCreateSuppressedAlertsParams,
|
||||
| 'wrapHits'
|
||||
| 'bulkCreate'
|
||||
| 'services'
|
||||
| 'buildReasonMessage'
|
||||
| 'ruleExecutionLogger'
|
||||
| 'tuple'
|
||||
| 'alertSuppression'
|
||||
| 'wrapSuppressedHits'
|
||||
| 'alertWithSuppression'
|
||||
| 'alertTimestampOverride'
|
||||
> {
|
||||
enrichedEvents: SignalSourceHit[];
|
||||
toReturn: SearchAfterAndBulkCreateReturnType;
|
||||
experimentalFeatures?: ExperimentalFeatures;
|
||||
}
|
||||
/**
|
||||
* wraps, bulk create and suppress alerts in memory, also takes care of missing fields logic.
|
||||
* If parameter alertSuppression.missingFieldsStrategy configured not to be suppressed, regular alerts will be created for such events without suppression
|
||||
*/
|
||||
export const bulkCreateSuppressedAlertsInMemory = async ({
|
||||
enrichedEvents,
|
||||
toReturn,
|
||||
wrapHits,
|
||||
bulkCreate,
|
||||
services,
|
||||
buildReasonMessage,
|
||||
ruleExecutionLogger,
|
||||
tuple,
|
||||
alertSuppression,
|
||||
wrapSuppressedHits,
|
||||
alertWithSuppression,
|
||||
alertTimestampOverride,
|
||||
experimentalFeatures,
|
||||
}: BulkCreateSuppressedAlertsParams) => {
|
||||
const suppressOnMissingFields =
|
||||
(alertSuppression?.missingFieldsStrategy ?? DEFAULT_SUPPRESSION_MISSING_FIELDS_STRATEGY) ===
|
||||
AlertSuppressionMissingFieldsStrategyEnum.suppress;
|
||||
|
||||
let suppressibleEvents = enrichedEvents;
|
||||
let unsuppressibleWrappedDocs: Array<WrappedFieldsLatest<BaseFieldsLatest>> = [];
|
||||
|
||||
if (!suppressOnMissingFields) {
|
||||
const partitionedEvents = partitionMissingFieldsEvents(
|
||||
enrichedEvents,
|
||||
alertSuppression?.groupBy || []
|
||||
);
|
||||
|
||||
unsuppressibleWrappedDocs = wrapHits(partitionedEvents[1], buildReasonMessage);
|
||||
suppressibleEvents = partitionedEvents[0];
|
||||
}
|
||||
|
||||
const suppressibleWrappedDocs = wrapSuppressedHits(suppressibleEvents, buildReasonMessage);
|
||||
|
||||
return executeBulkCreateAlerts({
|
||||
suppressibleWrappedDocs,
|
||||
unsuppressibleWrappedDocs,
|
||||
toReturn,
|
||||
bulkCreate,
|
||||
services,
|
||||
ruleExecutionLogger,
|
||||
tuple,
|
||||
alertSuppression,
|
||||
alertWithSuppression,
|
||||
alertTimestampOverride,
|
||||
experimentalFeatures,
|
||||
});
|
||||
};
|
||||
|
||||
export interface ExecuteBulkCreateAlertsParams<T extends SuppressionFieldsLatest & BaseFieldsLatest>
|
||||
extends Pick<
|
||||
SearchAfterAndBulkCreateSuppressedAlertsParams,
|
||||
| 'bulkCreate'
|
||||
| 'services'
|
||||
| 'ruleExecutionLogger'
|
||||
| 'tuple'
|
||||
| 'alertSuppression'
|
||||
| 'alertWithSuppression'
|
||||
| 'alertTimestampOverride'
|
||||
> {
|
||||
unsuppressibleWrappedDocs: Array<WrappedFieldsLatest<BaseFieldsLatest>>;
|
||||
suppressibleWrappedDocs: Array<WrappedFieldsLatest<T>>;
|
||||
toReturn: SearchAfterAndBulkCreateReturnType;
|
||||
experimentalFeatures?: ExperimentalFeatures;
|
||||
}
|
||||
|
||||
/**
|
||||
* creates alerts in ES, both suppressed and unsuppressed
|
||||
*/
|
||||
export const executeBulkCreateAlerts = async <
|
||||
T extends SuppressionFieldsLatest & BaseFieldsLatest
|
||||
>({
|
||||
unsuppressibleWrappedDocs,
|
||||
suppressibleWrappedDocs,
|
||||
toReturn,
|
||||
bulkCreate,
|
||||
services,
|
||||
ruleExecutionLogger,
|
||||
tuple,
|
||||
alertSuppression,
|
||||
alertWithSuppression,
|
||||
alertTimestampOverride,
|
||||
experimentalFeatures,
|
||||
}: ExecuteBulkCreateAlertsParams<T>) => {
|
||||
// max signals for suppression includes suppressed and created alerts
|
||||
// this allows to lift max signals limitation to higher value
|
||||
// and can detects events beyond default max_signals value
|
||||
const suppressionMaxSignals = MAX_SIGNALS_SUPPRESSION_MULTIPLIER * tuple.maxSignals;
|
||||
const suppressionDuration = alertSuppression?.duration;
|
||||
|
||||
const suppressionWindow = suppressionDuration
|
||||
? `now-${suppressionDuration.value}${suppressionDuration.unit}`
|
||||
: tuple.from.toISOString();
|
||||
|
||||
if (unsuppressibleWrappedDocs.length) {
|
||||
const unsuppressedResult = await bulkCreate(
|
||||
unsuppressibleWrappedDocs,
|
||||
tuple.maxSignals - toReturn.createdSignalsCount,
|
||||
createEnrichEventsFunction({
|
||||
services,
|
||||
logger: ruleExecutionLogger,
|
||||
})
|
||||
);
|
||||
|
||||
addToSearchAfterReturn({ current: toReturn, next: unsuppressedResult });
|
||||
}
|
||||
|
||||
const bulkCreateResult = await bulkCreateWithSuppression({
|
||||
alertWithSuppression,
|
||||
ruleExecutionLogger,
|
||||
wrappedDocs: suppressibleWrappedDocs,
|
||||
services,
|
||||
suppressionWindow,
|
||||
alertTimestampOverride,
|
||||
isSuppressionPerRuleExecution: !suppressionDuration,
|
||||
maxAlerts: tuple.maxSignals - toReturn.createdSignalsCount,
|
||||
experimentalFeatures,
|
||||
});
|
||||
|
||||
addToSearchAfterReturn({ current: toReturn, next: bulkCreateResult });
|
||||
|
||||
const alertsWereTruncated =
|
||||
(toReturn.suppressedAlertsCount ?? 0) + toReturn.createdSignalsCount >= suppressionMaxSignals ||
|
||||
toReturn.createdSignalsCount >= tuple.maxSignals;
|
||||
|
||||
return {
|
||||
...bulkCreateResult,
|
||||
alertsWereTruncated,
|
||||
};
|
||||
};
|
|
@ -21,6 +21,7 @@ import type {
|
|||
} from '../../../../../common/api/detection_engine/model/alerts';
|
||||
import type { RuleServices } from '../types';
|
||||
import { createEnrichEventsFunction } from './enrichments';
|
||||
import type { ExperimentalFeatures } from '../../../../../common';
|
||||
|
||||
export interface GenericBulkCreateResponse<T extends BaseFieldsLatest> {
|
||||
success: boolean;
|
||||
|
@ -30,6 +31,7 @@ export interface GenericBulkCreateResponse<T extends BaseFieldsLatest> {
|
|||
suppressedItemsCount: number;
|
||||
createdItems: Array<AlertWithCommonFieldsLatest<T> & { _id: string; _index: string }>;
|
||||
errors: string[];
|
||||
alertsWereTruncated: boolean;
|
||||
}
|
||||
|
||||
export const bulkCreateWithSuppression = async <
|
||||
|
@ -42,6 +44,8 @@ export const bulkCreateWithSuppression = async <
|
|||
suppressionWindow,
|
||||
alertTimestampOverride,
|
||||
isSuppressionPerRuleExecution,
|
||||
maxAlerts,
|
||||
experimentalFeatures,
|
||||
}: {
|
||||
alertWithSuppression: SuppressedAlertService;
|
||||
ruleExecutionLogger: IRuleExecutionLogForExecutors;
|
||||
|
@ -50,6 +54,8 @@ export const bulkCreateWithSuppression = async <
|
|||
suppressionWindow: string;
|
||||
alertTimestampOverride: Date | undefined;
|
||||
isSuppressionPerRuleExecution?: boolean;
|
||||
maxAlerts?: number;
|
||||
experimentalFeatures?: ExperimentalFeatures;
|
||||
}): Promise<GenericBulkCreateResponse<T>> => {
|
||||
if (wrappedDocs.length === 0) {
|
||||
return {
|
||||
|
@ -60,6 +66,7 @@ export const bulkCreateWithSuppression = async <
|
|||
createdItemsCount: 0,
|
||||
suppressedItemsCount: 0,
|
||||
createdItems: [],
|
||||
alertsWereTruncated: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -75,7 +82,7 @@ export const bulkCreateWithSuppression = async <
|
|||
const enrichAlertsWrapper: typeof enrichAlerts = async (alerts, params) => {
|
||||
enrichmentsTimeStart = performance.now();
|
||||
try {
|
||||
const enrichedAlerts = await enrichAlerts(alerts, params);
|
||||
const enrichedAlerts = await enrichAlerts(alerts, params, experimentalFeatures);
|
||||
return enrichedAlerts;
|
||||
} catch (error) {
|
||||
ruleExecutionLogger.error(`Alerts enrichment failed: ${error}`);
|
||||
|
@ -85,17 +92,19 @@ export const bulkCreateWithSuppression = async <
|
|||
}
|
||||
};
|
||||
|
||||
const { createdAlerts, errors, suppressedAlerts } = await alertWithSuppression(
|
||||
wrappedDocs.map((doc) => ({
|
||||
_id: doc._id,
|
||||
// `fields` should have already been merged into `doc._source`
|
||||
_source: doc._source,
|
||||
})),
|
||||
suppressionWindow,
|
||||
enrichAlertsWrapper,
|
||||
alertTimestampOverride,
|
||||
isSuppressionPerRuleExecution
|
||||
);
|
||||
const { createdAlerts, errors, suppressedAlerts, alertsWereTruncated } =
|
||||
await alertWithSuppression(
|
||||
wrappedDocs.map((doc) => ({
|
||||
_id: doc._id,
|
||||
// `fields` should have already been merged into `doc._source`
|
||||
_source: doc._source,
|
||||
})),
|
||||
suppressionWindow,
|
||||
enrichAlertsWrapper,
|
||||
alertTimestampOverride,
|
||||
isSuppressionPerRuleExecution,
|
||||
maxAlerts
|
||||
);
|
||||
|
||||
const end = performance.now();
|
||||
|
||||
|
@ -111,6 +120,7 @@ export const bulkCreateWithSuppression = async <
|
|||
createdItemsCount: createdAlerts.length,
|
||||
createdItems: createdAlerts,
|
||||
suppressedItemsCount: suppressedAlerts.length,
|
||||
alertsWereTruncated,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
|
@ -121,6 +131,7 @@ export const bulkCreateWithSuppression = async <
|
|||
createdItemsCount: createdAlerts.length,
|
||||
createdItems: createdAlerts,
|
||||
suppressedItemsCount: suppressedAlerts.length,
|
||||
alertsWereTruncated,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 { LicensingPluginSetup } from '@kbn/licensing-plugin/server';
|
||||
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
import type { AlertSuppressionCamel } from '../../../../../common/api/detection_engine/model/rule_schema';
|
||||
|
||||
interface GetIsAlertSuppressionActiveParams {
|
||||
alertSuppression: AlertSuppressionCamel | undefined;
|
||||
isFeatureDisabled: boolean | undefined;
|
||||
licensing: LicensingPluginSetup;
|
||||
}
|
||||
|
||||
/**
|
||||
* checks if alert suppression is active:
|
||||
* - rule should have alert suppression config
|
||||
* - feature flag should not be disabled
|
||||
* - license should be platinum
|
||||
*/
|
||||
export const getIsAlertSuppressionActive = async ({
|
||||
licensing,
|
||||
alertSuppression,
|
||||
isFeatureDisabled = false,
|
||||
}: GetIsAlertSuppressionActiveParams) => {
|
||||
if (isFeatureDisabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isAlertSuppressionConfigured = Boolean(alertSuppression?.groupBy?.length);
|
||||
|
||||
if (!isAlertSuppressionConfigured) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const license = await firstValueFrom(licensing.license$);
|
||||
const hasPlatinumLicense = license.hasAtLeast('platinum');
|
||||
|
||||
return hasPlatinumLicense;
|
||||
};
|
|
@ -28,3 +28,4 @@ export const createResultObject = <TState extends RuleTypeState>(state: TState)
|
|||
export * from './get_list_client';
|
||||
export * from './validate_mutated_params';
|
||||
export * from './build_timestamp_runtime_mapping';
|
||||
export * from './suppression_utils';
|
||||
|
|
|
@ -56,6 +56,63 @@ describe('partitionMissingFieldsEvents', () => {
|
|||
],
|
||||
]);
|
||||
});
|
||||
it('should partition when fields objects located in event property', () => {
|
||||
expect(
|
||||
partitionMissingFieldsEvents(
|
||||
[
|
||||
{
|
||||
event: {
|
||||
fields: {
|
||||
'agent.host': 'host-1',
|
||||
'agent.type': ['test-1', 'test-2'],
|
||||
'agent.version': 2,
|
||||
},
|
||||
_id: '1',
|
||||
_index: 'index-0',
|
||||
},
|
||||
},
|
||||
{
|
||||
event: {
|
||||
fields: {
|
||||
'agent.host': 'host-1',
|
||||
'agent.type': ['test-1', 'test-2'],
|
||||
},
|
||||
_id: '1',
|
||||
_index: 'index-0',
|
||||
},
|
||||
},
|
||||
],
|
||||
['agent.host', 'agent.type', 'agent.version'],
|
||||
['event']
|
||||
)
|
||||
).toEqual([
|
||||
[
|
||||
{
|
||||
event: {
|
||||
fields: {
|
||||
'agent.host': 'host-1',
|
||||
'agent.type': ['test-1', 'test-2'],
|
||||
'agent.version': 2,
|
||||
},
|
||||
_id: '1',
|
||||
_index: 'index-0',
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
event: {
|
||||
fields: {
|
||||
'agent.host': 'host-1',
|
||||
'agent.type': ['test-1', 'test-2'],
|
||||
},
|
||||
_id: '1',
|
||||
_index: 'index-0',
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
it('should partition if two fields are empty', () => {
|
||||
expect(
|
||||
partitionMissingFieldsEvents(
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import pick from 'lodash/pick';
|
||||
import get from 'lodash/get';
|
||||
import partition from 'lodash/partition';
|
||||
|
||||
import type { SignalSourceHit } from '../types';
|
||||
|
@ -15,16 +16,21 @@ import type { SignalSourceHit } from '../types';
|
|||
* 1. first one, where no suppressed by field has empty value
|
||||
* 2. where any of fields is empty
|
||||
*/
|
||||
export const partitionMissingFieldsEvents = (
|
||||
events: SignalSourceHit[],
|
||||
suppressedBy: string[] = []
|
||||
): SignalSourceHit[][] => {
|
||||
export const partitionMissingFieldsEvents = <
|
||||
T extends SignalSourceHit | { event: SignalSourceHit }
|
||||
>(
|
||||
events: T[],
|
||||
suppressedBy: string[] = [],
|
||||
// path to fields property within event object. At this point, it can be in root of event object or within event key
|
||||
fieldsPath: ['event'] | [] = []
|
||||
): T[][] => {
|
||||
return partition(events, (event) => {
|
||||
if (suppressedBy.length === 0) {
|
||||
return true;
|
||||
}
|
||||
const eventFields = get(event, [...fieldsPath, 'fields']);
|
||||
const hasMissingFields =
|
||||
Object.keys(pick(event.fields, suppressedBy)).length < suppressedBy.length;
|
||||
Object.keys(pick(eventFields, suppressedBy)).length < suppressedBy.length;
|
||||
|
||||
return !hasMissingFields;
|
||||
});
|
||||
|
|
|
@ -7,20 +7,14 @@
|
|||
|
||||
import type { SuppressedAlertService } from '@kbn/rule-registry-plugin/server';
|
||||
|
||||
import { bulkCreateWithSuppression } from './bulk_create_with_suppression';
|
||||
import { addToSearchAfterReturn, getSuppressionMaxSignalsWarning } from './utils';
|
||||
import { getSuppressionMaxSignalsWarning } from './utils';
|
||||
import type {
|
||||
SearchAfterAndBulkCreateParams,
|
||||
SearchAfterAndBulkCreateReturnType,
|
||||
WrapSuppressedHits,
|
||||
} from '../types';
|
||||
import { MAX_SIGNALS_SUPPRESSION_MULTIPLIER } from '../constants';
|
||||
|
||||
import { createEnrichEventsFunction } from './enrichments';
|
||||
import { AlertSuppressionMissingFieldsStrategyEnum } from '../../../../../common/api/detection_engine/model/rule_schema';
|
||||
import type { AlertSuppressionCamel } from '../../../../../common/api/detection_engine/model/rule_schema';
|
||||
import { DEFAULT_SUPPRESSION_MISSING_FIELDS_STRATEGY } from '../../../../../common/detection_engine/constants';
|
||||
import { partitionMissingFieldsEvents } from './partition_missing_fields_events';
|
||||
|
||||
interface SearchAfterAndBulkCreateSuppressedAlertsParams extends SearchAfterAndBulkCreateParams {
|
||||
wrapSuppressedHits: WrapSuppressedHits;
|
||||
alertTimestampOverride: Date | undefined;
|
||||
|
@ -30,6 +24,7 @@ interface SearchAfterAndBulkCreateSuppressedAlertsParams extends SearchAfterAndB
|
|||
|
||||
import type { SearchAfterAndBulkCreateFactoryParams } from './search_after_bulk_create_factory';
|
||||
import { searchAfterAndBulkCreateFactory } from './search_after_bulk_create_factory';
|
||||
import { bulkCreateSuppressedAlertsInMemory } from './bulk_create_suppressed_alerts_in_memory';
|
||||
|
||||
/**
|
||||
* search_after through documents and re-index using bulk endpoint
|
||||
|
@ -55,62 +50,20 @@ export const searchAfterAndBulkCreateSuppressedAlerts = async (
|
|||
enrichedEvents,
|
||||
toReturn,
|
||||
}) => {
|
||||
// max signals for suppression includes suppressed and created alerts
|
||||
// this allows to lift max signals limitation to higher value
|
||||
// and can detects threats beyond default max_signals value
|
||||
const suppressionMaxSignals = MAX_SIGNALS_SUPPRESSION_MULTIPLIER * tuple.maxSignals;
|
||||
|
||||
const suppressionDuration = alertSuppression?.duration;
|
||||
const suppressionWindow = suppressionDuration
|
||||
? `now-${suppressionDuration.value}${suppressionDuration.unit}`
|
||||
: tuple.from.toISOString();
|
||||
|
||||
const suppressOnMissingFields =
|
||||
(alertSuppression?.missingFieldsStrategy ?? DEFAULT_SUPPRESSION_MISSING_FIELDS_STRATEGY) ===
|
||||
AlertSuppressionMissingFieldsStrategyEnum.suppress;
|
||||
|
||||
let suppressibleEvents = enrichedEvents;
|
||||
if (!suppressOnMissingFields) {
|
||||
const partitionedEvents = partitionMissingFieldsEvents(
|
||||
enrichedEvents,
|
||||
alertSuppression?.groupBy || []
|
||||
);
|
||||
|
||||
const wrappedDocs = wrapHits(partitionedEvents[1], buildReasonMessage);
|
||||
suppressibleEvents = partitionedEvents[0];
|
||||
|
||||
const unsuppressedResult = await bulkCreate(
|
||||
wrappedDocs,
|
||||
tuple.maxSignals - toReturn.createdSignalsCount,
|
||||
createEnrichEventsFunction({
|
||||
services,
|
||||
logger: ruleExecutionLogger,
|
||||
})
|
||||
);
|
||||
|
||||
addToSearchAfterReturn({ current: toReturn, next: unsuppressedResult });
|
||||
}
|
||||
|
||||
const wrappedDocs = wrapSuppressedHits(suppressibleEvents, buildReasonMessage);
|
||||
|
||||
const bulkCreateResult = await bulkCreateWithSuppression({
|
||||
alertWithSuppression,
|
||||
ruleExecutionLogger,
|
||||
wrappedDocs,
|
||||
return bulkCreateSuppressedAlertsInMemory({
|
||||
wrapHits,
|
||||
bulkCreate,
|
||||
services,
|
||||
suppressionWindow,
|
||||
buildReasonMessage,
|
||||
ruleExecutionLogger,
|
||||
tuple,
|
||||
alertSuppression,
|
||||
wrapSuppressedHits,
|
||||
alertWithSuppression,
|
||||
alertTimestampOverride,
|
||||
isSuppressionPerRuleExecution: !suppressionDuration,
|
||||
enrichedEvents,
|
||||
toReturn,
|
||||
});
|
||||
|
||||
addToSearchAfterReturn({ current: toReturn, next: bulkCreateResult });
|
||||
|
||||
return {
|
||||
...bulkCreateResult,
|
||||
alertsWereTruncated:
|
||||
(toReturn.suppressedAlertsCount ?? 0) + toReturn.createdSignalsCount >=
|
||||
suppressionMaxSignals || toReturn.createdSignalsCount >= tuple.maxSignals,
|
||||
};
|
||||
};
|
||||
|
||||
return searchAfterAndBulkCreateFactory({
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* 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 {
|
||||
ALERT_SUPPRESSION_DOCS_COUNT,
|
||||
ALERT_INSTANCE_ID,
|
||||
ALERT_SUPPRESSION_TERMS,
|
||||
ALERT_SUPPRESSION_START,
|
||||
ALERT_SUPPRESSION_END,
|
||||
} from '@kbn/rule-data-utils';
|
||||
|
||||
import { getSuppressionTerms, getSuppressionAlertFields } from './suppression_utils';
|
||||
|
||||
describe('getSuppressionAlertFields', () => {
|
||||
const suppressionTerms = [
|
||||
{
|
||||
field: 'agent.name',
|
||||
value: ['agent-0'],
|
||||
},
|
||||
];
|
||||
const instanceId = 'mock-id';
|
||||
const fallbackTimestamp = '2020-10-28T06:30:00.000Z';
|
||||
const expectedTimestamp = '2022-02-24T06:30:00.000Z';
|
||||
|
||||
it('should return suppression fields', () => {
|
||||
expect(
|
||||
getSuppressionAlertFields({
|
||||
primaryTimestamp: '@timestamp',
|
||||
fields: { '@timestamp': expectedTimestamp },
|
||||
suppressionTerms,
|
||||
instanceId,
|
||||
fallbackTimestamp,
|
||||
})
|
||||
).toEqual({
|
||||
[ALERT_SUPPRESSION_TERMS]: suppressionTerms,
|
||||
[ALERT_SUPPRESSION_START]: new Date(expectedTimestamp),
|
||||
[ALERT_SUPPRESSION_END]: new Date(expectedTimestamp),
|
||||
[ALERT_SUPPRESSION_DOCS_COUNT]: 0,
|
||||
[ALERT_INSTANCE_ID]: instanceId,
|
||||
});
|
||||
});
|
||||
it('should set suppression boundaries from secondary timestamp field', () => {
|
||||
expect(
|
||||
getSuppressionAlertFields({
|
||||
primaryTimestamp: '@timestamp',
|
||||
secondaryTimestamp: 'event.ingested',
|
||||
fields: { 'event.ingested': expectedTimestamp },
|
||||
suppressionTerms,
|
||||
instanceId,
|
||||
fallbackTimestamp,
|
||||
})
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
[ALERT_SUPPRESSION_START]: new Date(expectedTimestamp),
|
||||
[ALERT_SUPPRESSION_END]: new Date(expectedTimestamp),
|
||||
})
|
||||
);
|
||||
});
|
||||
it('should set suppression boundaries from fallback timestamp', () => {
|
||||
expect(
|
||||
getSuppressionAlertFields({
|
||||
primaryTimestamp: '@timestamp',
|
||||
secondaryTimestamp: 'event.ingested',
|
||||
fields: {},
|
||||
suppressionTerms,
|
||||
instanceId,
|
||||
fallbackTimestamp,
|
||||
})
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
[ALERT_SUPPRESSION_START]: new Date(fallbackTimestamp),
|
||||
[ALERT_SUPPRESSION_END]: new Date(fallbackTimestamp),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSuppressionTerms', () => {
|
||||
it('should return suppression terms', () => {
|
||||
expect(
|
||||
getSuppressionTerms({
|
||||
alertSuppression: {
|
||||
groupBy: ['host.name'],
|
||||
},
|
||||
fields: { 'host.name': 'localhost-1' },
|
||||
})
|
||||
).toEqual([{ field: 'host.name', value: 'localhost-1' }]);
|
||||
});
|
||||
it('should return suppression terms array when fields do not have matches', () => {
|
||||
expect(
|
||||
getSuppressionTerms({
|
||||
alertSuppression: {
|
||||
groupBy: ['host.name'],
|
||||
},
|
||||
fields: { 'host.ip': '127.0.0.1' },
|
||||
})
|
||||
).toEqual([{ field: 'host.name', value: null }]);
|
||||
});
|
||||
it('should return sorted suppression terms array value', () => {
|
||||
expect(
|
||||
getSuppressionTerms({
|
||||
alertSuppression: {
|
||||
groupBy: ['host.name'],
|
||||
},
|
||||
fields: { 'host.name': ['localhost-2', 'localhost-1'] },
|
||||
})
|
||||
).toEqual([{ field: 'host.name', value: ['localhost-1', 'localhost-2'] }]);
|
||||
});
|
||||
it('should return multiple suppression terms', () => {
|
||||
expect(
|
||||
getSuppressionTerms({
|
||||
alertSuppression: {
|
||||
groupBy: ['host.name', 'host.ip'],
|
||||
},
|
||||
fields: { 'host.name': ['localhost-1'], 'agent.name': 'test', 'host.ip': '127.0.0.1' },
|
||||
})
|
||||
).toEqual([
|
||||
{ field: 'host.name', value: ['localhost-1'] },
|
||||
{ field: 'host.ip', value: '127.0.0.1' },
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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 pick from 'lodash/pick';
|
||||
import get from 'lodash/get';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
|
||||
import {
|
||||
ALERT_SUPPRESSION_DOCS_COUNT,
|
||||
ALERT_INSTANCE_ID,
|
||||
ALERT_SUPPRESSION_TERMS,
|
||||
ALERT_SUPPRESSION_START,
|
||||
ALERT_SUPPRESSION_END,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import type { AlertSuppressionCamel } from '../../../../../common/api/detection_engine/model/rule_schema';
|
||||
interface SuppressionTerm {
|
||||
field: string;
|
||||
value: string[] | number[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns an object containing the standard suppression fields (ALERT_INSTANCE_ID, ALERT_SUPPRESSION_TERMS, etc), with corresponding values populated from the `fields` parameter.
|
||||
*/
|
||||
export const getSuppressionAlertFields = ({
|
||||
primaryTimestamp,
|
||||
secondaryTimestamp,
|
||||
fields,
|
||||
suppressionTerms,
|
||||
fallbackTimestamp,
|
||||
instanceId,
|
||||
}: {
|
||||
fields: Record<string, string | number> | undefined;
|
||||
primaryTimestamp: string;
|
||||
secondaryTimestamp?: string;
|
||||
suppressionTerms: SuppressionTerm[];
|
||||
fallbackTimestamp: string;
|
||||
instanceId: string;
|
||||
}) => {
|
||||
const suppressionTime = new Date(
|
||||
get(fields, primaryTimestamp) ??
|
||||
(secondaryTimestamp && get(fields, secondaryTimestamp)) ??
|
||||
fallbackTimestamp
|
||||
);
|
||||
|
||||
const suppressionFields = {
|
||||
[ALERT_INSTANCE_ID]: instanceId,
|
||||
[ALERT_SUPPRESSION_TERMS]: suppressionTerms,
|
||||
[ALERT_SUPPRESSION_START]: suppressionTime,
|
||||
[ALERT_SUPPRESSION_END]: suppressionTime,
|
||||
[ALERT_SUPPRESSION_DOCS_COUNT]: 0,
|
||||
};
|
||||
|
||||
return suppressionFields;
|
||||
};
|
||||
|
||||
/**
|
||||
* returns an array of {@link SuppressionTerm}s by retrieving the appropriate field values based on the provided alertSuppression configuration
|
||||
*/
|
||||
export const getSuppressionTerms = ({
|
||||
alertSuppression,
|
||||
fields,
|
||||
}: {
|
||||
fields: Record<string, unknown> | undefined;
|
||||
alertSuppression: AlertSuppressionCamel | undefined;
|
||||
}): SuppressionTerm[] => {
|
||||
const suppressedBy = alertSuppression?.groupBy ?? [];
|
||||
|
||||
const suppressedProps = pick(fields, suppressedBy) as Record<
|
||||
string,
|
||||
string[] | number[] | undefined
|
||||
>;
|
||||
const suppressionTerms = suppressedBy.map((field) => {
|
||||
const value = suppressedProps[field] ?? null;
|
||||
const sortedValue = Array.isArray(value) ? (sortBy(value) as string[] | number[]) : value;
|
||||
return {
|
||||
field,
|
||||
value: sortedValue,
|
||||
};
|
||||
});
|
||||
|
||||
return suppressionTerms;
|
||||
};
|
|
@ -6,19 +6,9 @@
|
|||
*/
|
||||
|
||||
import objectHash from 'object-hash';
|
||||
import pick from 'lodash/pick';
|
||||
import get from 'lodash/get';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
|
||||
import type { SuppressionFieldsLatest } from '@kbn/rule-registry-plugin/common/schemas';
|
||||
import {
|
||||
ALERT_SUPPRESSION_DOCS_COUNT,
|
||||
ALERT_INSTANCE_ID,
|
||||
ALERT_SUPPRESSION_TERMS,
|
||||
ALERT_SUPPRESSION_START,
|
||||
ALERT_SUPPRESSION_END,
|
||||
TIMESTAMP,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { TIMESTAMP } from '@kbn/rule-data-utils';
|
||||
import type { SignalSourceHit } from '../types';
|
||||
|
||||
import type {
|
||||
|
@ -29,6 +19,7 @@ import type { ConfigType } from '../../../../config';
|
|||
import type { CompleteRule, ThreatRuleParams } from '../../rule_schema';
|
||||
import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring';
|
||||
import { buildBulkBody } from '../factories/utils/build_bulk_body';
|
||||
import { getSuppressionAlertFields, getSuppressionTerms } from './suppression_utils';
|
||||
|
||||
import type { BuildReasonMessage } from './reason_formatters';
|
||||
|
||||
|
@ -62,20 +53,10 @@ export const wrapSuppressedAlerts = ({
|
|||
primaryTimestamp: string;
|
||||
secondaryTimestamp?: string;
|
||||
}): Array<WrappedFieldsLatest<BaseFieldsLatest & SuppressionFieldsLatest>> => {
|
||||
const suppressedBy = completeRule?.ruleParams?.alertSuppression?.groupBy ?? [];
|
||||
|
||||
return events.map((event) => {
|
||||
const suppressedProps = pick(event.fields, suppressedBy) as Record<
|
||||
string,
|
||||
string[] | number[] | undefined
|
||||
>;
|
||||
const suppressionTerms = suppressedBy.map((field) => {
|
||||
const value = suppressedProps[field] ?? null;
|
||||
const sortedValue = Array.isArray(value) ? (sortBy(value) as string[] | number[]) : value;
|
||||
return {
|
||||
field,
|
||||
value: sortedValue,
|
||||
};
|
||||
const suppressionTerms = getSuppressionTerms({
|
||||
alertSuppression: completeRule?.ruleParams?.alertSuppression,
|
||||
fields: event.fields,
|
||||
});
|
||||
|
||||
const id = objectHash([
|
||||
|
@ -102,22 +83,19 @@ export const wrapSuppressedAlerts = ({
|
|||
publicBaseUrl
|
||||
);
|
||||
|
||||
const suppressionTime = new Date(
|
||||
get(event.fields, primaryTimestamp) ??
|
||||
(secondaryTimestamp && get(event.fields, secondaryTimestamp)) ??
|
||||
baseAlert[TIMESTAMP]
|
||||
);
|
||||
|
||||
return {
|
||||
_id: id,
|
||||
_index: '',
|
||||
_source: {
|
||||
...baseAlert,
|
||||
[ALERT_SUPPRESSION_TERMS]: suppressionTerms,
|
||||
[ALERT_SUPPRESSION_START]: suppressionTime,
|
||||
[ALERT_SUPPRESSION_END]: suppressionTime,
|
||||
[ALERT_SUPPRESSION_DOCS_COUNT]: 0,
|
||||
[ALERT_INSTANCE_ID]: instanceId,
|
||||
...getSuppressionAlertFields({
|
||||
primaryTimestamp,
|
||||
secondaryTimestamp,
|
||||
fields: event.fields,
|
||||
suppressionTerms,
|
||||
fallbackTimestamp: baseAlert[TIMESTAMP],
|
||||
instanceId,
|
||||
}),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
@ -67,6 +67,9 @@
|
|||
"name": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"ip": {
|
||||
"type": "ip"
|
||||
},
|
||||
"uptime": {
|
||||
"type": "long"
|
||||
}
|
||||
|
|
|
@ -79,6 +79,7 @@ export function createTestConfig(options: CreateTestConfigOptions, testFiles?: s
|
|||
'--xpack.ruleRegistry.unsafe.indexUpgrade.enabled=true',
|
||||
'--xpack.ruleRegistry.unsafe.legacyMultiTenancy.enabled=true',
|
||||
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
|
||||
'alertSuppressionForNewTermsRuleEnabled',
|
||||
'previewTelemetryUrlEnabled',
|
||||
'riskScoringPersistence',
|
||||
'riskScoringRoutesEnabled',
|
||||
|
|
|
@ -17,5 +17,8 @@ export default createTestConfig({
|
|||
'testing_ignored.constant',
|
||||
'/testing_regex*/',
|
||||
])}`, // See tests within the file "ignore_fields.ts" which use these values in "alertIgnoreFields"
|
||||
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
|
||||
'alertSuppressionForNewTermsRuleEnabled',
|
||||
])}`,
|
||||
],
|
||||
});
|
||||
|
|
|
@ -33,7 +33,7 @@ import { getMaxSignalsWarning as getMaxAlertsWarning } from '@kbn/security-solut
|
|||
import { ENABLE_ASSET_CRITICALITY_SETTING } from '@kbn/security-solution-plugin/common/constants';
|
||||
import {
|
||||
getEqlRuleForAlertTesting,
|
||||
getOpenAlerts,
|
||||
getAlerts,
|
||||
getPreviewAlerts,
|
||||
previewRule,
|
||||
dataGeneratorFactory,
|
||||
|
@ -98,7 +98,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
query: specificQueryForTests,
|
||||
};
|
||||
const createdRule = await createRule(supertest, log, rule);
|
||||
const alerts = await getOpenAlerts(supertest, log, es, createdRule);
|
||||
const alerts = await getAlerts(supertest, log, es, createdRule);
|
||||
expect(alerts.hits.hits.length).eql(1);
|
||||
const fullAlert = alerts.hits.hits[0]._source;
|
||||
if (!fullAlert) {
|
||||
|
|
|
@ -17,7 +17,7 @@ import { ENABLE_ASSET_CRITICALITY_SETTING } from '@kbn/security-solution-plugin/
|
|||
import {
|
||||
getPreviewAlerts,
|
||||
previewRule,
|
||||
getOpenAlerts,
|
||||
getAlerts,
|
||||
dataGeneratorFactory,
|
||||
previewRuleWithExceptionEntries,
|
||||
removeRandomValuedPropertiesFromAlert,
|
||||
|
@ -83,7 +83,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
});
|
||||
|
||||
const createdRule = await createRule(supertest, log, rule);
|
||||
const alerts = await getOpenAlerts(supertest, log, es, createdRule);
|
||||
const alerts = await getAlerts(supertest, log, es, createdRule);
|
||||
|
||||
expect(alerts.hits.hits.length).toBe(1);
|
||||
expect(removeRandomValuedPropertiesFromAlert(alerts.hits.hits[0]._source)).toEqual({
|
||||
|
@ -786,7 +786,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
const createdRule = await createRule(supertest, log, rule);
|
||||
|
||||
// first rule run should generate 100 alerts from first 3 batches of index documents
|
||||
const alertsResponseFromFirstRuleExecution = await getOpenAlerts(
|
||||
const alertsResponseFromFirstRuleExecution = await getAlerts(
|
||||
supertest,
|
||||
log,
|
||||
es,
|
||||
|
@ -811,7 +811,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
enabled: true,
|
||||
});
|
||||
|
||||
const alertsResponse = await getOpenAlerts(
|
||||
const alertsResponse = await getAlerts(
|
||||
supertest,
|
||||
log,
|
||||
es,
|
||||
|
|
|
@ -13,6 +13,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => {
|
|||
loadTestFile(require.resolve('./esql'));
|
||||
loadTestFile(require.resolve('./machine_learning'));
|
||||
loadTestFile(require.resolve('./new_terms'));
|
||||
loadTestFile(require.resolve('./new_terms_alert_suppression'));
|
||||
loadTestFile(require.resolve('./saved_query'));
|
||||
loadTestFile(require.resolve('./threat_match'));
|
||||
loadTestFile(require.resolve('./threat_match_alert_suppression'));
|
||||
|
|
|
@ -37,7 +37,7 @@ import {
|
|||
import {
|
||||
executeSetupModuleRequest,
|
||||
forceStartDatafeeds,
|
||||
getOpenAlerts,
|
||||
getAlerts,
|
||||
getPreviewAlerts,
|
||||
previewRule,
|
||||
previewRuleWithExceptionEntries,
|
||||
|
@ -96,7 +96,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
// First test creates a real rule - remaining tests use preview API
|
||||
it('should create 1 alert from ML rule when record meets anomaly_threshold', async () => {
|
||||
const createdRule = await createRule(supertest, log, rule);
|
||||
const alerts = await getOpenAlerts(supertest, log, es, createdRule);
|
||||
const alerts = await getAlerts(supertest, log, es, createdRule);
|
||||
expect(alerts.hits.hits.length).toBe(1);
|
||||
const alert = alerts.hits.hits[0];
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import { getCreateNewTermsRulesSchemaMock } from '@kbn/security-solution-plugin/
|
|||
import { getMaxSignalsWarning as getMaxAlertsWarning } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_types/utils/utils';
|
||||
import { ENABLE_ASSET_CRITICALITY_SETTING } from '@kbn/security-solution-plugin/common/constants';
|
||||
import {
|
||||
getOpenAlerts,
|
||||
getAlerts,
|
||||
getPreviewAlerts,
|
||||
previewRule,
|
||||
dataGeneratorFactory,
|
||||
|
@ -108,7 +108,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
};
|
||||
|
||||
const createdRule = await createRule(supertest, log, rule);
|
||||
const alerts = await getOpenAlerts(supertest, log, es, createdRule);
|
||||
const alerts = await getAlerts(supertest, log, es, createdRule);
|
||||
|
||||
expect(alerts.hits.hits.length).eql(1);
|
||||
expect(removeRandomValuedPropertiesFromAlert(alerts.hits.hits[0]._source)).eql({
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -51,7 +51,7 @@ import { deleteAllExceptions } from '../../../../../lists_and_exception_lists/ut
|
|||
import {
|
||||
createExceptionList,
|
||||
createExceptionListItem,
|
||||
getOpenAlerts,
|
||||
getAlerts,
|
||||
getPreviewAlerts,
|
||||
getSimpleRule,
|
||||
previewRule,
|
||||
|
@ -127,7 +127,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
query: `_id:${ID}`,
|
||||
};
|
||||
const createdRule = await createRule(supertest, log, rule);
|
||||
const alerts = await getOpenAlerts(supertest, log, es, createdRule);
|
||||
const alerts = await getAlerts(supertest, log, es, createdRule);
|
||||
expect(alerts.hits.hits.length).greaterThan(0);
|
||||
expect(alerts.hits.hits[0]._source?.['kibana.alert.ancestors'][0].id).eql(ID);
|
||||
});
|
||||
|
@ -811,7 +811,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
},
|
||||
};
|
||||
const createdRule = await createRule(supertest, log, rule);
|
||||
const alerts = await getOpenAlerts(supertest, log, es, createdRule);
|
||||
const alerts = await getAlerts(supertest, log, es, createdRule);
|
||||
expect(alerts.hits.hits.length).eql(1);
|
||||
expect(alerts.hits.hits[0]._source).to.eql({
|
||||
...alerts.hits.hits[0]._source,
|
||||
|
@ -841,7 +841,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
await patchRule(supertest, log, { id: createdRule.id, enabled: false });
|
||||
await patchRule(supertest, log, { id: createdRule.id, enabled: true });
|
||||
const afterTimestamp = new Date();
|
||||
const secondAlerts = await getOpenAlerts(
|
||||
const secondAlerts = await getAlerts(
|
||||
supertest,
|
||||
log,
|
||||
es,
|
||||
|
@ -892,7 +892,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
},
|
||||
};
|
||||
const createdRule = await createRule(supertest, log, rule);
|
||||
const alerts = await getOpenAlerts(supertest, log, es, createdRule);
|
||||
const alerts = await getAlerts(supertest, log, es, createdRule);
|
||||
|
||||
// Close the alert. Subsequent rule executions should ignore this closed alert
|
||||
// for suppression purposes.
|
||||
|
@ -917,7 +917,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
await patchRule(supertest, log, { id: createdRule.id, enabled: false });
|
||||
await patchRule(supertest, log, { id: createdRule.id, enabled: true });
|
||||
const afterTimestamp = new Date();
|
||||
const secondAlerts = await getOpenAlerts(
|
||||
const secondAlerts = await getAlerts(
|
||||
supertest,
|
||||
log,
|
||||
es,
|
||||
|
@ -2357,7 +2357,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
.set('elastic-api-version', '2023-10-31')
|
||||
.expect(200);
|
||||
|
||||
const alertsAfterEnable = await getOpenAlerts(supertest, log, es, ruleBody, 'succeeded');
|
||||
const alertsAfterEnable = await getAlerts(supertest, log, es, ruleBody, 'succeeded');
|
||||
expect(alertsAfterEnable.hits.hits.length > 0).eql(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
ALERT_ORIGINAL_TIME,
|
||||
ALERT_ORIGINAL_EVENT,
|
||||
} from '@kbn/security-solution-plugin/common/field_maps/field_names';
|
||||
import { getOpenAlerts } from '../../../../utils';
|
||||
import { getAlerts } from '../../../../utils';
|
||||
import {
|
||||
createRule,
|
||||
deleteAllRules,
|
||||
|
@ -63,7 +63,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
saved_id: 'doesnt-exist',
|
||||
};
|
||||
const createdRule = await createRule(supertest, log, rule);
|
||||
const alerts = await getOpenAlerts(supertest, log, es, createdRule);
|
||||
const alerts = await getAlerts(supertest, log, es, createdRule);
|
||||
const alert = alerts.hits.hits[0]._source;
|
||||
expect(alert).eql({
|
||||
...alert,
|
||||
|
|
|
@ -38,7 +38,7 @@ import {
|
|||
import { RuleExecutionStatusEnum } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_monitoring';
|
||||
import { getMaxSignalsWarning as getMaxAlertsWarning } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_types/utils/utils';
|
||||
import { ENABLE_ASSET_CRITICALITY_SETTING } from '@kbn/security-solution-plugin/common/constants';
|
||||
import { previewRule, getOpenAlerts, getPreviewAlerts } from '../../../../utils';
|
||||
import { previewRule, getAlerts, getPreviewAlerts } from '../../../../utils';
|
||||
import {
|
||||
deleteAllAlerts,
|
||||
deleteAllRules,
|
||||
|
@ -175,7 +175,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
const rule: ThreatMatchRuleCreateProps = createThreatMatchRule();
|
||||
|
||||
const createdRule = await createRule(supertest, log, rule);
|
||||
const alerts = await getOpenAlerts(
|
||||
const alerts = await getAlerts(
|
||||
supertest,
|
||||
log,
|
||||
es,
|
||||
|
@ -356,7 +356,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
});
|
||||
|
||||
const createdRule = await createRule(supertest, log, rule);
|
||||
const alerts = await getOpenAlerts(
|
||||
const alerts = await getAlerts(
|
||||
supertest,
|
||||
log,
|
||||
es,
|
||||
|
@ -558,7 +558,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
|
||||
const createdRuleTerm = await createRule(supertest, log, termRule);
|
||||
const createdRuleMatch = await createRule(supertest, log, matchRule);
|
||||
const alertsTerm = await getOpenAlerts(
|
||||
const alertsTerm = await getAlerts(
|
||||
supertest,
|
||||
log,
|
||||
es,
|
||||
|
@ -566,7 +566,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
RuleExecutionStatusEnum.succeeded,
|
||||
100
|
||||
);
|
||||
const alertsMatch = await getOpenAlerts(
|
||||
const alertsMatch = await getAlerts(
|
||||
supertest,
|
||||
log,
|
||||
es,
|
||||
|
|
|
@ -334,8 +334,8 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
const secondDocument = {
|
||||
id,
|
||||
'@timestamp': secondTimestamp,
|
||||
agent: {
|
||||
name: 'agent-1',
|
||||
host: {
|
||||
name: 'host-a',
|
||||
},
|
||||
};
|
||||
// Add new documents, then disable and re-enable to trigger another rule run. The second doc should
|
||||
|
|
|
@ -27,7 +27,7 @@ import { getMaxSignalsWarning as getMaxAlertsWarning } from '@kbn/security-solut
|
|||
import { ENABLE_ASSET_CRITICALITY_SETTING } from '@kbn/security-solution-plugin/common/constants';
|
||||
import { createRule } from '../../../../../../../common/utils/security_solution';
|
||||
import {
|
||||
getOpenAlerts,
|
||||
getAlerts,
|
||||
getPreviewAlerts,
|
||||
getThresholdRuleForAlertTesting,
|
||||
previewRule,
|
||||
|
@ -66,7 +66,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
},
|
||||
};
|
||||
const createdRule = await createRule(supertest, log, rule);
|
||||
const alerts = await getOpenAlerts(supertest, log, es, createdRule);
|
||||
const alerts = await getAlerts(supertest, log, es, createdRule);
|
||||
expect(alerts.hits.hits.length).toEqual(1);
|
||||
const fullAlert = alerts.hits.hits[0]._source;
|
||||
if (!fullAlert) {
|
||||
|
@ -332,7 +332,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
},
|
||||
};
|
||||
const createdRule = await createRule(supertest, log, rule);
|
||||
const alerts = await getOpenAlerts(supertest, log, es, createdRule);
|
||||
const alerts = await getAlerts(supertest, log, es, createdRule);
|
||||
expect(alerts.hits.hits.length).toEqual(1);
|
||||
});
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ import { RuleExecutionStatusEnum } from '@kbn/security-solution-plugin/common/ap
|
|||
import { ALERT_ORIGINAL_TIME } from '@kbn/security-solution-plugin/common/field_maps/field_names';
|
||||
import { createRule } from '../../../../../../../common/utils/security_solution';
|
||||
import {
|
||||
getOpenAlerts,
|
||||
getAlerts,
|
||||
getPreviewAlerts,
|
||||
getThresholdRuleForAlertTesting,
|
||||
previewRule,
|
||||
|
@ -85,7 +85,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
interval: '30m',
|
||||
};
|
||||
const createdRule = await createRule(supertest, log, rule);
|
||||
const alerts = await getOpenAlerts(supertest, log, es, createdRule);
|
||||
const alerts = await getAlerts(supertest, log, es, createdRule);
|
||||
expect(alerts.hits.hits.length).toEqual(1);
|
||||
|
||||
// suppression start equal to alert timestamp
|
||||
|
@ -122,7 +122,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
await patchRule(supertest, log, { id: createdRule.id, enabled: false });
|
||||
await patchRule(supertest, log, { id: createdRule.id, enabled: true });
|
||||
const afterTimestamp = new Date();
|
||||
const secondAlerts = await getOpenAlerts(
|
||||
const secondAlerts = await getAlerts(
|
||||
supertest,
|
||||
log,
|
||||
es,
|
||||
|
@ -177,7 +177,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
interval: '30m',
|
||||
};
|
||||
const createdRule = await createRule(supertest, log, rule);
|
||||
const alerts = await getOpenAlerts(supertest, log, es, createdRule);
|
||||
const alerts = await getAlerts(supertest, log, es, createdRule);
|
||||
|
||||
// Close the alert. Subsequent rule executions should ignore this closed alert
|
||||
// for suppression purposes.
|
||||
|
@ -202,7 +202,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
await patchRule(supertest, log, { id: createdRule.id, enabled: false });
|
||||
await patchRule(supertest, log, { id: createdRule.id, enabled: true });
|
||||
const afterTimestamp = new Date();
|
||||
const secondAlerts = await getOpenAlerts(
|
||||
const secondAlerts = await getAlerts(
|
||||
supertest,
|
||||
log,
|
||||
es,
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
} from '@kbn/security-solution-plugin/common/api/detection_engine';
|
||||
import { ALERT_ORIGINAL_TIME } from '@kbn/security-solution-plugin/common/field_maps/field_names';
|
||||
|
||||
import { getOpenAlerts, getEqlRuleForAlertTesting } from '../../../utils';
|
||||
import { getAlerts, getEqlRuleForAlertTesting } from '../../../utils';
|
||||
import {
|
||||
createAlertsIndex,
|
||||
deleteAllRules,
|
||||
|
@ -251,7 +251,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
};
|
||||
|
||||
const createdRule = await createRule(supertest, log, rule);
|
||||
const alertsOpen = await getOpenAlerts(
|
||||
const alertsOpen = await getAlerts(
|
||||
supertest,
|
||||
log,
|
||||
es,
|
||||
|
@ -330,7 +330,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
timestamp_override_fallback_disabled: true,
|
||||
};
|
||||
const createdRule = await createRule(supertest, log, rule);
|
||||
const alertsOpen = await getOpenAlerts(
|
||||
const alertsOpen = await getAlerts(
|
||||
supertest,
|
||||
log,
|
||||
es,
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 SuperTest from 'supertest';
|
||||
import type { Client } from '@elastic/elasticsearch';
|
||||
import type { ToolingLog } from '@kbn/tooling-log';
|
||||
import {
|
||||
RuleExecutionStatus,
|
||||
RuleExecutionStatusEnum,
|
||||
} from '@kbn/security-solution-plugin/common/api/detection_engine/rule_monitoring';
|
||||
import type { RuleResponse } from '@kbn/security-solution-plugin/common/api/detection_engine';
|
||||
|
||||
import { refreshIndex } from '..';
|
||||
import { getAlertsByIds, waitForRuleStatus } from '../../../../../common/utils/security_solution';
|
||||
|
||||
export type GetAlerts = (
|
||||
supertest: SuperTest.SuperTest<SuperTest.Test>,
|
||||
log: ToolingLog,
|
||||
es: Client,
|
||||
rule: RuleResponse,
|
||||
status?: RuleExecutionStatus,
|
||||
size?: number,
|
||||
afterDate?: Date
|
||||
) => ReturnType<typeof getAlertsByIds>;
|
||||
|
||||
/**
|
||||
* returns all alerts: opened and closed
|
||||
*/
|
||||
export const getAlerts: GetAlerts = async (
|
||||
supertest,
|
||||
log,
|
||||
es,
|
||||
rule,
|
||||
status = RuleExecutionStatusEnum.succeeded,
|
||||
size,
|
||||
afterDate
|
||||
) => {
|
||||
await waitForRuleStatus(status, { supertest, log, id: rule.id, afterDate });
|
||||
// Critically important that we wait for rule success AND refresh the write index in that order before we
|
||||
// assert that no Alerts were created. Otherwise, Alerts could be written but not available to query yet
|
||||
// when we search, causing tests that check that Alerts are NOT created to pass when they should fail.
|
||||
await refreshIndex(es, '.alerts-security.alerts-default*');
|
||||
return getAlertsByIds(supertest, log, [rule.id], size);
|
||||
};
|
|
@ -5,31 +5,19 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type SuperTest from 'supertest';
|
||||
import type { Client } from '@elastic/elasticsearch';
|
||||
import type { ToolingLog } from '@kbn/tooling-log';
|
||||
import {
|
||||
RuleExecutionStatus,
|
||||
RuleExecutionStatusEnum,
|
||||
} from '@kbn/security-solution-plugin/common/api/detection_engine/rule_monitoring';
|
||||
import type { RuleResponse } from '@kbn/security-solution-plugin/common/api/detection_engine';
|
||||
import { ALERT_WORKFLOW_STATUS } from '@kbn/rule-data-utils';
|
||||
import { getAlerts } from './get_alerts';
|
||||
import type { GetAlerts } from './get_alerts';
|
||||
|
||||
import { refreshIndex } from '..';
|
||||
import { getAlertsByIds, waitForRuleStatus } from '../../../../../common/utils/security_solution';
|
||||
/**
|
||||
* returns only alerts with open status
|
||||
*/
|
||||
export const getOpenAlerts: GetAlerts = async (...args) => {
|
||||
const alerts = await getAlerts(...args);
|
||||
|
||||
export const getOpenAlerts = async (
|
||||
supertest: SuperTest.SuperTest<SuperTest.Test>,
|
||||
log: ToolingLog,
|
||||
es: Client,
|
||||
rule: RuleResponse,
|
||||
status: RuleExecutionStatus = RuleExecutionStatusEnum.succeeded,
|
||||
size?: number,
|
||||
afterDate?: Date
|
||||
) => {
|
||||
await waitForRuleStatus(status, { supertest, log, id: rule.id, afterDate });
|
||||
// Critically important that we wait for rule success AND refresh the write index in that order before we
|
||||
// assert that no Alerts were created. Otherwise, Alerts could be written but not available to query yet
|
||||
// when we search, causing tests that check that Alerts are NOT created to pass when they should fail.
|
||||
await refreshIndex(es, '.alerts-security.alerts-default*');
|
||||
return getAlertsByIds(supertest, log, [rule.id], size);
|
||||
alerts.hits.hits = alerts.hits.hits.filter(
|
||||
(alert) => alert?._source?.[ALERT_WORKFLOW_STATUS] === 'open'
|
||||
);
|
||||
|
||||
return alerts;
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
export * from './wait_for_alert_to_complete';
|
||||
export * from './wait_for_alert_to_complete';
|
||||
export * from './get_alerts';
|
||||
export * from './get_open_alerts';
|
||||
export * from './remove_random_valued_properties_from_alert';
|
||||
export * from './set_alert_status';
|
||||
|
|
|
@ -45,6 +45,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
'--xpack.alerting.rules.minimumScheduleInterval.value=1s',
|
||||
'--xpack.ruleRegistry.unsafe.legacyMultiTenancy.enabled=true',
|
||||
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
|
||||
'alertSuppressionForNewTermsRuleEnabled',
|
||||
'chartEmbeddablesEnabled',
|
||||
])}`,
|
||||
// mock cloud to enable the guided onboarding tour in e2e tests
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 {
|
||||
selectThresholdRuleType,
|
||||
selectIndicatorMatchType,
|
||||
selectNewTermsRuleType,
|
||||
} from '../../../../tasks/create_new_rule';
|
||||
import { login } from '../../../../tasks/login';
|
||||
import { visit } from '../../../../tasks/navigation';
|
||||
import {
|
||||
ALERT_SUPPRESSION_FIELDS_INPUT,
|
||||
THRESHOLD_ENABLE_SUPPRESSION_CHECKBOX,
|
||||
} from '../../../../screens/create_new_rule';
|
||||
import { CREATE_RULE_URL } from '../../../../urls/navigation';
|
||||
|
||||
describe(
|
||||
'Detection rules, Alert Suppression for Essentials tier',
|
||||
{
|
||||
tags: ['@serverless'],
|
||||
env: {
|
||||
ftrConfig: {
|
||||
productTypes: [
|
||||
{ product_line: 'security', product_tier: 'essentials' },
|
||||
{ product_line: 'endpoint', product_tier: 'essentials' },
|
||||
],
|
||||
// alertSuppressionForNewTermsRuleEnabled feature flag is also enabled in a global config
|
||||
kbnServerArgs: [
|
||||
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
|
||||
'alertSuppressionForNewTermsRuleEnabled',
|
||||
])}`,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
() => {
|
||||
beforeEach(() => {
|
||||
login();
|
||||
visit(CREATE_RULE_URL);
|
||||
});
|
||||
|
||||
it('Alert suppression is enabled for essentials tier for rule types that support it', () => {
|
||||
// default custom query rule
|
||||
cy.get(ALERT_SUPPRESSION_FIELDS_INPUT).should('be.enabled');
|
||||
|
||||
selectIndicatorMatchType();
|
||||
cy.get(ALERT_SUPPRESSION_FIELDS_INPUT).should('be.enabled');
|
||||
|
||||
selectNewTermsRuleType();
|
||||
cy.get(ALERT_SUPPRESSION_FIELDS_INPUT).should('be.enabled');
|
||||
|
||||
selectThresholdRuleType();
|
||||
cy.get(THRESHOLD_ENABLE_SUPPRESSION_CHECKBOX).should('be.enabled');
|
||||
});
|
||||
}
|
||||
);
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 {
|
||||
THRESHOLD_ENABLE_SUPPRESSION_CHECKBOX,
|
||||
ALERT_SUPPRESSION_DURATION_INPUT,
|
||||
} from '../../../../screens/create_new_rule';
|
||||
|
||||
import {
|
||||
selectIndicatorMatchType,
|
||||
selectNewTermsRuleType,
|
||||
selectThresholdRuleType,
|
||||
openSuppressionFieldsTooltipAndCheckLicense,
|
||||
} from '../../../../tasks/create_new_rule';
|
||||
import { startBasicLicense } from '../../../../tasks/api_calls/licensing';
|
||||
import { login } from '../../../../tasks/login';
|
||||
import { visit } from '../../../../tasks/navigation';
|
||||
import { CREATE_RULE_URL } from '../../../../urls/navigation';
|
||||
import { TOOLTIP } from '../../../../screens/common';
|
||||
|
||||
import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
|
||||
|
||||
describe(
|
||||
'Detection rules, Common flows Alert Suppression',
|
||||
{
|
||||
tags: ['@ess'],
|
||||
},
|
||||
() => {
|
||||
describe('Create rule form', () => {
|
||||
beforeEach(() => {
|
||||
deleteAlertsAndRules();
|
||||
login();
|
||||
visit(CREATE_RULE_URL);
|
||||
startBasicLicense();
|
||||
});
|
||||
|
||||
it('can not create rule with rule execution suppression on basic license for all rules with enabled suppression', () => {
|
||||
// Default query rule
|
||||
openSuppressionFieldsTooltipAndCheckLicense();
|
||||
|
||||
selectIndicatorMatchType();
|
||||
openSuppressionFieldsTooltipAndCheckLicense();
|
||||
|
||||
selectNewTermsRuleType();
|
||||
openSuppressionFieldsTooltipAndCheckLicense();
|
||||
|
||||
selectThresholdRuleType();
|
||||
cy.get(THRESHOLD_ENABLE_SUPPRESSION_CHECKBOX).should('be.disabled');
|
||||
cy.get(THRESHOLD_ENABLE_SUPPRESSION_CHECKBOX).parent().trigger('mouseover');
|
||||
// Platinum license is required, tooltip on disabled alert suppression checkbox should tell this
|
||||
cy.get(TOOLTIP).contains('Platinum license');
|
||||
|
||||
cy.get(ALERT_SUPPRESSION_DURATION_INPUT).eq(0).should('be.disabled');
|
||||
cy.get(ALERT_SUPPRESSION_DURATION_INPUT).eq(1).should('be.disabled');
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
|
@ -7,10 +7,6 @@
|
|||
|
||||
import { getNewThreatIndicatorRule } from '../../../../objects/rule';
|
||||
|
||||
import {
|
||||
ALERT_SUPPRESSION_FIELDS_INPUT,
|
||||
ALERT_SUPPRESSION_FIELDS,
|
||||
} from '../../../../screens/create_new_rule';
|
||||
import {
|
||||
SUPPRESS_FOR_DETAILS,
|
||||
DETAILS_TITLE,
|
||||
|
@ -20,7 +16,6 @@ import {
|
|||
ALERT_SUPPRESSION_INSUFFICIENT_LICENSING_ICON,
|
||||
} from '../../../../screens/rule_details';
|
||||
|
||||
import { selectIndicatorMatchType } from '../../../../tasks/create_new_rule';
|
||||
import { startBasicLicense } from '../../../../tasks/api_calls/licensing';
|
||||
import { createRule } from '../../../../tasks/api_calls/rules';
|
||||
import { login } from '../../../../tasks/login';
|
||||
|
@ -47,16 +42,6 @@ describe(
|
|||
startBasicLicense();
|
||||
});
|
||||
|
||||
it('can not create rule with rule execution suppression on basic license', () => {
|
||||
selectIndicatorMatchType();
|
||||
|
||||
cy.get(ALERT_SUPPRESSION_FIELDS_INPUT).should('be.disabled');
|
||||
cy.get(ALERT_SUPPRESSION_FIELDS).trigger('mouseover');
|
||||
|
||||
// Platinum license is required, tooltip on disabled alert suppression checkbox should tell this
|
||||
cy.get(TOOLTIP).contains('Platinum license');
|
||||
});
|
||||
|
||||
it('shows upselling message on rule details with suppression on basic license', () => {
|
||||
const rule = getNewThreatIndicatorRule();
|
||||
|
||||
|
|
|
@ -41,6 +41,9 @@ import {
|
|||
TIMELINE_TEMPLATE_DETAILS,
|
||||
NEW_TERMS_HISTORY_WINDOW_DETAILS,
|
||||
NEW_TERMS_FIELDS_DETAILS,
|
||||
SUPPRESS_BY_DETAILS,
|
||||
SUPPRESS_FOR_DETAILS,
|
||||
SUPPRESS_MISSING_FIELD,
|
||||
} from '../../../../screens/rule_details';
|
||||
|
||||
import { getDetails, waitForTheRuleToBeExecuted } from '../../../../tasks/rule_details';
|
||||
|
@ -49,94 +52,153 @@ import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
|
|||
import {
|
||||
createAndEnableRule,
|
||||
fillAboutRuleAndContinue,
|
||||
fillDefineNewTermsRule,
|
||||
fillDefineNewTermsRuleAndContinue,
|
||||
fillScheduleRuleAndContinue,
|
||||
selectNewTermsRuleType,
|
||||
waitForAlertsToPopulate,
|
||||
fillAlertSuppressionFields,
|
||||
fillAboutRuleMinimumAndContinue,
|
||||
createRuleWithoutEnabling,
|
||||
skipScheduleRuleAction,
|
||||
continueFromDefineStep,
|
||||
selectAlertSuppressionPerInterval,
|
||||
setAlertSuppressionDuration,
|
||||
selectDoNotSuppressForMissingFields,
|
||||
} from '../../../../tasks/create_new_rule';
|
||||
import { login } from '../../../../tasks/login';
|
||||
import { visit } from '../../../../tasks/navigation';
|
||||
import { CREATE_RULE_URL } from '../../../../urls/navigation';
|
||||
import { openRuleManagementPageViaBreadcrumbs } from '../../../../tasks/rules_management';
|
||||
|
||||
describe('New Terms rules', { tags: ['@ess', '@serverless'] }, () => {
|
||||
describe('Detection rules, New Terms', () => {
|
||||
const rule = getNewTermsRule();
|
||||
const expectedUrls = rule.references?.join('');
|
||||
const expectedFalsePositives = rule.false_positives?.join('');
|
||||
const expectedTags = rule.tags?.join('');
|
||||
const mitreAttack = rule.threat;
|
||||
const expectedMitre = formatMitreAttackDescription(mitreAttack ?? []);
|
||||
const expectedNumberOfRules = 1;
|
||||
describe(
|
||||
'New Terms rules',
|
||||
{
|
||||
tags: ['@ess', '@serverless'],
|
||||
env: {
|
||||
// alertSuppressionForNewTermsRuleEnabled feature flag is also enabled in a global config
|
||||
kbnServerArgs: [
|
||||
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
|
||||
'alertSuppressionForNewTermsRuleEnabled',
|
||||
])}`,
|
||||
],
|
||||
},
|
||||
},
|
||||
() => {
|
||||
describe('Detection rules, New Terms', () => {
|
||||
const rule = getNewTermsRule();
|
||||
const expectedUrls = rule.references?.join('');
|
||||
const expectedFalsePositives = rule.false_positives?.join('');
|
||||
const expectedTags = rule.tags?.join('');
|
||||
const mitreAttack = rule.threat;
|
||||
const expectedMitre = formatMitreAttackDescription(mitreAttack ?? []);
|
||||
const expectedNumberOfRules = 1;
|
||||
|
||||
beforeEach(() => {
|
||||
deleteAlertsAndRules();
|
||||
login();
|
||||
beforeEach(() => {
|
||||
deleteAlertsAndRules();
|
||||
login();
|
||||
visit(CREATE_RULE_URL);
|
||||
selectNewTermsRuleType();
|
||||
});
|
||||
|
||||
it('Creates and enables a new terms rule', function () {
|
||||
fillDefineNewTermsRuleAndContinue(rule);
|
||||
fillAboutRuleAndContinue(rule);
|
||||
fillScheduleRuleAndContinue(rule);
|
||||
createAndEnableRule();
|
||||
openRuleManagementPageViaBreadcrumbs();
|
||||
|
||||
cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)');
|
||||
|
||||
expectNumberOfRules(RULES_MANAGEMENT_TABLE, expectedNumberOfRules);
|
||||
|
||||
cy.get(RULE_NAME).should('have.text', rule.name);
|
||||
cy.get(RISK_SCORE).should('have.text', rule.risk_score);
|
||||
cy.get(SEVERITY).should('have.text', 'High');
|
||||
cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');
|
||||
|
||||
goToRuleDetailsOf(rule.name);
|
||||
|
||||
cy.get(RULE_NAME_HEADER).should('contain', `${rule.name}`);
|
||||
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', rule.description);
|
||||
cy.get(ABOUT_DETAILS).within(() => {
|
||||
getDetails(SEVERITY_DETAILS).should('have.text', 'High');
|
||||
getDetails(RISK_SCORE_DETAILS).should('have.text', rule.risk_score);
|
||||
getDetails(REFERENCE_URLS_DETAILS).should((details) => {
|
||||
expect(removeExternalLinkText(details.text())).equal(expectedUrls);
|
||||
});
|
||||
getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives);
|
||||
getDetails(MITRE_ATTACK_DETAILS).should((mitre) => {
|
||||
expect(removeExternalLinkText(mitre.text())).equal(expectedMitre);
|
||||
});
|
||||
getDetails(TAGS_DETAILS).should('have.text', expectedTags);
|
||||
});
|
||||
cy.get(INVESTIGATION_NOTES_TOGGLE).click();
|
||||
cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN);
|
||||
cy.get(DEFINITION_DETAILS).within(() => {
|
||||
getDetails(INDEX_PATTERNS_DETAILS).should('have.text', getIndexPatterns().join(''));
|
||||
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', rule.query);
|
||||
getDetails(RULE_TYPE_DETAILS).should('have.text', 'New Terms');
|
||||
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
|
||||
getDetails(NEW_TERMS_FIELDS_DETAILS).should('have.text', 'host.name');
|
||||
getDetails(NEW_TERMS_HISTORY_WINDOW_DETAILS).should('have.text', '51000h');
|
||||
});
|
||||
cy.get(SCHEDULE_DETAILS).within(() => {
|
||||
getDetails(RUNS_EVERY_DETAILS).should('have.text', `${rule.interval}`);
|
||||
const humanizedDuration = getHumanizedDuration(
|
||||
rule.from ?? 'now-6m',
|
||||
rule.interval ?? '5m'
|
||||
);
|
||||
getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should('have.text', `${humanizedDuration}`);
|
||||
});
|
||||
|
||||
waitForTheRuleToBeExecuted();
|
||||
waitForAlertsToPopulate();
|
||||
|
||||
cy.get(ALERT_DATA_GRID)
|
||||
.invoke('text')
|
||||
.then((text) => {
|
||||
expect(text).contains(rule.name);
|
||||
expect(text).contains(rule.severity);
|
||||
expect(text).contains(rule.risk_score);
|
||||
});
|
||||
});
|
||||
|
||||
it('Creates rule rule with time interval suppression', () => {
|
||||
const SUPPRESS_BY_FIELDS = ['agent.hostname', 'agent.type'];
|
||||
|
||||
fillDefineNewTermsRule(rule);
|
||||
|
||||
// fill suppress by fields and select non-default suppression options
|
||||
fillAlertSuppressionFields(SUPPRESS_BY_FIELDS);
|
||||
selectAlertSuppressionPerInterval();
|
||||
setAlertSuppressionDuration(45, 'm');
|
||||
selectDoNotSuppressForMissingFields();
|
||||
continueFromDefineStep();
|
||||
|
||||
// ensures details preview works correctly
|
||||
cy.get(DEFINITION_DETAILS).within(() => {
|
||||
getDetails(SUPPRESS_BY_DETAILS).should('have.text', SUPPRESS_BY_FIELDS.join(''));
|
||||
getDetails(SUPPRESS_FOR_DETAILS).should('have.text', '45m');
|
||||
getDetails(SUPPRESS_MISSING_FIELD).should(
|
||||
'have.text',
|
||||
'Do not suppress alerts for events with missing fields'
|
||||
);
|
||||
});
|
||||
|
||||
fillAboutRuleMinimumAndContinue(rule);
|
||||
skipScheduleRuleAction();
|
||||
createRuleWithoutEnabling();
|
||||
|
||||
cy.get(DEFINITION_DETAILS).within(() => {
|
||||
getDetails(SUPPRESS_BY_DETAILS).should('have.text', SUPPRESS_BY_FIELDS.join(''));
|
||||
getDetails(SUPPRESS_FOR_DETAILS).should('have.text', '45m');
|
||||
getDetails(SUPPRESS_MISSING_FIELD).should(
|
||||
'have.text',
|
||||
'Do not suppress alerts for events with missing fields'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Creates and enables a new terms rule', function () {
|
||||
visit(CREATE_RULE_URL);
|
||||
selectNewTermsRuleType();
|
||||
fillDefineNewTermsRuleAndContinue(rule);
|
||||
fillAboutRuleAndContinue(rule);
|
||||
fillScheduleRuleAndContinue(rule);
|
||||
createAndEnableRule();
|
||||
openRuleManagementPageViaBreadcrumbs();
|
||||
|
||||
cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)');
|
||||
|
||||
expectNumberOfRules(RULES_MANAGEMENT_TABLE, expectedNumberOfRules);
|
||||
|
||||
cy.get(RULE_NAME).should('have.text', rule.name);
|
||||
cy.get(RISK_SCORE).should('have.text', rule.risk_score);
|
||||
cy.get(SEVERITY).should('have.text', 'High');
|
||||
cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');
|
||||
|
||||
goToRuleDetailsOf(rule.name);
|
||||
|
||||
cy.get(RULE_NAME_HEADER).should('contain', `${rule.name}`);
|
||||
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', rule.description);
|
||||
cy.get(ABOUT_DETAILS).within(() => {
|
||||
getDetails(SEVERITY_DETAILS).should('have.text', 'High');
|
||||
getDetails(RISK_SCORE_DETAILS).should('have.text', rule.risk_score);
|
||||
getDetails(REFERENCE_URLS_DETAILS).should((details) => {
|
||||
expect(removeExternalLinkText(details.text())).equal(expectedUrls);
|
||||
});
|
||||
getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives);
|
||||
getDetails(MITRE_ATTACK_DETAILS).should((mitre) => {
|
||||
expect(removeExternalLinkText(mitre.text())).equal(expectedMitre);
|
||||
});
|
||||
getDetails(TAGS_DETAILS).should('have.text', expectedTags);
|
||||
});
|
||||
cy.get(INVESTIGATION_NOTES_TOGGLE).click();
|
||||
cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN);
|
||||
cy.get(DEFINITION_DETAILS).within(() => {
|
||||
getDetails(INDEX_PATTERNS_DETAILS).should('have.text', getIndexPatterns().join(''));
|
||||
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', rule.query);
|
||||
getDetails(RULE_TYPE_DETAILS).should('have.text', 'New Terms');
|
||||
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
|
||||
getDetails(NEW_TERMS_FIELDS_DETAILS).should('have.text', 'host.name');
|
||||
getDetails(NEW_TERMS_HISTORY_WINDOW_DETAILS).should('have.text', '51000h');
|
||||
});
|
||||
cy.get(SCHEDULE_DETAILS).within(() => {
|
||||
getDetails(RUNS_EVERY_DETAILS).should('have.text', `${rule.interval}`);
|
||||
const humanizedDuration = getHumanizedDuration(
|
||||
rule.from ?? 'now-6m',
|
||||
rule.interval ?? '5m'
|
||||
);
|
||||
getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should('have.text', `${humanizedDuration}`);
|
||||
});
|
||||
|
||||
waitForTheRuleToBeExecuted();
|
||||
waitForAlertsToPopulate();
|
||||
|
||||
cy.get(ALERT_DATA_GRID)
|
||||
.invoke('text')
|
||||
.then((text) => {
|
||||
expect(text).contains(rule.name);
|
||||
expect(text).contains(rule.severity);
|
||||
expect(text).contains(rule.risk_score);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* 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 {
|
||||
ALERT_SUPPRESSION_DURATION_INPUT,
|
||||
THRESHOLD_ENABLE_SUPPRESSION_CHECKBOX,
|
||||
} from '../../../../screens/create_new_rule';
|
||||
|
||||
import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
|
||||
import { startBasicLicense } from '../../../../tasks/api_calls/licensing';
|
||||
import { selectThresholdRuleType } from '../../../../tasks/create_new_rule';
|
||||
import { login } from '../../../../tasks/login';
|
||||
import { visit } from '../../../../tasks/navigation';
|
||||
import { CREATE_RULE_URL } from '../../../../urls/navigation';
|
||||
import { TOOLTIP } from '../../../../screens/common';
|
||||
|
||||
describe('Threshold rules, ESS basic license', { tags: ['@ess'] }, () => {
|
||||
beforeEach(() => {
|
||||
deleteAlertsAndRules();
|
||||
login();
|
||||
visit(CREATE_RULE_URL);
|
||||
startBasicLicense();
|
||||
});
|
||||
|
||||
it('Alert suppression is disabled for basic license', () => {
|
||||
selectThresholdRuleType();
|
||||
|
||||
cy.get(THRESHOLD_ENABLE_SUPPRESSION_CHECKBOX).should('be.disabled');
|
||||
cy.get(THRESHOLD_ENABLE_SUPPRESSION_CHECKBOX).parent().trigger('mouseover');
|
||||
// Platinum license is required, tooltip on disabled alert suppression checkbox should tell this
|
||||
cy.get(TOOLTIP).contains('Platinum license');
|
||||
|
||||
cy.get(ALERT_SUPPRESSION_DURATION_INPUT).eq(0).should('be.disabled');
|
||||
cy.get(ALERT_SUPPRESSION_DURATION_INPUT).eq(1).should('be.disabled');
|
||||
});
|
||||
});
|
|
@ -1,42 +0,0 @@
|
|||
/*
|
||||
* 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 { THRESHOLD_ENABLE_SUPPRESSION_CHECKBOX } from '../../../../screens/create_new_rule';
|
||||
import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
|
||||
import { selectThresholdRuleType } from '../../../../tasks/create_new_rule';
|
||||
import { login } from '../../../../tasks/login';
|
||||
import { visit } from '../../../../tasks/navigation';
|
||||
import { CREATE_RULE_URL } from '../../../../urls/navigation';
|
||||
|
||||
describe(
|
||||
'Threshold rules, Serverless essentials license',
|
||||
{
|
||||
tags: ['@serverless'],
|
||||
|
||||
env: {
|
||||
ftrConfig: {
|
||||
productTypes: [
|
||||
{ product_line: 'security', product_tier: 'essentials' },
|
||||
{ product_line: 'endpoint', product_tier: 'essentials' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
() => {
|
||||
beforeEach(() => {
|
||||
deleteAlertsAndRules();
|
||||
login();
|
||||
visit(CREATE_RULE_URL);
|
||||
});
|
||||
|
||||
it('Alert suppression is enabled for essentials', () => {
|
||||
selectThresholdRuleType();
|
||||
|
||||
cy.get(THRESHOLD_ENABLE_SUPPRESSION_CHECKBOX).should('be.enabled');
|
||||
});
|
||||
}
|
||||
);
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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 { getNewTermsRule } from '../../../../objects/rule';
|
||||
|
||||
import {
|
||||
SUPPRESS_FOR_DETAILS,
|
||||
DEFINITION_DETAILS,
|
||||
SUPPRESS_MISSING_FIELD,
|
||||
SUPPRESS_BY_DETAILS,
|
||||
} from '../../../../screens/rule_details';
|
||||
|
||||
import {
|
||||
ALERT_SUPPRESSION_DURATION_INPUT,
|
||||
ALERT_SUPPRESSION_FIELDS,
|
||||
ALERT_SUPPRESSION_MISSING_FIELDS_SUPPRESS,
|
||||
} from '../../../../screens/create_new_rule';
|
||||
|
||||
import { createRule } from '../../../../tasks/api_calls/rules';
|
||||
|
||||
import { RULES_MANAGEMENT_URL } from '../../../../urls/rules_management';
|
||||
import { getDetails } from '../../../../tasks/rule_details';
|
||||
import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
|
||||
import { login } from '../../../../tasks/login';
|
||||
|
||||
import { editFirstRule } from '../../../../tasks/alerts_detection_rules';
|
||||
|
||||
import { saveEditedRule } from '../../../../tasks/edit_rule';
|
||||
import {
|
||||
selectAlertSuppressionPerRuleExecution,
|
||||
selectDoNotSuppressForMissingFields,
|
||||
fillAlertSuppressionFields,
|
||||
} from '../../../../tasks/create_new_rule';
|
||||
import { visit } from '../../../../tasks/navigation';
|
||||
|
||||
const SUPPRESS_BY_FIELDS = ['agent.hostname', 'agent.type'];
|
||||
|
||||
const rule = getNewTermsRule();
|
||||
|
||||
describe(
|
||||
'Detection rules, New terms, Edit',
|
||||
{
|
||||
tags: ['@ess', '@serverless'],
|
||||
env: {
|
||||
// alertSuppressionForNewTermsRuleEnabled feature flag is also enabled in a global config
|
||||
kbnServerArgs: [
|
||||
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
|
||||
'alertSuppressionForNewTermsRuleEnabled',
|
||||
])}`,
|
||||
],
|
||||
},
|
||||
},
|
||||
() => {
|
||||
beforeEach(() => {
|
||||
login();
|
||||
deleteAlertsAndRules();
|
||||
});
|
||||
|
||||
describe('with suppression configured', () => {
|
||||
beforeEach(() => {
|
||||
createRule({
|
||||
...rule,
|
||||
alert_suppression: {
|
||||
group_by: SUPPRESS_BY_FIELDS.slice(0, 1),
|
||||
duration: { value: 20, unit: 'm' },
|
||||
missing_fields_strategy: 'suppress',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('displays suppress options correctly on edit form and allows its editing', () => {
|
||||
visit(RULES_MANAGEMENT_URL);
|
||||
editFirstRule();
|
||||
|
||||
// check saved suppression settings
|
||||
cy.get(ALERT_SUPPRESSION_DURATION_INPUT)
|
||||
.eq(0)
|
||||
.should('be.enabled')
|
||||
.should('have.value', 20);
|
||||
cy.get(ALERT_SUPPRESSION_DURATION_INPUT)
|
||||
.eq(1)
|
||||
.should('be.enabled')
|
||||
.should('have.value', 'm');
|
||||
cy.get(ALERT_SUPPRESSION_FIELDS).should('contain', SUPPRESS_BY_FIELDS.slice(0, 1).join(''));
|
||||
cy.get(ALERT_SUPPRESSION_MISSING_FIELDS_SUPPRESS).should('be.checked');
|
||||
|
||||
selectAlertSuppressionPerRuleExecution();
|
||||
selectDoNotSuppressForMissingFields();
|
||||
fillAlertSuppressionFields(SUPPRESS_BY_FIELDS.slice(1));
|
||||
|
||||
saveEditedRule();
|
||||
|
||||
cy.get(DEFINITION_DETAILS).within(() => {
|
||||
getDetails(SUPPRESS_BY_DETAILS).should('have.text', SUPPRESS_BY_FIELDS.join(''));
|
||||
getDetails(SUPPRESS_FOR_DETAILS).should('have.text', 'One rule execution');
|
||||
getDetails(SUPPRESS_MISSING_FIELD).should(
|
||||
'have.text',
|
||||
'Do not suppress alerts for events with missing fields'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
|
@ -326,6 +326,7 @@ describe('Detection rules, Prebuilt Rules Installation and Update workflow', ()
|
|||
$state: { store: 'appState' },
|
||||
},
|
||||
],
|
||||
alert_suppression: undefined,
|
||||
});
|
||||
|
||||
const ESQL_RULE = createRuleAssetSavedObject({
|
||||
|
|
|
@ -32,6 +32,8 @@ import { convertHistoryStartToSize, getHumanizedDuration } from '../helpers/rule
|
|||
import {
|
||||
ABOUT_CONTINUE_BTN,
|
||||
ALERT_SUPPRESSION_DURATION_INPUT,
|
||||
ALERT_SUPPRESSION_FIELDS,
|
||||
ALERT_SUPPRESSION_FIELDS_INPUT,
|
||||
ALERT_SUPPRESSION_FIELDS_COMBO_BOX,
|
||||
ALERT_SUPPRESSION_MISSING_FIELDS_DO_NOT_SUPPRESS,
|
||||
THRESHOLD_ENABLE_SUPPRESSION_CHECKBOX,
|
||||
|
@ -145,6 +147,7 @@ import { ruleFields } from '../data/detection_engine';
|
|||
import { waitForAlerts } from './alerts';
|
||||
import { refreshPage } from './security_header';
|
||||
import { EMPTY_ALERT_TABLE } from '../screens/alerts';
|
||||
import { TOOLTIP } from '../screens/common';
|
||||
|
||||
export const createAndEnableRule = () => {
|
||||
cy.get(CREATE_AND_ENABLE_BTN).click();
|
||||
|
@ -541,14 +544,12 @@ export const fillDefineEqlRuleAndContinue = (rule: EqlRuleCreateProps) => {
|
|||
cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true });
|
||||
};
|
||||
|
||||
export const fillDefineNewTermsRuleAndContinue = (rule: NewTermsRuleCreateProps) => {
|
||||
export const fillDefineNewTermsRule = (rule: NewTermsRuleCreateProps) => {
|
||||
cy.get(CUSTOM_QUERY_INPUT)
|
||||
.first()
|
||||
.type(rule.query || '');
|
||||
cy.get(NEW_TERMS_INPUT_AREA).find(INPUT).click();
|
||||
cy.get(NEW_TERMS_INPUT_AREA).find(INPUT).type(rule.new_terms_fields[0], { delay: 35 });
|
||||
|
||||
cy.get(EUI_FILTER_SELECT_ITEM).click();
|
||||
cy.get(NEW_TERMS_INPUT_AREA).find(INPUT).type(`${rule.new_terms_fields[0]}{enter}`);
|
||||
|
||||
cy.focused().type('{esc}'); // Close combobox dropdown so next inputs can be interacted with
|
||||
const historySize = convertHistoryStartToSize(rule.history_window_start);
|
||||
|
@ -557,6 +558,10 @@ export const fillDefineNewTermsRuleAndContinue = (rule: NewTermsRuleCreateProps)
|
|||
cy.get(NEW_TERMS_INPUT_AREA).find(NEW_TERMS_HISTORY_SIZE).type('{selectAll}');
|
||||
cy.get(NEW_TERMS_INPUT_AREA).find(NEW_TERMS_HISTORY_SIZE).type(historySizeNumber);
|
||||
cy.get(NEW_TERMS_INPUT_AREA).find(NEW_TERMS_HISTORY_TIME_TYPE).select(historySizeType);
|
||||
};
|
||||
|
||||
export const fillDefineNewTermsRuleAndContinue = (rule: NewTermsRuleCreateProps) => {
|
||||
fillDefineNewTermsRule(rule);
|
||||
cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true });
|
||||
};
|
||||
|
||||
|
@ -874,6 +879,18 @@ export const setAlertSuppressionDuration = (interval: number, timeUnit: 's' | 'm
|
|||
cy.get(ALERT_SUPPRESSION_DURATION_INPUT).eq(1).select(timeUnit);
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens tooltip on disabled suppression fields and checks if it contains requirement for Platinum license.
|
||||
*
|
||||
* Suppression fields are disabled when app has insufficient license
|
||||
*/
|
||||
export const openSuppressionFieldsTooltipAndCheckLicense = () => {
|
||||
cy.get(ALERT_SUPPRESSION_FIELDS_INPUT).should('be.disabled');
|
||||
cy.get(ALERT_SUPPRESSION_FIELDS).trigger('mouseover');
|
||||
// Platinum license is required, tooltip on disabled alert suppression checkbox should tell this
|
||||
cy.get(TOOLTIP).contains('Platinum license');
|
||||
};
|
||||
|
||||
export const checkLoadQueryDynamically = () => {
|
||||
cy.get(LOAD_QUERY_DYNAMICALLY_CHECKBOX).click({ force: true });
|
||||
cy.get(LOAD_QUERY_DYNAMICALLY_CHECKBOX).should('be.checked');
|
||||
|
|
|
@ -34,6 +34,9 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
{ product_line: 'endpoint', product_tier: 'complete' },
|
||||
{ product_line: 'cloud', product_tier: 'complete' },
|
||||
])}`,
|
||||
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
|
||||
'alertSuppressionForNewTermsRuleEnabled',
|
||||
])}`,
|
||||
],
|
||||
},
|
||||
testRunner: SecuritySolutionConfigurableCypressTestRunner,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue