diff --git a/packages/kbn-alerts-as-data-utils/src/schemas/generated/security_schema.ts b/packages/kbn-alerts-as-data-utils/src/schemas/generated/security_schema.ts index 5b79568cb485b..1d141def7f511 100644 --- a/packages/kbn-alerts-as-data-utils/src/schemas/generated/security_schema.ts +++ b/packages/kbn-alerts-as-data-utils/src/schemas/generated/security_schema.ts @@ -131,6 +131,7 @@ const SecurityAlertOptional = rt.partial({ 'kibana.alert.flapping_history': schemaBooleanArray, 'kibana.alert.group.id': schemaString, 'kibana.alert.group.index': schemaNumber, + 'kibana.alert.host.criticality_level': schemaString, 'kibana.alert.last_detected': schemaDate, 'kibana.alert.maintenance_window_ids': schemaStringArray, 'kibana.alert.new_terms': schemaStringArray, @@ -193,6 +194,7 @@ const SecurityAlertOptional = rt.partial({ ), 'kibana.alert.time_range': schemaDateRange, 'kibana.alert.url': schemaString, + 'kibana.alert.user.criticality_level': schemaString, 'kibana.alert.workflow_assignee_ids': schemaStringArray, 'kibana.alert.workflow_reason': schemaString, 'kibana.alert.workflow_status': schemaString, diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/alerts/8.13.0/index.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/alerts/8.13.0/index.ts new file mode 100644 index 0000000000000..594dc685097db --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/alerts/8.13.0/index.ts @@ -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 type { AlertWithCommonFields800 } from '@kbn/rule-registry-plugin/common/schemas/8.0.0'; +import type { + ALERT_HOST_CRITICALITY, + ALERT_USER_CRITICALITY, +} from '../../../../../field_maps/field_names'; +import type { + Ancestor8120, + BaseFields8120, + EqlBuildingBlockFields8120, + EqlShellFields8120, + NewTermsFields8120, +} from '../8.12.0'; + +/* DO NOT MODIFY THIS SCHEMA TO ADD NEW FIELDS. These types represent the alerts that shipped in 8.13.0. +Any changes to these types should be bug fixes so the types more accurately represent the alerts from 8.13.0. +If you are adding new fields for a new release of Kibana, create a new sibling folder to this one +for the version to be released and add the field(s) to the schema in that folder. +Then, update `../index.ts` to import from the new folder that has the latest schemas, add the +new schemas to the union of all alert schemas, and re-export the new schemas as the `*Latest` schemas. +*/ + +export type { Ancestor8120 as Ancestor8130 }; + +export interface BaseFields8130 extends BaseFields8120 { + [ALERT_HOST_CRITICALITY]: string | undefined; + [ALERT_USER_CRITICALITY]: string | undefined; +} + +export interface WrappedFields8130 { + _id: string; + _index: string; + _source: T; +} + +export type GenericAlert8130 = AlertWithCommonFields800; + +export type EqlShellFields8130 = EqlShellFields8120 & BaseFields8130; + +export type EqlBuildingBlockFields8130 = EqlBuildingBlockFields8120 & BaseFields8130; + +export type NewTermsFields8130 = NewTermsFields8120 & BaseFields8130; + +export type NewTermsAlert8130 = NewTermsFields8120 & BaseFields8130; + +export type EqlBuildingBlockAlert8130 = AlertWithCommonFields800; + +export type EqlShellAlert8130 = AlertWithCommonFields800; + +export type DetectionAlert8130 = + | GenericAlert8130 + | EqlShellAlert8130 + | EqlBuildingBlockAlert8130 + | NewTermsAlert8130; diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/alerts/index.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/alerts/index.ts index 742e5fd4ecfc1..6bf7b1d5dfd7e 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/alerts/index.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/alerts/index.ts @@ -12,15 +12,16 @@ import type { DetectionAlert860 } from './8.6.0'; import type { DetectionAlert870 } from './8.7.0'; import type { DetectionAlert880 } from './8.8.0'; import type { DetectionAlert890 } from './8.9.0'; +import type { DetectionAlert8120 } from './8.12.0'; import type { - Ancestor8120, - BaseFields8120, - DetectionAlert8120, - EqlBuildingBlockFields8120, - EqlShellFields8120, - NewTermsFields8120, - WrappedFields8120, -} from './8.12.0'; + Ancestor8130, + BaseFields8130, + DetectionAlert8130, + EqlBuildingBlockFields8130, + EqlShellFields8130, + NewTermsFields8130, + WrappedFields8130, +} from './8.13.0'; // When new Alert schemas are created for new Kibana versions, add the DetectionAlert type from the new version // here, e.g. `export type DetectionAlert = DetectionAlert800 | DetectionAlert820` if a new schema is created in 8.2.0 @@ -31,14 +32,15 @@ export type DetectionAlert = | DetectionAlert870 | DetectionAlert880 | DetectionAlert890 - | DetectionAlert8120; + | DetectionAlert8120 + | DetectionAlert8130; export type { - Ancestor8120 as AncestorLatest, - BaseFields8120 as BaseFieldsLatest, - DetectionAlert8120 as DetectionAlertLatest, - WrappedFields8120 as WrappedFieldsLatest, - EqlBuildingBlockFields8120 as EqlBuildingBlockFieldsLatest, - EqlShellFields8120 as EqlShellFieldsLatest, - NewTermsFields8120 as NewTermsFieldsLatest, + Ancestor8130 as AncestorLatest, + BaseFields8130 as BaseFieldsLatest, + DetectionAlert8130 as DetectionAlertLatest, + WrappedFields8130 as WrappedFieldsLatest, + EqlBuildingBlockFields8130 as EqlBuildingBlockFieldsLatest, + EqlShellFields8130 as EqlShellFieldsLatest, + NewTermsFields8130 as NewTermsFieldsLatest, }; diff --git a/x-pack/plugins/security_solution/common/field_maps/8.13.0/alerts.ts b/x-pack/plugins/security_solution/common/field_maps/8.13.0/alerts.ts new file mode 100644 index 0000000000000..86c84092891b8 --- /dev/null +++ b/x-pack/plugins/security_solution/common/field_maps/8.13.0/alerts.ts @@ -0,0 +1,33 @@ +/* + * 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 { alertsFieldMap840 } from '../8.4.0'; +import { ALERT_HOST_CRITICALITY, ALERT_USER_CRITICALITY } from '../field_names'; + +export const alertsFieldMap8130 = { + ...alertsFieldMap840, + /** + * Stores the criticality level for the host, as determined by analysts, in relation to the alert. + * The Criticality level is copied from the asset criticality index. + */ + [ALERT_HOST_CRITICALITY]: { + type: 'keyword', + array: false, + required: false, + }, + /** + * Stores the criticality level for the user, as determined by analysts, in relation to the alert. + * The Criticality level is copied from the asset criticality index. + */ + [ALERT_USER_CRITICALITY]: { + type: 'keyword', + array: false, + required: false, + }, +} as const; + +export type AlertsFieldMap8130 = typeof alertsFieldMap8130; diff --git a/x-pack/plugins/security_solution/common/field_maps/8.13.0/index.ts b/x-pack/plugins/security_solution/common/field_maps/8.13.0/index.ts new file mode 100644 index 0000000000000..291ca7f8dff82 --- /dev/null +++ b/x-pack/plugins/security_solution/common/field_maps/8.13.0/index.ts @@ -0,0 +1,11 @@ +/* + * 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 { AlertsFieldMap8130 } from './alerts'; +import { alertsFieldMap8130 } from './alerts'; +export type { AlertsFieldMap8130 }; +export { alertsFieldMap8130 }; diff --git a/x-pack/plugins/security_solution/common/field_maps/field_names.ts b/x-pack/plugins/security_solution/common/field_maps/field_names.ts index 53ebfc5c188d1..6124cc08ebd2b 100644 --- a/x-pack/plugins/security_solution/common/field_maps/field_names.ts +++ b/x-pack/plugins/security_solution/common/field_maps/field_names.ts @@ -17,6 +17,8 @@ export const ALERT_THRESHOLD_RESULT = `${ALERT_NAMESPACE}.threshold_result` as c export const ALERT_THRESHOLD_RESULT_COUNT = `${ALERT_THRESHOLD_RESULT}.count` as const; export const ALERT_NEW_TERMS = `${ALERT_NAMESPACE}.new_terms` as const; export const ALERT_NEW_TERMS_FIELDS = `${ALERT_RULE_PARAMETERS}.new_terms_fields` as const; +export const ALERT_HOST_CRITICALITY = `${ALERT_NAMESPACE}.host.criticality_level` as const; +export const ALERT_USER_CRITICALITY = `${ALERT_NAMESPACE}.user.criticality_level` as const; export const ALERT_ORIGINAL_EVENT = `${ALERT_NAMESPACE}.original_event` as const; export const ALERT_ORIGINAL_EVENT_ACTION = `${ALERT_ORIGINAL_EVENT}.action` as const; diff --git a/x-pack/plugins/security_solution/common/field_maps/index.ts b/x-pack/plugins/security_solution/common/field_maps/index.ts index c6780a33fc64f..fe903776d1dd4 100644 --- a/x-pack/plugins/security_solution/common/field_maps/index.ts +++ b/x-pack/plugins/security_solution/common/field_maps/index.ts @@ -5,9 +5,9 @@ * 2.0. */ -import type { AlertsFieldMap840 } from './8.4.0'; -import { alertsFieldMap840 } from './8.4.0'; +import type { AlertsFieldMap8130 } from './8.13.0'; +import { alertsFieldMap8130 } from './8.13.0'; import type { RulesFieldMap } from './8.0.0/rules'; import { rulesFieldMap } from './8.0.0/rules'; -export type { AlertsFieldMap840 as AlertsFieldMap, RulesFieldMap }; -export { alertsFieldMap840 as alertsFieldMap, rulesFieldMap }; +export type { AlertsFieldMap8130 as AlertsFieldMap, RulesFieldMap }; +export { alertsFieldMap8130 as alertsFieldMap, rulesFieldMap }; diff --git a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts index bfce842096448..384c6bf955e51 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts +++ b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts @@ -6,6 +6,10 @@ */ import type { EuiDataGridColumn } from '@elastic/eui'; +import { + ALERT_HOST_CRITICALITY, + ALERT_USER_CRITICALITY, +} from '../../../../common/field_maps/field_names'; import type { LicenseService } from '../../../../common/license'; import type { ColumnHeaderOptions } from '../../../../common/types'; @@ -72,6 +76,18 @@ const getBaseColumns = ( id: 'user.risk.calculated_level', } : null, + isPlatinumPlus + ? { + columnHeaderType: defaultColumnHeaderType, + id: ALERT_HOST_CRITICALITY, + } + : null, + isPlatinumPlus + ? { + columnHeaderType: defaultColumnHeaderType, + id: ALERT_USER_CRITICALITY, + } + : null, { columnHeaderType: defaultColumnHeaderType, id: 'process.name', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts index 846c714a9c099..024f1b123ff99 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts @@ -80,6 +80,8 @@ import { ALERT_RULE_THREAT, ALERT_RULE_EXCEPTIONS_LIST, ALERT_RULE_IMMUTABLE, + ALERT_HOST_CRITICALITY, + ALERT_USER_CRITICALITY, } from '../../../../../../common/field_maps/field_names'; import type { CompleteRule, RuleParams } from '../../../rule_schema'; import { commonParamsCamelToSnake, typeSpecificCamelToSnake } from '../../../rule_management'; @@ -256,6 +258,9 @@ export const buildAlert = ( 'kibana.alert.rule.risk_score': params.riskScore, 'kibana.alert.rule.severity': params.severity, 'kibana.alert.rule.building_block_type': params.buildingBlockType, + // asset criticality fields will be enriched before ingestion + [ALERT_HOST_CRITICALITY]: undefined, + [ALERT_USER_CRITICALITY]: undefined, }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/__mocks__/alerts.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/__mocks__/alerts.ts index e19e7ad1bc0ee..efbf39d815aea 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/__mocks__/alerts.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/__mocks__/alerts.ts @@ -68,6 +68,8 @@ import { ALERT_RULE_TIMELINE_TITLE, ALERT_RULE_INDICES, ALERT_RULE_TIMESTAMP_OVERRIDE, + ALERT_HOST_CRITICALITY, + ALERT_USER_CRITICALITY, } from '../../../../../../../common/field_maps/field_names'; export const createAlert = ( @@ -194,6 +196,8 @@ export const createAlert = ( rule_name_override: undefined, timestamp_override: undefined, }, + [ALERT_HOST_CRITICALITY]: undefined, + [ALERT_USER_CRITICALITY]: undefined, ...data, }, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/create_single_field_match_enrichment.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/create_single_field_match_enrichment.ts index 982de01b8bae7..874556fb94dae 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/create_single_field_match_enrichment.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/create_single_field_match_enrichment.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { flatten, chunk } from 'lodash'; +import { chunk } from 'lodash'; import { searchEnrichments } from './search_enrichments'; import { makeSingleFieldMatchQuery } from './utils/requests'; import { getEventValue, getFieldValue } from './utils/events'; @@ -22,12 +22,14 @@ export const createSingleFieldMatchEnrichment: CreateFieldsMatchEnrichment = asy createEnrichmentFunction, name, enrichmentResponseFields, + extraFilters, }) => { try { logger.debug(`Enrichment ${name}: started`); - const eventsWithField = events.filter((event) => getEventValue(event, mappingField.eventField)); - const eventsMapByFieldValue = eventsWithField.reduce((acc, event) => { + const eventsToEnrich = events.filter((event) => getEventValue(event, mappingField.eventField)); + + const eventsMapByFieldValue = eventsToEnrich.reduce((acc, event) => { const eventFieldValue = getEventValue(event, mappingField.eventField); if (!eventFieldValue) return {}; @@ -39,6 +41,7 @@ export const createSingleFieldMatchEnrichment: CreateFieldsMatchEnrichment = asy }, {} as { [key: string]: typeof events }); const uniqueEventsValuesToSearchBy = Object.keys(eventsMapByFieldValue); + const chunksUniqueEventsValuesToSearchBy = chunk(uniqueEventsValuesToSearchBy, MAX_CLAUSES); const getAllEnrichment = chunksUniqueEventsValuesToSearchBy @@ -46,6 +49,7 @@ export const createSingleFieldMatchEnrichment: CreateFieldsMatchEnrichment = asy makeSingleFieldMatchQuery({ values: enrichmentValuesChunk, searchByField: mappingField.enrichmentField, + extraFilters, }) ) .filter((query) => query.query?.bool?.should?.length > 0) @@ -59,11 +63,9 @@ export const createSingleFieldMatchEnrichment: CreateFieldsMatchEnrichment = asy }) ); - const enrichmentsResults = (await Promise.allSettled(getAllEnrichment)) + const enrichments = (await Promise.allSettled(getAllEnrichment)) .filter((result) => result.status === 'fulfilled') - .map((result) => (result as PromiseFulfilledResult)?.value); - - const enrichments = flatten(enrichmentsResults); + .flatMap((result) => (result as PromiseFulfilledResult)?.value); if (enrichments.length === 0) { logger.debug(`Enrichment ${name}: no enrichment found`); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/enrichment_by_type/asset_criticality.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/enrichment_by_type/asset_criticality.ts new file mode 100644 index 0000000000000..e2bd3319062ae --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/enrichment_by_type/asset_criticality.ts @@ -0,0 +1,90 @@ +/* + * 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_HOST_CRITICALITY, + ALERT_USER_CRITICALITY, +} from '../../../../../../../common/field_maps/field_names'; +import { createSingleFieldMatchEnrichment } from '../create_single_field_match_enrichment'; +import type { CreateCriticalityEnrichment, CreateEnrichmentFunction } from '../types'; +import { getFieldValue } from '../utils/events'; +import { getAssetCriticalityIndex } from '../../../../../../../common/entity_analytics/asset_criticality'; + +const enrichmentResponseFields = ['id_value', 'criticality_level']; + +const getExtraFiltersForEnrichment = (field: string) => [ + { + match: { + id_field: { + query: field, + }, + }, + }, +]; + +const createEnrichmentFactoryFunction = + ( + alertField: typeof ALERT_HOST_CRITICALITY | typeof ALERT_USER_CRITICALITY + ): CreateEnrichmentFunction => + (enrichment) => + (event) => { + const criticality = getFieldValue(enrichment, 'criticality_level'); + + if (!criticality) { + return event; + } + const newEvent = cloneDeep(event); + if (criticality && newEvent._source) { + newEvent._source[alertField] = criticality; + } + return newEvent; + }; + +export const createHostAssetCriticalityEnrichments: CreateCriticalityEnrichment = async ({ + services, + logger, + events, + spaceId, +}) => { + return createSingleFieldMatchEnrichment({ + name: 'Host Asset Criticality', + index: [getAssetCriticalityIndex(spaceId)], + services, + logger, + events, + mappingField: { + eventField: 'host.name', + enrichmentField: 'id_value', + }, + enrichmentResponseFields, + extraFilters: getExtraFiltersForEnrichment('host.name'), + createEnrichmentFunction: createEnrichmentFactoryFunction(ALERT_HOST_CRITICALITY), + }); +}; + +export const createUserAssetCriticalityEnrichments: CreateCriticalityEnrichment = async ({ + services, + logger, + events, + spaceId, +}) => { + return createSingleFieldMatchEnrichment({ + name: 'User Asset Criticality', + index: [getAssetCriticalityIndex(spaceId)], + services, + logger, + events, + mappingField: { + eventField: 'user.name', + enrichmentField: 'id_value', + }, + enrichmentResponseFields, + extraFilters: getExtraFiltersForEnrichment('user.name'), + createEnrichmentFunction: createEnrichmentFactoryFunction(ALERT_USER_CRITICALITY), + }); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/enrichment_by_type/host_risk.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/enrichment_by_type/host_risk.ts index 6b18979c0d3c0..1b34f6cb87859 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/enrichment_by_type/host_risk.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/enrichment_by_type/host_risk.ts @@ -10,23 +10,9 @@ import { cloneDeep } from 'lodash'; import { getHostRiskIndex } from '../../../../../../../common/search_strategy/security_solution/risk_score/common'; import { RiskScoreFields } from '../../../../../../../common/search_strategy/security_solution/risk_score/all'; import { createSingleFieldMatchEnrichment } from '../create_single_field_match_enrichment'; -import type { CreateRiskEnrichment, GetIsRiskScoreAvailable } from '../types'; +import type { CreateRiskEnrichment } from '../types'; import { getFieldValue } from '../utils/events'; -export const getIsHostRiskScoreAvailable: GetIsRiskScoreAvailable = async ({ - spaceId, - services, - isNewRiskScoreModuleInstalled, -}) => { - const isHostRiskScoreIndexExist = await services.scopedClusterClient.asCurrentUser.indices.exists( - { - index: getHostRiskIndex(spaceId, true, isNewRiskScoreModuleInstalled), - } - ); - - return isHostRiskScoreIndexExist; -}; - export const createHostRiskEnrichments: CreateRiskEnrichment = async ({ services, logger, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/enrichment_by_type/user_risk.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/enrichment_by_type/user_risk.ts index b0e8d87f3019f..27ae894f28134 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/enrichment_by_type/user_risk.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/enrichment_by_type/user_risk.ts @@ -9,23 +9,9 @@ import { cloneDeep } from 'lodash'; import { getUserRiskIndex } from '../../../../../../../common/search_strategy/security_solution/risk_score/common'; import { RiskScoreFields } from '../../../../../../../common/search_strategy/security_solution/risk_score/all'; import { createSingleFieldMatchEnrichment } from '../create_single_field_match_enrichment'; -import type { CreateRiskEnrichment, GetIsRiskScoreAvailable } from '../types'; +import type { CreateRiskEnrichment } from '../types'; import { getFieldValue } from '../utils/events'; -export const getIsUserRiskScoreAvailable: GetIsRiskScoreAvailable = async ({ - services, - spaceId, - isNewRiskScoreModuleInstalled, -}) => { - const isUserRiskScoreIndexExist = await services.scopedClusterClient.asCurrentUser.indices.exists( - { - index: getUserRiskIndex(spaceId, true, isNewRiskScoreModuleInstalled), - } - ); - - return isUserRiskScoreIndexExist; -}; - export const createUserRiskEnrichments: CreateRiskEnrichment = async ({ services, logger, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/index.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/index.test.ts index 8f98b5bfe04b7..4c87c6f5a8272 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/index.test.ts @@ -11,26 +11,20 @@ import { enrichEvents } from '.'; import { searchEnrichments } from './search_enrichments'; import { ruleExecutionLogMock } from '../../../rule_monitoring/mocks'; import { createAlert } from './__mocks__/alerts'; -import { getIsHostRiskScoreAvailable } from './enrichment_by_type/host_risk'; -import { getIsUserRiskScoreAvailable } from './enrichment_by_type/user_risk'; +import { isIndexExist } from './utils/is_index_exist'; + +import { allowedExperimentalValues } from '../../../../../../common'; jest.mock('./search_enrichments', () => ({ searchEnrichments: jest.fn(), })); const mockSearchEnrichments = searchEnrichments as jest.Mock; -jest.mock('./enrichment_by_type/host_risk', () => ({ - ...jest.requireActual('./enrichment_by_type/host_risk'), - getIsHostRiskScoreAvailable: jest.fn(), -})); -const mockGetIsHostRiskScoreAvailable = getIsHostRiskScoreAvailable as jest.Mock; - -jest.mock('./enrichment_by_type/user_risk', () => ({ - ...jest.requireActual('./enrichment_by_type/user_risk'), - getIsUserRiskScoreAvailable: jest.fn(), +jest.mock('./utils/is_index_exist', () => ({ + isIndexExist: jest.fn(), })); -const mockGetIsUserRiskScoreAvailable = getIsUserRiskScoreAvailable as jest.Mock; +const mockIsIndexExist = isIndexExist as jest.Mock; const hostEnrichmentResponse = [ { @@ -66,6 +60,30 @@ const userEnrichmentResponse = [ }, ]; +const assetCriticalityUserResponse = [ + { + fields: { + id_value: ['user name 1'], + criticality_level: ['important'], + }, + }, +]; + +const assetCriticalityHostResponse = [ + { + fields: { + id_value: ['host name 2'], + criticality_level: ['very_important'], + }, + }, + { + fields: { + id_value: ['host name 1'], + criticality_level: ['low'], + }, + }, +]; + describe('enrichEvents', () => { let ruleExecutionLogger: ReturnType; let alertServices: RuleExecutorServicesMock; @@ -76,11 +94,13 @@ describe('enrichEvents', () => { ruleExecutionLogger = ruleExecutionLogMock.forExecutors.create(); alertServices = alertsMock.createRuleExecutorServices(); }); + afterEach(() => { + mockIsIndexExist.mockClear(); + }); it('return the same events, if risk indexes are not available', async () => { mockSearchEnrichments.mockImplementation(() => []); - mockGetIsUserRiskScoreAvailable.mockImplementation(() => false); - mockGetIsHostRiskScoreAvailable.mockImplementation(() => false); + mockIsIndexExist.mockImplementation(() => false); const events = [ createAlert('1', createEntity('host', 'host name')), createAlert('2', createEntity('user', 'user name')), @@ -97,8 +117,7 @@ describe('enrichEvents', () => { it('return the same events, if there no fields', async () => { mockSearchEnrichments.mockImplementation(() => []); - mockGetIsUserRiskScoreAvailable.mockImplementation(() => true); - mockGetIsHostRiskScoreAvailable.mockImplementation(() => true); + mockIsIndexExist.mockImplementation(() => true); const events = [createAlert('1'), createAlert('2')]; const enrichedEvents = await enrichEvents({ logger: ruleExecutionLogger, @@ -110,12 +129,11 @@ describe('enrichEvents', () => { expect(enrichedEvents).toEqual(events); }); - it('return enriched events', async () => { + it('return enriched events with risk score', async () => { mockSearchEnrichments .mockReturnValueOnce(hostEnrichmentResponse) .mockReturnValueOnce(userEnrichmentResponse); - mockGetIsUserRiskScoreAvailable.mockImplementation(() => true); - mockGetIsHostRiskScoreAvailable.mockImplementation(() => true); + mockIsIndexExist.mockImplementation(() => true); const enrichedEvents = await enrichEvents({ logger: ruleExecutionLogger, @@ -159,14 +177,57 @@ describe('enrichEvents', () => { ]); }); + it('return enriched events with asset criticality', async () => { + mockSearchEnrichments + .mockReturnValueOnce(assetCriticalityUserResponse) + .mockReturnValueOnce(assetCriticalityHostResponse); + + // disable risk score enrichments + mockIsIndexExist.mockImplementationOnce(() => false); + mockIsIndexExist.mockImplementationOnce(() => false); + mockIsIndexExist.mockImplementationOnce(() => false); + // enable for asset criticality + mockIsIndexExist.mockImplementation(() => true); + + const enrichedEvents = await enrichEvents({ + logger: ruleExecutionLogger, + services: alertServices, + events: [ + createAlert('1', { + ...createEntity('host', 'host name 1'), + ...createEntity('user', 'user name 1'), + }), + createAlert('2', createEntity('host', 'user name 1')), + ], + spaceId: 'default', + experimentalFeatures: { + ...allowedExperimentalValues, + entityAnalyticsAssetCriticalityEnabled: true, + }, + }); + + expect(enrichedEvents).toEqual([ + createAlert('1', { + ...createEntity('user', 'user name 1'), + ...createEntity('host', 'host name 1'), + + 'kibana.alert.host.criticality_level': 'low', + 'kibana.alert.user.criticality_level': 'important', + }), + createAlert('2', { + ...createEntity('host', 'user name 1'), + }), + ]); + }); + it('if some enrichments failed, another work as expected', async () => { mockSearchEnrichments .mockImplementationOnce(() => { throw new Error('1'); }) .mockImplementationOnce(() => userEnrichmentResponse); - mockGetIsUserRiskScoreAvailable.mockImplementation(() => true); - mockGetIsHostRiskScoreAvailable.mockImplementation(() => true); + mockIsIndexExist.mockImplementation(() => true); + mockIsIndexExist.mockImplementation(() => true); const enrichedEvents = await enrichEvents({ logger: ruleExecutionLogger, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/index.ts index a27cc55801820..cfd51f21e20cd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/index.ts @@ -5,21 +5,26 @@ * 2.0. */ -import { - createHostRiskEnrichments, - getIsHostRiskScoreAvailable, -} from './enrichment_by_type/host_risk'; +import { createHostRiskEnrichments } from './enrichment_by_type/host_risk'; + +import { createUserRiskEnrichments } from './enrichment_by_type/user_risk'; import { - createUserRiskEnrichments, - getIsUserRiskScoreAvailable, -} from './enrichment_by_type/user_risk'; + createHostAssetCriticalityEnrichments, + createUserAssetCriticalityEnrichments, +} from './enrichment_by_type/asset_criticality'; +import { getAssetCriticalityIndex } from '../../../../../../common/entity_analytics/asset_criticality'; import type { EnrichEventsFunction, EventsMapByEnrichments, CreateEnrichEventsFunction, } from './types'; import { applyEnrichmentsToEvents } from './utils/transforms'; +import { isIndexExist } from './utils/is_index_exist'; +import { + getHostRiskIndex, + getUserRiskIndex, +} from '../../../../../../common/search_strategy/security_solution/risk_score/common'; export const enrichEvents: EnrichEventsFunction = async ({ services, @@ -29,23 +34,30 @@ export const enrichEvents: EnrichEventsFunction = async ({ experimentalFeatures, }) => { try { - const enrichments = []; + const enrichments: Array> = []; logger.debug('Alert enrichments started'); const isNewRiskScoreModuleAvailable = experimentalFeatures?.riskScoringRoutesEnabled ?? false; + const isAssetCriticalityEnabled = + experimentalFeatures?.entityAnalyticsAssetCriticalityEnabled ?? false; let isNewRiskScoreModuleInstalled = false; if (isNewRiskScoreModuleAvailable) { - isNewRiskScoreModuleInstalled = await getIsHostRiskScoreAvailable({ - spaceId, + isNewRiskScoreModuleInstalled = await isIndexExist({ services, - isNewRiskScoreModuleInstalled: true, + index: getHostRiskIndex(spaceId, true, true), }); } const [isHostRiskScoreIndexExist, isUserRiskScoreIndexExist] = await Promise.all([ - getIsHostRiskScoreAvailable({ spaceId, services, isNewRiskScoreModuleInstalled }), - getIsUserRiskScoreAvailable({ spaceId, services, isNewRiskScoreModuleInstalled }), + isIndexExist({ + services, + index: getHostRiskIndex(spaceId, true, isNewRiskScoreModuleInstalled), + }), + isIndexExist({ + services, + index: getUserRiskIndex(spaceId, true, isNewRiskScoreModuleInstalled), + }), ]); if (isHostRiskScoreIndexExist) { @@ -72,9 +84,34 @@ export const enrichEvents: EnrichEventsFunction = async ({ ); } + if (isAssetCriticalityEnabled) { + const assetCriticalityIndexExist = await isIndexExist({ + services, + index: getAssetCriticalityIndex(spaceId), + }); + if (assetCriticalityIndexExist) { + enrichments.push( + createUserAssetCriticalityEnrichments({ + services, + logger, + events, + spaceId, + }) + ); + enrichments.push( + createHostAssetCriticalityEnrichments({ + services, + logger, + events, + spaceId, + }) + ); + } + } + const allEnrichmentsResults = await Promise.allSettled(enrichments); - const allFulfilledEnrichmentsResults = allEnrichmentsResults + const allFulfilledEnrichmentsResults: EventsMapByEnrichments[] = allEnrichmentsResults .filter((result) => result.status === 'fulfilled') .map((result) => (result as PromiseFulfilledResult)?.value); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/types.ts index 73c703235edca..70f710630da37 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/types.ts @@ -6,6 +6,7 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import type { Filter } from '@kbn/es-query'; import type { ExperimentalFeatures } from '../../../../../../common'; @@ -27,7 +28,6 @@ export type EnrichmentFunction = ( e: EventsForEnrichment ) => EventsForEnrichment; -// export interface EventsMapByEnrichments { [id: string]: EnrichmentFunction[]; } @@ -48,11 +48,6 @@ interface BasedEnrichParamters { events: Array>; } -interface SingleMappingField { - eventField: string; - enrichmentField: string; -} - export type GetEventValue = ( events: EventsForEnrichment, path: string @@ -60,9 +55,10 @@ export type GetEventValue = ( export type GetFieldValue = (events: EnrichmentType, path: string) => string | undefined; -export type MakeSingleFieldMatchQuery = (params: { +export type MakeSingleFieldMatchQuery = (params: { values: string[]; searchByField: string; + extraFilters?: QueryDslQueryContainer[]; }) => Filter; export type SearchEnrichments = (params: { @@ -79,6 +75,8 @@ export type GetIsRiskScoreAvailable = (params: { isNewRiskScoreModuleInstalled: boolean; }) => Promise; +export type IsIndexExist = (params: { services: RuleServices; index: string }) => Promise; + export type CreateRiskEnrichment = ( params: BasedEnrichParamters & { spaceId: string; @@ -86,13 +84,28 @@ export type CreateRiskEnrichment = ( } ) => Promise; +export type CreateCriticalityEnrichment = ( + params: BasedEnrichParamters & { + spaceId: string; + } +) => Promise; + +export type CreateEnrichmentFunction = (enrichmentDoc: EnrichmentType) => EnrichmentFunction; + export type CreateFieldsMatchEnrichment = ( params: BasedEnrichParamters & { name: string; index: string[]; - mappingField: SingleMappingField; + mappingField: { + /** The field on events which contains the value we'll use to build a query. */ + eventField: string; + /** Used in a `match` query to find documents that match the values of `eventField`. */ + enrichmentField: string; + }; + /** Specifies which fields should be returned when querying the enrichment index. */ enrichmentResponseFields: string[]; - createEnrichmentFunction: (enrichmentDoc: EnrichmentType) => EnrichmentFunction; + createEnrichmentFunction: CreateEnrichmentFunction; + extraFilters?: QueryDslQueryContainer[]; } ) => Promise; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/utils/events.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/utils/events.ts index 54eea14c6e94a..b08b0f2b362b8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/utils/events.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/utils/events.ts @@ -6,7 +6,8 @@ */ import { get } from 'lodash'; -import type { GetEventValue, GetFieldValue } from '../types'; +import type { BaseFieldsLatest } from '../../../../../../../common/api/detection_engine/model/alerts'; +import type { EventsForEnrichment, GetEventValue, GetFieldValue } from '../types'; export const getEventValue: GetEventValue = (event, path) => { const value = get(event, `_source.${path}`) || event?._source?.[path]; @@ -19,3 +20,30 @@ export const getEventValue: GetEventValue = (event, path) => { }; export const getFieldValue: GetFieldValue = (event, path) => get(event?.fields, path)?.[0]; + +/** Given an eventField, returns a map of values found in that field to the events that contain that value. */ +export function getEventsMapByFieldValue( + events: Array>, + eventField: string +): Record< + /** values found in mappingField.eventField */ string, + /** Array of events with the corresponding value */ typeof events +> { + const eventsWithField = events.filter((event) => getEventValue(event, eventField)); + + const eventsMapByFieldValue: Record< + /** values found in mappingField.eventField */ string, + /** Array of events with the corresponding value */ typeof events + > = eventsWithField.reduce((acc, event) => { + const eventFieldValue = getEventValue(event, eventField); + + if (!eventFieldValue) return {}; + + acc[eventFieldValue] ??= []; + acc[eventFieldValue].push(event); + + return acc; + }, {} as { [key: string]: typeof events }); + + return eventsMapByFieldValue; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/utils/is_index_exist.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/utils/is_index_exist.ts new file mode 100644 index 0000000000000..eb7813c350416 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/utils/is_index_exist.ts @@ -0,0 +1,17 @@ +/* + * 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 { IsIndexExist } from '../types'; + +export const isIndexExist: IsIndexExist = async ({ services, index }) => { + const isAssetCriticalityIndexExist = + await services.scopedClusterClient.asInternalUser.indices.exists({ + index, + }); + + return isAssetCriticalityIndexExist; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/utils/requests.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/utils/requests.test.ts index 8cae81c8ef3b0..b8f3253bb69f7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/utils/requests.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/utils/requests.test.ts @@ -20,6 +20,7 @@ describe('makeSingleFieldMatchQuery', () => { query: { bool: { should: [], + filter: [], minimum_should_match: 1, }, }, @@ -58,6 +59,46 @@ describe('makeSingleFieldMatchQuery', () => { }, }, ], + filter: [], + minimum_should_match: 1, + }, + }, + }); + }); + + it('return query with extra filters', () => { + expect( + makeSingleFieldMatchQuery({ + values: [], + searchByField: 'host.name', + extraFilters: [ + { + match: { + id_field: { + query: 'host.name', + }, + }, + }, + ], + }) + ).toEqual({ + meta: { + alias: null, + negate: false, + disabled: false, + }, + query: { + bool: { + should: [], + filter: [ + { + match: { + id_field: { + query: 'host.name', + }, + }, + }, + ], minimum_should_match: 1, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/utils/requests.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/utils/requests.ts index b4567481691b6..704fb89812e4a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/utils/requests.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/utils/requests.ts @@ -7,7 +7,12 @@ import type { MakeSingleFieldMatchQuery } from '../types'; -export const makeSingleFieldMatchQuery: MakeSingleFieldMatchQuery = ({ values, searchByField }) => { +/** makes a query that gets back any documents with the given `values` in the `searchByField` */ +export const makeSingleFieldMatchQuery: MakeSingleFieldMatchQuery = ({ + values, + searchByField, + extraFilters, +}) => { const shouldClauses = values.map((value) => ({ match: { [searchByField]: { @@ -26,6 +31,7 @@ export const makeSingleFieldMatchQuery: MakeSingleFieldMatchQuery = ({ values, s query: { bool: { should: shouldClauses, + filter: extraFilters ?? [], minimum_should_match: 1, }, }, diff --git a/x-pack/test/functional/es_archives/asset_criticality/data.json b/x-pack/test/functional/es_archives/asset_criticality/data.json new file mode 100644 index 0000000000000..dae5bd12006a8 --- /dev/null +++ b/x-pack/test/functional/es_archives/asset_criticality/data.json @@ -0,0 +1,143 @@ +{ + "type": "doc", + "value": { + "index": ".asset-criticality.asset-criticality-default", + "id": "1", + "source": { + "id_field": "host.name", + "id_value": "suricata-zeek-sensor-toronto", + "criticality_level": "important", + "@timestamp": "2022-08-12T14:45:36.171Z", + "updated_at": "2022-08-12T14:45:36.171Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "index": ".asset-criticality.asset-criticality-default", + "id": "2", + "source": { + "id_field": "host.name", + "id_value": "host-0", + "criticality_level": "very_important", + "@timestamp": "2022-08-12T14:45:36.171Z", + "updated_at": "2022-08-12T14:45:36.171Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "index": ".asset-criticality.asset-criticality-default", + "id": "20", + "source": { + "id_field": "host.name", + "id_value": "zeek-newyork-sha-aa8df15", + "criticality_level": "normal", + "@timestamp": "2022-08-12T14:45:36.171Z", + "updated_at": "2022-08-12T14:45:36.171Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "index": ".asset-criticality.asset-criticality-default", + "id": "21", + "source": { + "id_field": "host.name", + "id_value": "zeek-sensor-amsterdam", + "criticality_level": "low", + "@timestamp": "2022-08-12T14:45:36.171Z", + "updated_at": "2022-08-12T14:45:36.171Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "index": ".asset-criticality.asset-criticality-default", + "id": "22", + "source": { + "id_field": "host.name", + "id_value": "suricata-sensor-london", + "criticality_level": "important", + "@timestamp": "2022-08-12T14:45:36.171Z", + "updated_at": "2022-08-12T14:45:36.171Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "index": ".asset-criticality.asset-criticality-default", + "id": "3", + "source": { + "id_field": "user.name", + "id_value": "root", + "criticality_level": "very_important", + "@timestamp": "2022-08-12T14:45:36.171Z", + "updated_at": "2022-08-12T14:45:36.171Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "index": ".asset-criticality.asset-criticality-default", + "id": "4", + "source": { + "id_field": "user.name", + "id_value": "User 2", + "criticality_level": "important", + "@timestamp": "2022-08-12T14:45:36.171Z", + "updated_at": "2022-08-12T14:45:36.171Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "index": ".asset-criticality.asset-criticality-default", + "id": "5", + "source": { + "id_field": "host.name", + "id_value": "abc", + "criticality_level": "normal", + "@timestamp": "2022-08-12T14:45:36.171Z", + "updated_at": "2022-08-12T14:45:36.171Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "index": ".asset-criticality.asset-criticality-default", + "id": "6", + "source": { + "id_field": "user.name", + "id_value": "abc", + "criticality_level": "not_important", + "@timestamp": "2022-08-12T14:45:36.171Z", + "updated_at": "2022-08-12T14:45:36.171Z" + }, + "type": "_doc" + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/asset_criticality/mappings.json b/x-pack/test/functional/es_archives/asset_criticality/mappings.json new file mode 100644 index 0000000000000..94fe5389706b2 --- /dev/null +++ b/x-pack/test/functional/es_archives/asset_criticality/mappings.json @@ -0,0 +1,32 @@ +{ + "type": "index", + "value": { + "index": ".asset-criticality.asset-criticality-default", + "mappings": { + "properties": { + "id_value": { + "type": "keyword" + }, + "id_field": { + "type": "keyword" + }, + "criticality_level": { + "type": "keyword" + }, + "@timestamp": { + "type": "date" + }, + "updated_at": { + "type": "date" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/security_solution_api_integration/config/ess/config.base.ts b/x-pack/test/security_solution_api_integration/config/ess/config.base.ts index b4fbdba6de4c4..89e6df3c68cd2 100644 --- a/x-pack/test/security_solution_api_integration/config/ess/config.base.ts +++ b/x-pack/test/security_solution_api_integration/config/ess/config.base.ts @@ -81,6 +81,7 @@ export function createTestConfig(options: CreateTestConfigOptions, testFiles?: s 'previewTelemetryUrlEnabled', 'riskScoringPersistence', 'riskScoringRoutesEnabled', + 'entityAnalyticsAssetCriticalityEnabled', ])}`, '--xpack.task_manager.poll_interval=1000', `--xpack.actions.preconfigured=${JSON.stringify({ diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/configs/serverless.config.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/configs/serverless.config.ts index 7bcb663699d68..1f43395efcd90 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/configs/serverless.config.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/configs/serverless.config.ts @@ -16,5 +16,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([ + 'entityAnalyticsAssetCriticalityEnabled', + ])}`, ], }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/execution_logic/eql.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/execution_logic/eql.ts index db5a924b48a05..03af11e239c68 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/execution_logic/eql.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/execution_logic/eql.ts @@ -41,6 +41,13 @@ import { import { FtrProviderContext } from '../../../../../ftr_provider_context'; import { EsArchivePathBuilder } from '../../../../../es_archive_path_builder'; +/** + * Specific AGENT_ID to use for some of the tests. If the archiver changes and you see errors + * here, update this to a new value of a chosen auditbeat record and update the tests values. + */ +const AGENT_ID = 'a1d7b39c-f898-4dbe-a761-efb61939302d'; +const specificQueryForTests = `configuration where agent.id=="${AGENT_ID}"`; + export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); @@ -73,7 +80,7 @@ export default ({ getService }: FtrProviderContext) => { it('generates a correctly formatted alert from EQL non-sequence queries', async () => { const rule: EqlRuleCreateProps = { ...getEqlRuleForAlertTesting(['auditbeat-*']), - query: 'configuration where agent.id=="a1d7b39c-f898-4dbe-a761-efb61939302d"', + query: specificQueryForTests, }; const createdRule = await createRule(supertest, log, rule); const alerts = await getOpenAlerts(supertest, log, es, createdRule); @@ -88,7 +95,7 @@ export default ({ getService }: FtrProviderContext) => { agent: { ephemeral_id: '0010d67a-14f7-41da-be30-489fea735967', hostname: 'suricata-zeek-sensor-toronto', - id: 'a1d7b39c-f898-4dbe-a761-efb61939302d', + id: AGENT_ID, type: 'auditbeat', version: '8.0.0', }, @@ -196,7 +203,7 @@ export default ({ getService }: FtrProviderContext) => { it('uses the provided event_category_override', async () => { const rule: EqlRuleCreateProps = { ...getEqlRuleForAlertTesting(['auditbeat-*']), - query: 'config_change where agent.id=="a1d7b39c-f898-4dbe-a761-efb61939302d"', + query: `config_change where agent.id=="${AGENT_ID}"`, event_category_override: 'auditd.message_type', }; const { previewId } = await previewRule({ supertest, rule }); @@ -542,7 +549,7 @@ export default ({ getService }: FtrProviderContext) => { it('generates alerts when an index name contains special characters to encode', async () => { const rule: EqlRuleCreateProps = { ...getEqlRuleForAlertTesting(['auditbeat-*', '']), - query: 'configuration where agent.id=="a1d7b39c-f898-4dbe-a761-efb61939302d"', + query: specificQueryForTests, }; const { previewId } = await previewRule({ supertest, rule }); const previewAlerts = await getPreviewAlerts({ es, previewId }); @@ -607,7 +614,7 @@ export default ({ getService }: FtrProviderContext) => { it('should be enriched with host risk score', async () => { const rule: EqlRuleCreateProps = { ...getEqlRuleForAlertTesting(['auditbeat-*']), - query: 'configuration where agent.id=="a1d7b39c-f898-4dbe-a761-efb61939302d"', + query: specificQueryForTests, }; const { previewId } = await previewRule({ supertest, rule }); const previewAlerts = await getPreviewAlerts({ es, previewId }); @@ -620,5 +627,27 @@ export default ({ getService }: FtrProviderContext) => { expect(fullAlert?.host?.risk?.calculated_score_norm).to.eql(96); }); }); + + describe('with asset criticality', async () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/asset_criticality'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/asset_criticality'); + }); + + it('should be enriched alert with criticality_level', async () => { + const rule: EqlRuleCreateProps = { + ...getEqlRuleForAlertTesting(['auditbeat-*']), + query: specificQueryForTests, + }; + + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + const fullAlert = previewAlerts[0]._source; + expect(fullAlert?.['kibana.alert.host.criticality_level']).to.eql('important'); + }); + }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/execution_logic/esql.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/execution_logic/esql.ts index cb0f31ad25460..caf649896abf3 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/execution_logic/esql.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/execution_logic/esql.ts @@ -859,6 +859,45 @@ export default ({ getService }: FtrProviderContext) => { }); }); + describe('with asset criticality', async () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/asset_criticality'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/asset_criticality'); + }); + + it('should be enriched alert with criticality_level', async () => { + const id = uuidv4(); + const interval: [string, string] = ['2020-10-28T06:00:00.000Z', '2020-10-28T06:10:00.000Z']; + const doc1 = { host: { name: 'host-0' } }; + + await indexEnhancedDocuments({ documents: [doc1], interval, id }); + + const rule: EsqlRuleCreateProps = { + ...getCreateEsqlRulesSchemaMock('rule-1', true), + query: `from ecs_compliant ${internalIdPipe(id)} | where host.name=="host-0"`, + from: 'now-1h', + interval: '1h', + }; + + const { previewId } = await previewRule({ + supertest, + rule, + timeframeEnd: new Date('2020-10-28T06:30:00.000Z'), + }); + + const previewAlerts = await getPreviewAlerts({ es, previewId }); + + expect(previewAlerts.length).toBe(1); + + expect(previewAlerts[0]?._source?.['kibana.alert.host.criticality_level']).toBe( + 'very_important' + ); + }); + }); + describe('ECS fields validation', () => { it('creates alert if ECS field has multifields', async () => { const id = uuidv4(); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/execution_logic/machine_learning.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/execution_logic/machine_learning.ts index d58227377f116..8787a51871125 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/execution_logic/machine_learning.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/execution_logic/machine_learning.ts @@ -273,5 +273,25 @@ export default ({ getService }: FtrProviderContext) => { expect(fullAlert?.host?.risk?.calculated_score_norm).toBe(1); }); }); + + describe('with asset criticality', async () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/asset_criticality'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/asset_criticality'); + }); + + it('should be enriched alert with criticality_level', async () => { + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).toBe(1); + const fullAlert = previewAlerts[0]._source; + + expect(fullAlert?.['kibana.alert.host.criticality_level']).toBe('normal'); + expect(fullAlert?.['kibana.alert.user.criticality_level']).toBe('very_important'); + }); + }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/execution_logic/new_terms.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/execution_logic/new_terms.ts index 8a47aeaa89bdc..9aea83afb95d0 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/execution_logic/new_terms.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/execution_logic/new_terms.ts @@ -1040,5 +1040,31 @@ export default ({ getService }: FtrProviderContext) => { expect(previewAlerts[0]?._source?.host?.risk?.calculated_score_norm).to.eql(23); }); }); + + describe('with asset criticality', async () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/asset_criticality'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/asset_criticality'); + }); + + it('should be enriched alert with criticality_level', async () => { + const rule: NewTermsRuleCreateProps = { + ...getCreateNewTermsRulesSchemaMock('rule-1', true), + new_terms_fields: ['host.name'], + from: '2019-02-19T20:42:00.000Z', + history_window_start: '2019-01-19T20:42:00.000Z', + }; + + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + const fullAlert = previewAlerts[0]._source; + + expect(fullAlert?.['kibana.alert.host.criticality_level']).to.eql('normal'); + expect(fullAlert?.['kibana.alert.user.criticality_level']).to.eql('very_important'); + }); + }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/execution_logic/query.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/execution_logic/query.ts index 19c02fe389fe4..38930bafa564e 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/execution_logic/query.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/execution_logic/query.ts @@ -280,6 +280,31 @@ export default ({ getService }: FtrProviderContext) => { }); }); + describe('with asset criticality', async () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/asset_criticality'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/asset_criticality'); + }); + + it('should be enriched alert with criticality_level', async () => { + const rule: QueryRuleCreateProps = { + ...getRuleForAlertTesting(['auditbeat-*']), + query: `_id:${ID}`, + }; + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts[0]?._source?.['kibana.alert.host.criticality_level']).to.eql( + 'important' + ); + expect(previewAlerts[0]?._source?.['kibana.alert.user.criticality_level']).to.eql( + 'very_important' + ); + }); + }); + /** * Here we test the functionality of Severity and Risk Score overrides (also called "mappings" * in the code). If the rule specifies a mapping, then the final Severity or Risk Score diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/execution_logic/threat_match.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/execution_logic/threat_match.ts index 9b6c525b5e351..734583d009ca3 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/execution_logic/threat_match.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/execution_logic/threat_match.ts @@ -1623,5 +1623,49 @@ export default ({ getService }: FtrProviderContext) => { expect(fullAlert?.host?.risk?.calculated_score_norm).to.eql(70); }); }); + + describe('with asset criticality', async () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/asset_criticality'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/asset_criticality'); + }); + + it('should be enriched alert with criticality_level', async () => { + const rule: ThreatMatchRuleCreateProps = createThreatMatchRule({ + query: '*:*', + threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip + threat_mapping: [ + // We match host.name against host.name + { + entries: [ + { + field: 'host.name', + value: 'host.name', + type: 'mapping', + }, + ], + }, + ], + }); + + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId, size: 100 }); + expect(previewAlerts.length).equal(88); + const fullSource = previewAlerts.find( + (alert) => + (alert._source?.[ALERT_ANCESTORS] as Ancestor[])[0].id === '7yJ-B2kBR346wHgnhlMn' + ); + const fullAlert = fullSource?._source; + if (!fullAlert) { + return expect(fullAlert).to.be.ok(); + } + + expect(fullAlert?.['kibana.alert.host.criticality_level']).to.eql('low'); + expect(fullAlert?.['kibana.alert.user.criticality_level']).to.eql('very_important'); + }); + }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/execution_logic/threshold.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/execution_logic/threshold.ts index 5edee29c02dc6..dce4886bc1ba5 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/execution_logic/threshold.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/execution_logic/threshold.ts @@ -430,5 +430,30 @@ export default ({ getService }: FtrProviderContext) => { expect(previewAlerts[1]?._source?.host?.risk?.calculated_score_norm).toEqual(96); }); }); + + describe('with asset criticality', async () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/asset_criticality'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/asset_criticality'); + }); + + it('should be enriched alert with criticality_level', async () => { + const rule: ThresholdRuleCreateProps = { + ...getThresholdRuleForAlertTesting(['auditbeat-*']), + threshold: { + field: 'host.name', + value: 100, + }, + }; + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId, sort: ['host.name'] }); + const fullAlert = previewAlerts[0]?._source; + + expect(fullAlert?.['kibana.alert.host.criticality_level']).toEqual('important'); + }); + }); }); };