diff --git a/x-pack/platform/plugins/private/telemetry_collection_xpack/schema/xpack_security.json b/x-pack/platform/plugins/private/telemetry_collection_xpack/schema/xpack_security.json index 7625478a73c44..0cea94961fc0d 100644 --- a/x-pack/platform/plugins/private/telemetry_collection_xpack/schema/xpack_security.json +++ b/x-pack/platform/plugins/private/telemetry_collection_xpack/schema/xpack_security.json @@ -7242,6 +7242,130 @@ } } } + }, + "elastic_detection_rule_customization_status": { + "properties": { + "alert_suppression": { + "type": "long", + "_meta": { + "description": "The number of prebuilt rules with customized alert_suppression field" + } + }, + "anomaly_threshold": { + "type": "long", + "_meta": { + "description": "The number of prebuilt rules with customized anomaly_threshold field" + } + }, + "data_view_id": { + "type": "long", + "_meta": { + "description": "The number of prebuilt rules with customized data_view_id field" + } + }, + "description": { + "type": "long", + "_meta": { + "description": "The number of prebuilt rules with customized description field" + } + }, + "filters": { + "type": "long", + "_meta": { + "description": "The number of prebuilt rules with customized filters field" + } + }, + "from": { + "type": "long", + "_meta": { + "description": "The number of prebuilt rules with customized from field" + } + }, + "index": { + "type": "long", + "_meta": { + "description": "The number of prebuilt rules with customized index field" + } + }, + "interval": { + "type": "long", + "_meta": { + "description": "The number of prebuilt rules with customized interval field" + } + }, + "investigation_fields": { + "type": "long", + "_meta": { + "description": "The number of prebuilt rules with customized investigation_fields field" + } + }, + "name": { + "type": "long", + "_meta": { + "description": "The number of prebuilt rules with customized name field" + } + }, + "new_terms_fields": { + "type": "long", + "_meta": { + "description": "The number of prebuilt rules with customized new_terms_fields field" + } + }, + "note": { + "type": "long", + "_meta": { + "description": "The number of prebuilt rules with customized note field" + } + }, + "query": { + "type": "long", + "_meta": { + "description": "The number of prebuilt rules with customized query field" + } + }, + "risk_score": { + "type": "long", + "_meta": { + "description": "The number of prebuilt rules with customized risk_score field" + } + }, + "severity": { + "type": "long", + "_meta": { + "description": "The number of prebuilt rules with customized severity field" + } + }, + "setup": { + "type": "long", + "_meta": { + "description": "The number of prebuilt rules with customized setup field" + } + }, + "tags": { + "type": "long", + "_meta": { + "description": "The number of prebuilt rules with customized tags field" + } + }, + "threat_query": { + "type": "long", + "_meta": { + "description": "The number of prebuilt rules with customized threat_query field" + } + }, + "threshold": { + "type": "long", + "_meta": { + "description": "The number of prebuilt rules with customized threshold field" + } + }, + "timeline_id": { + "type": "long", + "_meta": { + "description": "The number of prebuilt rules with customized timeline_id field" + } + } + } } } }, diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/get_initial_usage.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/get_initial_usage.ts index 63d91553839a2..65ae9f83c5933 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/get_initial_usage.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/get_initial_usage.ts @@ -10,6 +10,7 @@ import type { DetectionMetrics } from './types'; import { getInitialMlJobUsage } from './ml_jobs/get_initial_usage'; import { getInitialEventLogUsage, + getInitialRuleCustomizationStatus, getInitialRuleUpgradeStatus, getInitialRulesUsage, getInitialSpacesUsage, @@ -30,6 +31,7 @@ export const getInitialDetectionMetrics = (): DetectionMetrics => ({ detection_rule_usage: getInitialRulesUsage(), detection_rule_status: getInitialEventLogUsage(), elastic_detection_rule_upgrade_status: getInitialRuleUpgradeStatus(), + elastic_detection_rule_customization_status: getInitialRuleCustomizationStatus(), spaces_usage: getInitialSpacesUsage(), }, legacy_siem_signals: getInitialLegacySiemSignalsUsage(), diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/get_metrics.test.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/get_metrics.test.ts index 94a23707b661f..97d5dc94172e3 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/get_metrics.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/get_metrics.test.ts @@ -190,6 +190,28 @@ describe('Detections Usage and Metrics', () => { enabled: 0, disabled: 1, }, + elastic_detection_rule_customization_status: { + alert_suppression: 0, + anomaly_threshold: 0, + data_view_id: 0, + description: 0, + filters: 0, + from: 0, + index: 0, + interval: 0, + investigation_fields: 0, + name: 0, + new_terms_fields: 0, + note: 0, + query: 0, + risk_score: 0, + severity: 0, + setup: 0, + tags: 0, + threat_query: 0, + threshold: 0, + timeline_id: 0, + }, }, }); }); @@ -230,6 +252,7 @@ describe('Detections Usage and Metrics', () => { expect(result).toEqual({ ...getInitialDetectionMetrics(), detection_rules: { + ...getInitialDetectionMetrics().detection_rules, spaces_usage: { rules_in_spaces: [1], total: 1, @@ -312,6 +335,28 @@ describe('Detections Usage and Metrics', () => { enabled: 1, disabled: 0, }, + elastic_detection_rule_customization_status: { + alert_suppression: 0, + anomaly_threshold: 0, + data_view_id: 0, + description: 0, + filters: 0, + from: 0, + index: 0, + interval: 0, + investigation_fields: 0, + name: 0, + new_terms_fields: 0, + note: 0, + query: 0, + risk_score: 0, + severity: 0, + setup: 0, + tags: 0, + threat_query: 0, + threshold: 0, + timeline_id: 0, + }, }, }); }); @@ -352,6 +397,7 @@ describe('Detections Usage and Metrics', () => { expect(result).toEqual({ ...getInitialDetectionMetrics(), detection_rules: { + ...getInitialDetectionMetrics().detection_rules, spaces_usage: { rules_in_spaces: [1], total: 1, @@ -434,6 +480,28 @@ describe('Detections Usage and Metrics', () => { enabled: 0, disabled: 1, }, + elastic_detection_rule_customization_status: { + alert_suppression: 0, + anomaly_threshold: 0, + data_view_id: 0, + description: 1, + filters: 0, + from: 0, + index: 0, + interval: 0, + investigation_fields: 0, + name: 1, + new_terms_fields: 0, + note: 0, + query: 0, + risk_score: 0, + severity: 0, + setup: 0, + tags: 1, + threat_query: 0, + threshold: 0, + timeline_id: 0, + }, }, }); }); @@ -474,6 +542,7 @@ describe('Detections Usage and Metrics', () => { expect(result).toEqual({ ...getInitialDetectionMetrics(), detection_rules: { + ...getInitialDetectionMetrics().detection_rules, spaces_usage: { rules_in_spaces: [1], total: 1, @@ -556,6 +625,28 @@ describe('Detections Usage and Metrics', () => { enabled: 1, disabled: 0, }, + elastic_detection_rule_customization_status: { + alert_suppression: 0, + anomaly_threshold: 0, + data_view_id: 0, + description: 1, + filters: 0, + from: 0, + index: 0, + interval: 0, + investigation_fields: 0, + name: 1, + new_terms_fields: 0, + note: 0, + query: 0, + risk_score: 0, + severity: 0, + setup: 0, + tags: 1, + threat_query: 0, + threshold: 0, + timeline_id: 0, + }, }, }); }); @@ -568,7 +659,7 @@ describe('Detections Usage and Metrics', () => { savedObjectsClient.find.mockResolvedValueOnce( getMockRuleSearchResponse( true /* immutable (elastic) */, - true /* customized */, + false /* customized */, false /* enabled */ ) ); @@ -596,6 +687,7 @@ describe('Detections Usage and Metrics', () => { expect(result).toEqual({ ...getInitialDetectionMetrics(), detection_rules: { + ...getInitialDetectionMetrics().detection_rules, spaces_usage: { rules_in_spaces: [1], total: 1, @@ -607,7 +699,7 @@ describe('Detections Usage and Metrics', () => { cases_count_total: 1, created_on: '2021-03-23T17:15:59.634Z', elastic_rule: true, - is_customized: true, + is_customized: false, enabled: false, rule_id: '5370d4cd-2bb3-4d71-abf5-1e1d0ff5a2de', rule_name: 'Azure Diagnostic Settings Deletion', @@ -658,6 +750,20 @@ describe('Detections Usage and Metrics', () => { response_actions: initialResponseActionsUsage, }, elastic_customized_total: { + alerts: 0, + cases: 0, + disabled: 0, + enabled: 0, + legacy_notifications_enabled: 0, + legacy_notifications_disabled: 0, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_investigation_fields: 0, + alert_suppression: initialAlertSuppression, + has_exceptions: 0, + response_actions: initialResponseActionsUsage, + }, + elastic_noncustomized_total: { alerts: 3400, cases: 1, disabled: 1, @@ -678,6 +784,28 @@ describe('Detections Usage and Metrics', () => { enabled: 0, disabled: 0, }, + elastic_detection_rule_customization_status: { + alert_suppression: 0, + anomaly_threshold: 0, + data_view_id: 0, + description: 0, + filters: 0, + from: 0, + index: 0, + interval: 0, + investigation_fields: 0, + name: 0, + new_terms_fields: 0, + note: 0, + query: 0, + risk_score: 0, + severity: 0, + setup: 0, + tags: 0, + threat_query: 0, + threshold: 0, + timeline_id: 0, + }, }, }); }); @@ -718,6 +846,7 @@ describe('Detections Usage and Metrics', () => { expect(result).toEqual({ ...getInitialDetectionMetrics(), detection_rules: { + ...getInitialDetectionMetrics().detection_rules, spaces_usage: { rules_in_spaces: [1], total: 1, @@ -800,6 +929,28 @@ describe('Detections Usage and Metrics', () => { enabled: 0, disabled: 0, }, + elastic_detection_rule_customization_status: { + alert_suppression: 0, + anomaly_threshold: 0, + data_view_id: 0, + description: 0, + filters: 0, + from: 0, + index: 0, + interval: 0, + investigation_fields: 0, + name: 0, + new_terms_fields: 0, + note: 0, + query: 0, + risk_score: 0, + severity: 0, + setup: 0, + tags: 0, + threat_query: 0, + threshold: 0, + timeline_id: 0, + }, }, }); }); @@ -840,6 +991,7 @@ describe('Detections Usage and Metrics', () => { expect(result).toEqual({ ...getInitialDetectionMetrics(), detection_rules: { + ...getInitialDetectionMetrics().detection_rules, spaces_usage: { rules_in_spaces: [1], total: 1, @@ -922,6 +1074,28 @@ describe('Detections Usage and Metrics', () => { enabled: 0, disabled: 0, }, + elastic_detection_rule_customization_status: { + alert_suppression: 0, + anomaly_threshold: 0, + data_view_id: 0, + description: 1, + filters: 0, + from: 0, + index: 0, + interval: 0, + investigation_fields: 0, + name: 1, + new_terms_fields: 0, + note: 0, + query: 0, + risk_score: 0, + severity: 0, + setup: 0, + tags: 1, + threat_query: 0, + threshold: 0, + timeline_id: 0, + }, }, }); }); @@ -962,6 +1136,7 @@ describe('Detections Usage and Metrics', () => { expect(result).toEqual({ ...getInitialDetectionMetrics(), detection_rules: { + ...getInitialDetectionMetrics().detection_rules, spaces_usage: { rules_in_spaces: [1], total: 1, @@ -1044,6 +1219,28 @@ describe('Detections Usage and Metrics', () => { enabled: 0, disabled: 0, }, + elastic_detection_rule_customization_status: { + alert_suppression: 0, + anomaly_threshold: 0, + data_view_id: 0, + description: 1, + filters: 0, + from: 0, + index: 0, + interval: 0, + investigation_fields: 0, + name: 1, + new_terms_fields: 0, + note: 0, + query: 0, + risk_score: 0, + severity: 0, + setup: 0, + tags: 1, + threat_query: 0, + threshold: 0, + timeline_id: 0, + }, }, }); }); @@ -1125,6 +1322,28 @@ describe('Detections Usage and Metrics', () => { }, }, elastic_detection_rule_upgrade_status: getInitialRuleUpgradeStatus(), + elastic_detection_rule_customization_status: { + name: 0, + description: 0, + risk_score: 0, + severity: 0, + timeline_id: 0, + note: 0, + investigation_fields: 0, + tags: 0, + interval: 0, + from: 0, + setup: 0, + query: 0, + index: 0, + data_view_id: 0, + filters: 0, + alert_suppression: 0, + threshold: 0, + threat_query: 0, + anomaly_threshold: 0, + new_terms_fields: 0, + }, }, }); }); @@ -1245,6 +1464,28 @@ describe('Detections Usage and Metrics', () => { }, }, elastic_detection_rule_upgrade_status: getInitialRuleUpgradeStatus(), + elastic_detection_rule_customization_status: { + name: 0, + description: 0, + risk_score: 0, + severity: 0, + timeline_id: 0, + note: 0, + investigation_fields: 0, + tags: 0, + interval: 0, + from: 0, + setup: 0, + query: 0, + index: 0, + data_view_id: 0, + filters: 0, + alert_suppression: 0, + threshold: 0, + threat_query: 0, + anomaly_threshold: 0, + new_terms_fields: 0, + }, }, }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/get_metrics.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/get_metrics.ts index e64c5acde73d9..7e9e3a57a948b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/get_metrics.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/get_metrics.ts @@ -13,6 +13,7 @@ import { getMlJobMetrics } from './ml_jobs/get_metrics'; import { getRuleMetrics } from './rules/get_metrics'; import { getInitialEventLogUsage, + getInitialRuleCustomizationStatus, getInitialRuleUpgradeStatus, getInitialRulesUsage, getInitialSpacesUsage, @@ -61,6 +62,7 @@ export const getDetectionsMetrics = async ({ detection_rule_usage: getInitialRulesUsage(), detection_rule_status: getInitialEventLogUsage(), elastic_detection_rule_upgrade_status: getInitialRuleUpgradeStatus(), + elastic_detection_rule_customization_status: getInitialRuleCustomizationStatus(), spaces_usage: getInitialSpacesUsage(), }, legacy_siem_signals: diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/ml_jobs/get_metrics.mocks.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/ml_jobs/get_metrics.mocks.ts index cbd7646447b83..81114e33c2ce4 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/ml_jobs/get_metrics.mocks.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/ml_jobs/get_metrics.mocks.ts @@ -348,6 +348,9 @@ export const getMockRuleSearchResponse = ( ? { type: 'external', isCustomized, + customizedFields: isCustomized + ? [{ fieldName: 'tags' }, { fieldName: 'name' }, { fieldName: 'description' }] + : [], } : { type: 'internal' }, }, diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/get_initial_usage.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/get_initial_usage.ts index 38e7e17f6f388..12250fd8588c0 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/get_initial_usage.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/get_initial_usage.ts @@ -16,6 +16,7 @@ import type { FeatureTypeUsage, ResponseActionsUsage, UpgradeableRulesSummary, + RuleCustomizationCounts, } from './types'; export const initialAlertSuppression: AlertSuppressionUsage = { @@ -167,3 +168,26 @@ export const getInitialRuleUpgradeStatus = (): UpgradeableRulesSummary => ({ enabled: 0, disabled: 0, }); + +export const getInitialRuleCustomizationStatus = (): RuleCustomizationCounts => ({ + alert_suppression: 0, + anomaly_threshold: 0, + data_view_id: 0, + description: 0, + filters: 0, + from: 0, + index: 0, + interval: 0, + investigation_fields: 0, + name: 0, + new_terms_fields: 0, + note: 0, + query: 0, + risk_score: 0, + severity: 0, + setup: 0, + tags: 0, + threat_query: 0, + threshold: 0, + timeline_id: 0, +}); diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/get_metrics.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/get_metrics.ts index 0e5df98af7176..c8a42474b71ba 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/get_metrics.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/get_metrics.ts @@ -15,6 +15,7 @@ import { getAlerts } from '../../queries/get_alerts'; import { MAX_PER_PAGE, MAX_RESULTS_WINDOW } from '../../constants'; import { getInitialEventLogUsage, + getInitialRuleCustomizationStatus, getInitialRuleUpgradeStatus, getInitialRulesUsage, getInitialSpacesUsage, @@ -30,6 +31,8 @@ import { getEventLogByTypeAndStatus } from '../../queries/get_event_log_by_type_ // eslint-disable-next-line no-restricted-imports import { legacyGetRuleActions } from '../../queries/legacy_get_rule_actions'; import { calculateRuleUpgradeStatus } from './calculate_rules_upgrade_status'; +import type { ExternalRuleSourceInfo } from './get_rule_customization_status'; +import { getRuleCustomizationStatus } from './get_rule_customization_status'; export interface GetRuleMetricsOptions { signalsIndex: string; @@ -62,6 +65,7 @@ export const getRuleMetrics = async ({ detection_rule_usage: getInitialRulesUsage(), detection_rule_status: getInitialEventLogUsage(), elastic_detection_rule_upgrade_status: getInitialRuleUpgradeStatus(), + elastic_detection_rule_customization_status: getInitialRuleCustomizationStatus(), spaces_usage: getInitialSpacesUsage(), }; } @@ -145,6 +149,7 @@ export const getRuleMetrics = async ({ detection_rule_usage: rulesUsage, detection_rule_status: eventLogMetricsTypeStatus, elastic_detection_rule_upgrade_status: calculateRuleUpgradeStatus(upgradeableRules), + elastic_detection_rule_customization_status: prepareRuleCustomizationStatus(ruleResults), spaces_usage: getSpacesUsage(ruleResults), }; } catch (e) { @@ -157,7 +162,34 @@ export const getRuleMetrics = async ({ detection_rule_usage: getInitialRulesUsage(), detection_rule_status: getInitialEventLogUsage(), elastic_detection_rule_upgrade_status: getInitialRuleUpgradeStatus(), + elastic_detection_rule_customization_status: getInitialRuleCustomizationStatus(), spaces_usage: getInitialSpacesUsage(), }; } }; + +function prepareRuleCustomizationStatus( + ruleResults: Awaited> +) { + const ruleSources = ruleResults.flatMap((ruleResult): ExternalRuleSourceInfo[] => { + const ruleSource = ruleResult.attributes?.params?.ruleSource; + if ( + !ruleSource || + ruleSource?.type !== 'external' || + typeof ruleSource.isCustomized !== 'boolean' + ) { + return []; + } + + return [ + { + is_customized: ruleSource.isCustomized, + customized_fields: ruleSource.customizedFields ?? [], + }, + ]; + }); + + return ruleSources.length === 0 + ? getInitialRuleCustomizationStatus() + : getRuleCustomizationStatus(ruleSources); +} diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/get_rule_customization_status.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/get_rule_customization_status.ts new file mode 100644 index 0000000000000..6f4592878f185 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/get_rule_customization_status.ts @@ -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 { getInitialRuleCustomizationStatus } from './get_initial_usage'; +import type { RuleCustomizationCounts } from './types'; + +export interface ExternalRuleSourceInfo { + is_customized: boolean; + customized_fields: Array<{ fieldName: string }>; +} + +// we only publish a subset of most important fields that we know can be customized +// this is to avoid telemetry issues if we add new fields to the rule schema +// and to avoid counting fields of lesser importance +// see https://github.com/elastic/kibana/issues/140369 for more information +const ALLOWED_FIELDS: Set = new Set([ + 'alert_suppression', + 'anomaly_threshold', + 'data_view_id', + 'description', + 'filters', + 'from', + 'index', + 'interval', + 'investigation_fields', + 'name', + 'new_terms_fields', + 'note', + 'query', + 'risk_score', + 'severity', + 'setup', + 'tags', + 'threat_query', + 'threshold', + 'timeline_id', +]); + +export const getRuleCustomizationStatus = ( + ruleSources: ReadonlyArray +): RuleCustomizationCounts => { + const counts = getInitialRuleCustomizationStatus(); + + ruleSources.forEach((ruleSource) => { + if (!ruleSource.is_customized) { + return; + } + + ruleSource.customized_fields.forEach((field) => { + const fieldName = field.fieldName as keyof RuleCustomizationCounts; + if (ALLOWED_FIELDS.has(fieldName)) { + counts[fieldName] = (counts[fieldName] ?? 0) + 1; + } + }); + }); + + return counts; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/schema.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/schema.ts index 22d6a21e0e80a..3e7430bb808e2 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/schema.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/schema.ts @@ -11,6 +11,7 @@ import { ruleMetricsSchema } from './schemas/prebuilt_rule_detail'; import { ruleStatusMetricsSchema } from './schemas/detection_rule_status'; import { ruleUpgradeStatusSchema } from './schemas/detection_rule_upgrade_status'; import type { RuleAdoption } from './types'; +import { ruleCustomizedFieldsCounts } from './schemas/detection_rule_customization_status'; export const rulesMetricsSchema: MakeSchemaFrom = { spaces_usage: { @@ -33,4 +34,5 @@ export const rulesMetricsSchema: MakeSchemaFrom = { }, detection_rule_status: ruleStatusMetricsSchema, elastic_detection_rule_upgrade_status: ruleUpgradeStatusSchema, + elastic_detection_rule_customization_status: ruleCustomizedFieldsCounts, }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/schemas/detection_rule_customization_status.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/schemas/detection_rule_customization_status.ts new file mode 100644 index 0000000000000..3fdeb87714f6c --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/schemas/detection_rule_customization_status.ts @@ -0,0 +1,94 @@ +/* + * 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 { MakeSchemaFrom } from '@kbn/usage-collection-plugin/server'; +import type { RuleCustomizationCounts } from '../types'; + +export const ruleCustomizedFieldsCounts: MakeSchemaFrom = { + alert_suppression: { + type: 'long', + _meta: { description: 'The number of prebuilt rules with customized alert_suppression field' }, + }, + anomaly_threshold: { + type: 'long', + _meta: { description: 'The number of prebuilt rules with customized anomaly_threshold field' }, + }, + data_view_id: { + type: 'long', + _meta: { description: 'The number of prebuilt rules with customized data_view_id field' }, + }, + description: { + type: 'long', + _meta: { description: 'The number of prebuilt rules with customized description field' }, + }, + filters: { + type: 'long', + _meta: { description: 'The number of prebuilt rules with customized filters field' }, + }, + from: { + type: 'long', + _meta: { description: 'The number of prebuilt rules with customized from field' }, + }, + index: { + type: 'long', + _meta: { description: 'The number of prebuilt rules with customized index field' }, + }, + interval: { + type: 'long', + _meta: { description: 'The number of prebuilt rules with customized interval field' }, + }, + investigation_fields: { + type: 'long', + _meta: { + description: 'The number of prebuilt rules with customized investigation_fields field', + }, + }, + name: { + type: 'long', + _meta: { description: 'The number of prebuilt rules with customized name field' }, + }, + new_terms_fields: { + type: 'long', + _meta: { description: 'The number of prebuilt rules with customized new_terms_fields field' }, + }, + note: { + type: 'long', + _meta: { description: 'The number of prebuilt rules with customized note field' }, + }, + query: { + type: 'long', + _meta: { description: 'The number of prebuilt rules with customized query field' }, + }, + risk_score: { + type: 'long', + _meta: { description: 'The number of prebuilt rules with customized risk_score field' }, + }, + severity: { + type: 'long', + _meta: { description: 'The number of prebuilt rules with customized severity field' }, + }, + setup: { + type: 'long', + _meta: { description: 'The number of prebuilt rules with customized setup field' }, + }, + tags: { + type: 'long', + _meta: { description: 'The number of prebuilt rules with customized tags field' }, + }, + threat_query: { + type: 'long', + _meta: { description: 'The number of prebuilt rules with customized threat_query field' }, + }, + threshold: { + type: 'long', + _meta: { description: 'The number of prebuilt rules with customized threshold field' }, + }, + timeline_id: { + type: 'long', + _meta: { description: 'The number of prebuilt rules with customized timeline_id field' }, + }, +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/types.ts b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/types.ts index 0894d02fb17fb..b1f7c8556e876 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/usage/detections/rules/types.ts @@ -50,6 +50,29 @@ export interface UpgradeableRulesSummary { disabled: number; } +export interface RuleCustomizationCounts { + alert_suppression: number; + anomaly_threshold: number; + data_view_id: number; + description: number; + filters: number; + from: number; + index: number; + interval: number; + investigation_fields: number; + name: number; + new_terms_fields: number; + note: number; + query: number; + risk_score: number; + severity: number; + setup: number; + tags: number; + threat_query: number; + threshold: number; + timeline_id: number; +} + export interface RulesTypeUsage { query: FeatureTypeUsage; query_custom: FeatureTypeUsage; @@ -81,6 +104,7 @@ export interface RuleAdoption { detection_rule_usage: RulesTypeUsage; detection_rule_status: EventLogStatusMetric; elastic_detection_rule_upgrade_status: UpgradeableRulesSummary; + elastic_detection_rule_customization_status: RuleCustomizationCounts; spaces_usage: SpacesUsage; } diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/index.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/index.ts index 2a87e52d3389e..14ad75d44cf7f 100644 --- a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/index.ts +++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/index.ts @@ -14,6 +14,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./usage_collector/value_list_metrics')); loadTestFile(require.resolve('./usage_collector/detection_rule_status')); loadTestFile(require.resolve('./usage_collector/detection_rule_upgrade_status')); + loadTestFile(require.resolve('./usage_collector/detection_rule_customization_status')); loadTestFile(require.resolve('./usage_collector/detection_rules_legacy_action')); loadTestFile(require.resolve('./task_based/all_types')); diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rule_customization_status.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rule_customization_status.ts new file mode 100644 index 0000000000000..4041120c4bd17 --- /dev/null +++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rule_customization_status.ts @@ -0,0 +1,178 @@ +/* + * 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 expect from 'expect'; +import { PrebuiltRuleAsset } from '@kbn/security-solution-plugin/server/lib/detection_engine/prebuilt_rules'; +import { customizeRule, getStats } from '../../../utils'; +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { + deleteAllPrebuiltRuleAssets, + createRuleAssetSavedObject, + createPrebuiltRuleAssetSavedObjects, + installPrebuiltRules, +} from '../../../utils'; +import { deleteAllRules } from '../../../../../config/services/detections_response'; + +/** + * Test suite for detection rule customization status telemetry. + * + * This suite tests the telemetry metrics for prebuilt rules, + * verifying that the system correctly tracks telemetry for rule customizations. + */ +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const detectionsApi = getService('detectionsApi'); + const log = getService('log'); + describe('@ess @serverless @skipInServerlessMKI Snapshot telemetry for customization status', () => { + const ZERO_COUNTS = { + alert_suppression: 0, + anomaly_threshold: 0, + data_view_id: 0, + description: 0, + filters: 0, + from: 0, + index: 0, + interval: 0, + investigation_fields: 0, + name: 0, + new_terms_fields: 0, + note: 0, + query: 0, + risk_score: 0, + severity: 0, + setup: 0, + tags: 0, + threat_query: 0, + threshold: 0, + timeline_id: 0, + } as const; + + beforeEach(async () => { + await deleteAllPrebuiltRuleAssets(es, log); + await deleteAllRules(supertest, log); + }); + + const defaultRuleParams = { + description: 'some description', + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + severity: 'high', + type: 'query', + risk_score: 42, + language: 'kuery', + rule_id: 'some-random-id', + version: 1, + author: [], + license: 'Elastic License v2', + index: ['index-1', 'index-2'], + interval: '100m', + }; + + const getRuleAssetSavedObjects = () => [ + createRuleAssetSavedObject({ + ...PrebuiltRuleAsset.parse(defaultRuleParams), + rule_id: 'rule-1', + }), + createRuleAssetSavedObject({ + ...PrebuiltRuleAsset.parse(defaultRuleParams), + rule_id: 'rule-2', + }), + createRuleAssetSavedObject({ + ...PrebuiltRuleAsset.parse(defaultRuleParams), + rule_id: 'rule-3', + }), + ]; + + const setupInitialRules = async () => { + const ruleAssetSavedObjects = getRuleAssetSavedObjects(); + await createPrebuiltRuleAssetSavedObjects(es, ruleAssetSavedObjects); + await installPrebuiltRules(es, supertest); + return ruleAssetSavedObjects; + }; + + const getCustomizationStatus = async () => { + const stats = await getStats(supertest, log); + return stats?.detection_rules?.elastic_detection_rule_customization_status ?? ZERO_COUNTS; + }; + + it('returns zeroed customization status when there are no customizations', async () => { + await setupInitialRules(); + + const status = await getCustomizationStatus(); + expect(status).toEqual(ZERO_COUNTS); + }); + + it('aggregates per-field customization counts across multiple rules', async () => { + await setupInitialRules(); + + await customizeRule(detectionsApi, 'rule-1', { + ...defaultRuleParams, + rule_id: 'rule-1', + tags: ['a', 'b'], + }); + + await customizeRule(detectionsApi, 'rule-2', { + ...defaultRuleParams, + rule_id: 'rule-2', + severity: 'low', + }); + + // rule-3: no customization (control) + + const status = await getCustomizationStatus(); + + const expected = { + ...ZERO_COUNTS, + tags: 1, + severity: 1, + }; + + expect(status).toEqual(expected); + }); + + it('counts multiple customizations of the same field on the same rule as a single customized field', async () => { + await setupInitialRules(); + + await customizeRule(detectionsApi, 'rule-1', { + ...defaultRuleParams, + rule_id: 'rule-1', + tags: ['a', 'b'], + }); + await customizeRule(detectionsApi, 'rule-1', { + ...defaultRuleParams, + rule_id: 'rule-1', + tags: ['a', 'b', 'c'], + }); + await customizeRule(detectionsApi, 'rule-1', { + ...defaultRuleParams, + rule_id: 'rule-1', + tags: ['a', 'b', 'c', 'd'], + }); + + await customizeRule(detectionsApi, 'rule-2', { + ...defaultRuleParams, + rule_id: 'rule-2', + tags: ['x', 'y'], + }); + await customizeRule(detectionsApi, 'rule-2', { + ...defaultRuleParams, + rule_id: 'rule-2', + tags: ['x', 'y', 'z'], + }); + + const status = await getCustomizationStatus(); + + const expected = { + ...ZERO_COUNTS, + tags: 2, + }; + + expect(status).toEqual(expected); + }); + }); +}; diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rule_upgrade_status.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rule_upgrade_status.ts index cf5f20e8c86d9..16ce6af5dc849 100644 --- a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rule_upgrade_status.ts +++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rule_upgrade_status.ts @@ -7,7 +7,7 @@ import expect from 'expect'; import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; -import { getCustomQueryRuleParams, getStats } from '../../../utils'; +import { customizeRule, getStats } from '../../../utils'; import { FtrProviderContext } from '../../../../../ftr_provider_context'; import { deleteAllPrebuiltRuleAssets, @@ -30,6 +30,7 @@ import { deleteAllRules } from '../../../../../config/services/detections_respon export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); + const detectionsApi = getService('detectionsApi'); const log = getService('log'); describe('@ess @serverless @skipInServerlessMKI Prebuilt Rules status', () => { @@ -70,24 +71,6 @@ export default ({ getService }: FtrProviderContext): void => { return ruleAssetSavedObjects; }; - /** - * Customizes a rule with the given parameters. - * @param ruleId - The ID of the rule to customize - * @param customizations - Custom parameters for the rule - */ - const customizeRule = async (ruleId: string, customizations: Record) => { - const customRuleParams = getCustomQueryRuleParams({ - rule_id: ruleId, - ...customizations, - }); - - await supertest - .put(`${DETECTION_ENGINE_RULES_URL}?rule_id=${ruleId}`) - .set('kbn-xsrf', 'true') - .send(customRuleParams) - .expect(200); - }; - /** * Creates upgradeable rules by incrementing versions and verifies the setup. * @param ruleAssetSavedObjects - Array of rule assets to make upgradeable @@ -183,7 +166,7 @@ export default ({ getService }: FtrProviderContext): void => { const ruleAssetSavedObjects = await setupInitialRules(); // Customize rule-1 with custom name and description (remains disabled) - await customizeRule('rule-1', { + await customizeRule(detectionsApi, 'rule-1', { name: 'Customized Rule Name', description: 'This is a customized rule description', }); @@ -218,7 +201,7 @@ export default ({ getService }: FtrProviderContext): void => { const ruleAssetSavedObjects = await setupInitialRules(); // Customize and enable rule-1 - await customizeRule('rule-1', { + await customizeRule(detectionsApi, 'rule-1', { name: 'Customized Enabled Rule', description: 'This is a customized and enabled rule', enabled: true, @@ -249,7 +232,7 @@ export default ({ getService }: FtrProviderContext): void => { const ruleAssetSavedObjects = await setupInitialRules(); // Customize and enable one rule - await customizeRule('rule-1', { + await customizeRule(detectionsApi, 'rule-1', { name: 'Customized Rule', enabled: true, }); @@ -288,15 +271,15 @@ export default ({ getService }: FtrProviderContext): void => { const ruleAssetSavedObjects = await setupInitialRules(); // Customize and enable multiple rules - await customizeRule('rule-1', { + await customizeRule(detectionsApi, 'rule-1', { name: 'Customized Enabled Rule 1', enabled: true, }); - await customizeRule('rule-2', { + await customizeRule(detectionsApi, 'rule-2', { name: 'Customized Enabled Rule 2', enabled: true, }); - await customizeRule('rule-3', { + await customizeRule(detectionsApi, 'rule-3', { name: 'Customized Enabled Rule 3', enabled: true, }); @@ -365,7 +348,7 @@ export default ({ getService }: FtrProviderContext): void => { const ruleAssetSavedObjects = await setupInitialRules(); // rule-1: enabled + customized - await customizeRule('rule-1', { + await customizeRule(detectionsApi, 'rule-1', { name: 'Enabled Customized Rule', enabled: true, }); @@ -381,7 +364,7 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); // rule-3: disabled + customized - await customizeRule('rule-3', { + await customizeRule(detectionsApi, 'rule-3', { name: 'Disabled Customized Rule', // enabled: false is default }); @@ -413,7 +396,7 @@ export default ({ getService }: FtrProviderContext): void => { const ruleAssetSavedObjects = await setupInitialRules(); // Customize and enable only one rule - await customizeRule('rule-1', { + await customizeRule(detectionsApi, 'rule-1', { name: 'Single Customized Enabled Rule', description: 'The only upgradeable rule', enabled: true, diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/customize_rule.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/customize_rule.ts new file mode 100644 index 0000000000000..8be91ac3c4298 --- /dev/null +++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/customize_rule.ts @@ -0,0 +1,28 @@ +/* + * 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 { SecuritySolutionApiProvider as DetectionsApiProvider } from '@kbn/security-solution-test-api-clients/supertest/detections.gen'; + +import { getCustomQueryRuleParams } from '../get_rule_params'; + +/** + * Customizes a rule with the given parameters. + * @param ruleId - The ID of the rule to customize + * @param customizations - Custom parameters for the rule + */ +export async function customizeRule( + detectionsApi: ReturnType, + ruleId: string, + customizations: Record +) { + const customRuleParams = getCustomQueryRuleParams({ + rule_id: ruleId, + ...customizations, + }); + + await detectionsApi.updateRule({ body: customRuleParams }).expect(200); +} diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/index.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/index.ts index 0289f78067217..4d322d79dc8d2 100644 --- a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/index.ts +++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/index.ts @@ -5,6 +5,7 @@ * 2.0. */ export * from './create_prebuilt_rule_saved_objects'; +export * from './customize_rule'; export * from './delete_all_prebuilt_rule_assets'; export * from './delete_all_timelines'; export * from './delete_fleet_packages';