diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index ac26ef80cf21c..fcec53eb0cf30 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -267,6 +267,14 @@ export const DETECTION_ENGINE_RULE_EXECUTION_EVENTS_URL = export const detectionEngineRuleExecutionEventsUrl = (ruleId: string) => `${INTERNAL_DETECTION_ENGINE_URL}/rules/${ruleId}/execution/events` as const; +/** + * Telemetry detection endpoint for any previews requested of what data we are + * providing through UI/UX and for e2e tests. + * curl http//localhost:5601/internal/security_solution/telemetry + * to see the contents + */ +export const SECURITY_TELEMETRY_URL = `/internal/security_solution/telemetry` as const; + export const TIMELINE_RESOLVE_URL = '/api/timeline/resolve' as const; export const TIMELINE_URL = '/api/timeline' as const; export const TIMELINES_URL = '/api/timelines' as const; diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index a3a3c3a23acf0..97642faf6a572 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -24,6 +24,16 @@ export const allowedExperimentalValues = Object.freeze({ pendingActionResponsesWithAck: true, rulesBulkEditEnabled: true, policyListEnabled: false, + + /** + * This is used for enabling the end to end tests for the security_solution telemetry. + * We disable the telemetry since we don't have specific roles or permissions around it and + * we don't want people to be able to violate security by getting access to whole documents + * around telemetry they should not. + * @see telemetry_detection_rules_preview_route.ts + * @see test/detection_engine_api_integration/security_and_spaces/tests/telemetry/README.md + */ + previewTelemetryUrlEnabled: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts index 81dcbd07f4dd3..3e7a82a3fdbda 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts @@ -20,7 +20,7 @@ import { DETECTION_ENGINE_SIGNALS_STATUS_URL, } from '../../../../../common/constants'; import { buildSiemResponse } from '../utils'; -import { TelemetryEventsSender } from '../../../telemetry/sender'; +import { ITelemetryEventsSender } from '../../../telemetry/sender'; import { INSIGHTS_CHANNEL } from '../../../telemetry/constants'; import { SetupPlugins } from '../../../../plugin'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; @@ -33,7 +33,7 @@ export const setSignalsStatusRoute = ( router: SecuritySolutionPluginRouter, logger: Logger, security: SetupPlugins['security'], - sender: TelemetryEventsSender + sender: ITelemetryEventsSender ) => { router.post( { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/telemetry/telemetry_detection_rules_preview_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/telemetry/telemetry_detection_rules_preview_route.ts new file mode 100644 index 0000000000000..2aad64324cfae --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/telemetry/telemetry_detection_rules_preview_route.ts @@ -0,0 +1,68 @@ +/* + * 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 { Logger } from 'src/core/server'; + +import { SECURITY_TELEMETRY_URL } from '../../../../../common/constants'; +import type { SecuritySolutionPluginRouter } from '../../../../types'; +import { ITelemetryReceiver } from '../../../telemetry/receiver'; +import { ITelemetryEventsSender } from '../../../telemetry/sender'; +import { getDetectionRulesPreview } from './utils/get_detecton_rules_preview'; +import { getSecurityListsPreview } from './utils/get_security_lists_preview'; +import { getEndpointPreview } from './utils/get_endpoint_preview'; +import { getDiagnosticsPreview } from './utils/get_diagnostics_preview'; + +export const telemetryDetectionRulesPreviewRoute = ( + router: SecuritySolutionPluginRouter, + logger: Logger, + telemetryReceiver: ITelemetryReceiver, + telemetrySender: ITelemetryEventsSender +) => { + router.get( + { + path: SECURITY_TELEMETRY_URL, + validate: false, + options: { + tags: ['access:securitySolution'], + }, + }, + async (context, request, response) => { + const detectionRules = await getDetectionRulesPreview({ + logger, + telemetryReceiver, + telemetrySender, + }); + + const securityLists = await getSecurityListsPreview({ + logger, + telemetryReceiver, + telemetrySender, + }); + + const endpoints = await getEndpointPreview({ + logger, + telemetryReceiver, + telemetrySender, + }); + + const diagnostics = await getDiagnosticsPreview({ + logger, + telemetryReceiver, + telemetrySender, + }); + + return response.ok({ + body: { + detection_rules: detectionRules, + security_lists: securityLists, + endpoints, + diagnostics, + }, + }); + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/telemetry/utils/get_detecton_rules_preview.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/telemetry/utils/get_detecton_rules_preview.ts new file mode 100644 index 0000000000000..b7c9215e6c190 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/telemetry/utils/get_detecton_rules_preview.ts @@ -0,0 +1,41 @@ +/* + * 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 { Logger } from 'src/core/server'; + +import { PreviewTelemetryEventsSender } from '../../../../telemetry/preview_sender'; +import { ITelemetryReceiver } from '../../../../telemetry/receiver'; +import { ITelemetryEventsSender } from '../../../../telemetry/sender'; +import { createTelemetryDetectionRuleListsTaskConfig } from '../../../../telemetry/tasks/detection_rule'; +import { parseNdjson } from './parse_ndjson'; + +export const getDetectionRulesPreview = async ({ + logger, + telemetryReceiver, + telemetrySender, +}: { + logger: Logger; + telemetryReceiver: ITelemetryReceiver; + telemetrySender: ITelemetryEventsSender; +}): Promise => { + const taskExecutionPeriod = { + last: new Date(0).toISOString(), + current: new Date().toISOString(), + }; + + const taskSender = new PreviewTelemetryEventsSender(logger, telemetrySender); + const task = createTelemetryDetectionRuleListsTaskConfig(1000); + await task.runTask( + 'detection-rules-preview', + logger, + telemetryReceiver, + taskSender, + taskExecutionPeriod + ); + const messages = taskSender.getSentMessages(); + return parseNdjson(messages); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/telemetry/utils/get_diagnostics_preview.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/telemetry/utils/get_diagnostics_preview.ts new file mode 100644 index 0000000000000..e630ef7f250d3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/telemetry/utils/get_diagnostics_preview.ts @@ -0,0 +1,41 @@ +/* + * 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 { Logger } from 'src/core/server'; + +import { PreviewTelemetryEventsSender } from '../../../../telemetry/preview_sender'; +import { ITelemetryReceiver } from '../../../../telemetry/receiver'; +import { ITelemetryEventsSender } from '../../../../telemetry/sender'; +import { createTelemetryDiagnosticsTaskConfig } from '../../../../telemetry/tasks/diagnostic'; +import { parseNdjson } from './parse_ndjson'; + +export const getDiagnosticsPreview = async ({ + logger, + telemetryReceiver, + telemetrySender, +}: { + logger: Logger; + telemetryReceiver: ITelemetryReceiver; + telemetrySender: ITelemetryEventsSender; +}): Promise => { + const taskExecutionPeriod = { + last: new Date(0).toISOString(), + current: new Date().toISOString(), + }; + + const taskSender = new PreviewTelemetryEventsSender(logger, telemetrySender); + const task = createTelemetryDiagnosticsTaskConfig(); + await task.runTask( + 'diagnostics-preview', + logger, + telemetryReceiver, + taskSender, + taskExecutionPeriod + ); + const messages = taskSender.getSentMessages(); + return parseNdjson(messages); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/telemetry/utils/get_endpoint_preview.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/telemetry/utils/get_endpoint_preview.ts new file mode 100644 index 0000000000000..0ac9c95dadd40 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/telemetry/utils/get_endpoint_preview.ts @@ -0,0 +1,41 @@ +/* + * 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 { Logger } from 'src/core/server'; + +import { PreviewTelemetryEventsSender } from '../../../../telemetry/preview_sender'; +import { ITelemetryReceiver } from '../../../../telemetry/receiver'; +import { ITelemetryEventsSender } from '../../../../telemetry/sender'; +import { createTelemetryEndpointTaskConfig } from '../../../../telemetry/tasks/endpoint'; +import { parseNdjson } from './parse_ndjson'; + +export const getEndpointPreview = async ({ + logger, + telemetryReceiver, + telemetrySender, +}: { + logger: Logger; + telemetryReceiver: ITelemetryReceiver; + telemetrySender: ITelemetryEventsSender; +}): Promise => { + const taskExecutionPeriod = { + last: new Date(0).toISOString(), + current: new Date().toISOString(), + }; + + const taskSender = new PreviewTelemetryEventsSender(logger, telemetrySender); + const task = createTelemetryEndpointTaskConfig(1000); + await task.runTask( + 'endpoint-preview', + logger, + telemetryReceiver, + taskSender, + taskExecutionPeriod + ); + const messages = taskSender.getSentMessages(); + return parseNdjson(messages); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/telemetry/utils/get_security_lists_preview.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/telemetry/utils/get_security_lists_preview.ts new file mode 100644 index 0000000000000..ac79fea7acf9e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/telemetry/utils/get_security_lists_preview.ts @@ -0,0 +1,41 @@ +/* + * 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 { Logger } from 'src/core/server'; + +import { PreviewTelemetryEventsSender } from '../../../../telemetry/preview_sender'; +import { ITelemetryReceiver } from '../../../../telemetry/receiver'; +import { ITelemetryEventsSender } from '../../../../telemetry/sender'; +import { createTelemetrySecurityListTaskConfig } from '../../../../telemetry/tasks/security_lists'; +import { parseNdjson } from './parse_ndjson'; + +export const getSecurityListsPreview = async ({ + logger, + telemetryReceiver, + telemetrySender, +}: { + logger: Logger; + telemetryReceiver: ITelemetryReceiver; + telemetrySender: ITelemetryEventsSender; +}): Promise => { + const taskExecutionPeriod = { + last: new Date(0).toISOString(), + current: new Date().toISOString(), + }; + + const taskSender = new PreviewTelemetryEventsSender(logger, telemetrySender); + const task = createTelemetrySecurityListTaskConfig(1000); + await task.runTask( + 'security-lists-preview', + logger, + telemetryReceiver, + taskSender, + taskExecutionPeriod + ); + const messages = taskSender.getSentMessages(); + return parseNdjson(messages); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/telemetry/utils/parse_ndjson.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/telemetry/utils/parse_ndjson.ts new file mode 100644 index 0000000000000..25ffa5af1e793 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/telemetry/utils/parse_ndjson.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export const parseNdjson = (messages: string[]): object[][] => { + return messages.map((message) => { + const splitByNewLine = message.split('\n'); + const linesParsed = splitByNewLine.flatMap((line) => { + try { + return JSON.parse(line); + } catch (error) { + return []; + } + }); + return linesParsed; + }); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts index 9aae815a4188a..728e0e578853b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts @@ -37,7 +37,7 @@ import { import { ExperimentalFeatures } from '../../../../common/experimental_features'; import { IEventLogService } from '../../../../../event_log/server'; import { AlertsFieldMap, RulesFieldMap } from '../../../../common/field_maps'; -import { TelemetryEventsSender } from '../../telemetry/sender'; +import { ITelemetryEventsSender } from '../../telemetry/sender'; import { RuleExecutionLoggerFactory } from '../rule_execution_log'; import { commonParamsCamelToSnake } from '../schemas/rule_converters'; @@ -128,6 +128,6 @@ export interface CreateRuleOptions { experimentalFeatures: ExperimentalFeatures; logger: Logger; ml?: SetupPlugins['ml']; - eventsTelemetry?: TelemetryEventsSender | undefined; + eventsTelemetry?: ITelemetryEventsSender | undefined; version: string; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts index 47492f1db7fa9..120bf2c2ebfce 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts @@ -17,7 +17,7 @@ import { getFilter } from '../get_filter'; import { getInputIndex } from '../get_input_output_index'; import { searchAfterAndBulkCreate } from '../search_after_bulk_create'; import { RuleRangeTuple, BulkCreate, WrapHits } from '../types'; -import { TelemetryEventsSender } from '../../../telemetry/sender'; +import { ITelemetryEventsSender } from '../../../telemetry/sender'; import { BuildRuleMessage } from '../rule_messages'; import { CompleteRule, SavedQueryRuleParams, QueryRuleParams } from '../../schemas/rule_schemas'; import { ExperimentalFeatures } from '../../../../../common/experimental_features'; @@ -48,7 +48,7 @@ export const queryExecutor = async ({ version: string; searchAfterSize: number; logger: Logger; - eventsTelemetry: TelemetryEventsSender | undefined; + eventsTelemetry: ITelemetryEventsSender | undefined; buildRuleMessage: BuildRuleMessage; bulkCreate: BulkCreate; wrapHits: WrapHits; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threat_match.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threat_match.ts index b8ffa1a86c718..00edf2fffc8e0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threat_match.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threat_match.ts @@ -15,7 +15,7 @@ import { import { ListClient } from '../../../../../../lists/server'; import { getInputIndex } from '../get_input_output_index'; import { RuleRangeTuple, BulkCreate, WrapHits } from '../types'; -import { TelemetryEventsSender } from '../../../telemetry/sender'; +import { ITelemetryEventsSender } from '../../../telemetry/sender'; import { BuildRuleMessage } from '../rule_messages'; import { createThreatSignals } from '../threat_mapping/create_threat_signals'; import { CompleteRule, ThreatRuleParams } from '../../schemas/rule_schemas'; @@ -46,7 +46,7 @@ export const threatMatchExecutor = async ({ version: string; searchAfterSize: number; logger: Logger; - eventsTelemetry: TelemetryEventsSender | undefined; + eventsTelemetry: ITelemetryEventsSender | undefined; experimentalFeatures: ExperimentalFeatures; buildRuleMessage: BuildRuleMessage; bulkCreate: BulkCreate; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.ts index f9a5e4acb3160..5904f943183c3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { TelemetryEventsSender } from '../../telemetry/sender'; +import { ITelemetryEventsSender } from '../../telemetry/sender'; import { TelemetryEvent } from '../../telemetry/types'; import { BuildRuleMessage } from './rule_messages'; import { SignalSearchResponse, SignalSource } from './types'; @@ -29,7 +29,7 @@ export function selectEvents(filteredEvents: SignalSearchResponse): TelemetryEve export function sendAlertTelemetryEvents( logger: Logger, - eventsTelemetry: TelemetryEventsSender | undefined, + eventsTelemetry: ITelemetryEventsSender | undefined, filteredEvents: SignalSearchResponse, buildRuleMessage: BuildRuleMessage ) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts index b87c236afc140..45fa47288a958 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts @@ -25,7 +25,7 @@ import { AlertServices, } from '../../../../../../alerting/server'; import { ElasticsearchClient, Logger } from '../../../../../../../../src/core/server'; -import { TelemetryEventsSender } from '../../../telemetry/sender'; +import { ITelemetryEventsSender } from '../../../telemetry/sender'; import { BuildRuleMessage } from '../rule_messages'; import { BulkCreate, @@ -44,7 +44,7 @@ export interface CreateThreatSignalsOptions { bulkCreate: BulkCreate; completeRule: CompleteRule; concurrentSearches: ConcurrentSearches; - eventsTelemetry: TelemetryEventsSender | undefined; + eventsTelemetry: ITelemetryEventsSender | undefined; exceptionItems: ExceptionListItemSchema[]; filters: unknown[]; inputIndex: string[]; @@ -75,7 +75,7 @@ export interface CreateThreatSignalOptions { completeRule: CompleteRule; currentResult: SearchAfterAndBulkCreateReturnType; currentThreatList: ThreatListItem[]; - eventsTelemetry: TelemetryEventsSender | undefined; + eventsTelemetry: ITelemetryEventsSender | undefined; exceptionItems: ExceptionListItemSchema[]; filters: unknown[]; inputIndex: string[]; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index 1af6e62d30ff7..19e0c36bae052 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -30,7 +30,7 @@ import { import { ListClient } from '../../../../../lists/server'; import { Logger } from '../../../../../../../src/core/server'; import { BuildRuleMessage } from './rule_messages'; -import { TelemetryEventsSender } from '../../telemetry/sender'; +import { ITelemetryEventsSender } from '../../telemetry/sender'; import { CompleteRule, RuleParams } from '../schemas/rule_schemas'; import { GenericBulkCreateResponse } from './bulk_create_factory'; import { EcsFieldMap } from '../../../../../rule_registry/common/assets/field_maps/ecs_field_map'; @@ -314,7 +314,7 @@ export interface SearchAfterAndBulkCreateParams { listClient: ListClient; exceptionsList: ExceptionListItemSchema[]; logger: Logger; - eventsTelemetry: TelemetryEventsSender | undefined; + eventsTelemetry: ITelemetryEventsSender | undefined; id: string; inputIndexPattern: string[]; signalsIndex: string; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/preview_sender.ts b/x-pack/plugins/security_solution/server/lib/telemetry/preview_sender.ts new file mode 100644 index 0000000000000..3c5f3bdd52bde --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/telemetry/preview_sender.ts @@ -0,0 +1,139 @@ +/* + * 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 axios, { AxiosInstance, AxiosResponse } from 'axios'; + +import { Logger } from 'src/core/server'; +import { TelemetryPluginStart, TelemetryPluginSetup } from 'src/plugins/telemetry/server'; +import { UsageCounter } from 'src/plugins/usage_collection/server'; + +import { + TaskManagerSetupContract, + TaskManagerStartContract, +} from '../../../../task_manager/server'; +import { ITelemetryEventsSender } from './sender'; +import { TelemetryEvent } from './types'; +import { ITelemetryReceiver } from './receiver'; + +/** + * Preview telemetry events sender for the telemetry route. + * @see telemetry_detection_rules_preview_route + */ +export class PreviewTelemetryEventsSender implements ITelemetryEventsSender { + /** Inner composite telemetry events sender */ + private composite: ITelemetryEventsSender; + + /** Axios local instance */ + private axiosInstance = axios.create(); + + /** Last sent message */ + private sentMessages: string[] = []; + + /** Logger for this class */ + private logger: Logger; + + constructor(logger: Logger, composite: ITelemetryEventsSender) { + this.logger = logger; + this.composite = composite; + + /** + * Intercept the last message and save it for the preview within the lastSentMessage + * Reject the request intentionally to stop from sending to the server + */ + this.axiosInstance.interceptors.request.use((config) => { + this.logger.debug( + `Intercepting telemetry', ${JSON.stringify( + config.data + )} and not sending data to the telemetry server` + ); + const data = config.data != null ? [config.data] : []; + this.sentMessages = [...this.sentMessages, ...data]; + return Promise.reject(new Error('Not sending to telemetry server')); + }); + + /** + * Create a fake response for the preview on return within the error section. + * @param error The error we don't do anything with + * @returns The response resolved to stop the chain from continuing. + */ + this.axiosInstance.interceptors.response.use( + (response) => response, + (error) => { + // create a fake response for the preview as if the server had sent it back to us + const okResponse: AxiosResponse = { + data: {}, + status: 200, + statusText: 'ok', + headers: {}, + config: {}, + }; + return Promise.resolve(okResponse); + } + ); + } + + public getSentMessages() { + return this.sentMessages; + } + + public setup( + telemetryReceiver: ITelemetryReceiver, + telemetrySetup?: TelemetryPluginSetup, + taskManager?: TaskManagerSetupContract, + telemetryUsageCounter?: UsageCounter + ) { + return this.composite.setup( + telemetryReceiver, + telemetrySetup, + taskManager, + telemetryUsageCounter + ); + } + + public getClusterID(): string | undefined { + return this.composite.getClusterID(); + } + + public start( + telemetryStart?: TelemetryPluginStart, + taskManager?: TaskManagerStartContract, + receiver?: ITelemetryReceiver + ): void { + return this.composite.start(telemetryStart, taskManager, receiver); + } + + public stop(): void { + return this.composite.stop(); + } + + public async queueTelemetryEvents(events: TelemetryEvent[]) { + const result = this.composite.queueTelemetryEvents(events); + await this.composite.sendIfDue(this.axiosInstance); + return result; + } + + public isTelemetryOptedIn(): Promise { + return this.composite.isTelemetryOptedIn(); + } + + public sendIfDue(axiosInstance?: AxiosInstance): Promise { + return this.composite.sendIfDue(axiosInstance); + } + + public processEvents(events: TelemetryEvent[]): TelemetryEvent[] { + return this.composite.processEvents(events); + } + + public async sendOnDemand(channel: string, toSend: unknown[]) { + const result = await this.composite.sendOnDemand(channel, toSend, this.axiosInstance); + return result; + } + + public getV3UrlFromV2(v2url: string, channel: string): string { + return this.composite.getV3UrlFromV2(v2url, channel); + } +} diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts b/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts index 75bf3650158dc..f96643663b48b 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts @@ -11,7 +11,11 @@ import { ElasticsearchClient, SavedObjectsClientContract, } from 'src/core/server'; -import { SearchRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { + AggregationsAggregate, + SearchRequest, + SearchResponse, +} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants'; import { EQL_RULE_TYPE_ID, @@ -22,6 +26,8 @@ import { SIGNALS_ID, THRESHOLD_RULE_TYPE_ID, } from '@kbn/securitysolution-rules'; +import { TransportResult } from '@elastic/elasticsearch'; +import { Agent, AgentPolicy } from '../../../../fleet/common'; import { AgentClient, AgentPolicyServiceInterface } from '../../../../fleet/server'; import { ExceptionListClient } from '../../../../lists/server'; import { EndpointAppContextService } from '../../endpoint/endpoint_app_context_services'; @@ -37,9 +43,89 @@ import type { ESClusterInfo, GetEndpointListResponse, RuleSearchResult, + ExceptionListItem, } from './types'; -export class TelemetryReceiver { +export interface ITelemetryReceiver { + start( + core?: CoreStart, + kibanaIndex?: string, + endpointContextService?: EndpointAppContextService, + exceptionListClient?: ExceptionListClient + ): Promise; + + getClusterInfo(): ESClusterInfo | undefined; + + fetchFleetAgents(): Promise< + | { + agents: Agent[]; + total: number; + page: number; + perPage: number; + } + | undefined + >; + + fetchEndpointPolicyResponses( + executeFrom: string, + executeTo: string + ): Promise< + TransportResult>, unknown> + >; + + fetchEndpointMetrics( + executeFrom: string, + executeTo: string + ): Promise< + TransportResult>, unknown> + >; + + fetchDiagnosticAlerts( + executeFrom: string, + executeTo: string + ): Promise>>; + + fetchPolicyConfigs(id: string): Promise; + + fetchTrustedApplications(): Promise<{ + data: ExceptionListItem[] | undefined; + total: number; + page: number; + per_page: number; + }>; + + fetchEndpointList(listId: string): Promise; + + fetchDetectionRules(): Promise< + TransportResult< + SearchResponse>, + unknown + > + >; + + fetchDetectionExceptionList( + listId: string, + ruleVersion: number + ): Promise<{ + data: ExceptionListItem[]; + total: number; + page: number; + per_page: number; + }>; + + fetchClusterInfo(): Promise; + + fetchLicenseInfo(): Promise; + + copyLicenseFields(lic: ESLicense): { + issuer?: string | undefined; + issued_to?: string | undefined; + uid: string; + status: string; + type: string; + }; +} +export class TelemetryReceiver implements ITelemetryReceiver { private readonly logger: Logger; private agentClient?: AgentClient; private agentPolicyService?: AgentPolicyServiceInterface; @@ -230,6 +316,9 @@ export class TelemetryReceiver { throw Error('exception list client is unavailable: cannot retrieve trusted applications'); } + // Ensure list is created if it does not exist + await this.exceptionListClient.createTrustedAppsList(); + const results = await this.exceptionListClient.findExceptionListItem({ listId: ENDPOINT_TRUSTED_APPS_LIST_ID, page: 1, @@ -254,7 +343,7 @@ export class TelemetryReceiver { } // Ensure list is created if it does not exist - await this.exceptionListClient.createTrustedAppsList(); + await this.exceptionListClient.createEndpointList(); const results = await this.exceptionListClient.findExceptionListItem({ listId, diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts index fbc42acca036b..07c5ea408cbdf 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -6,18 +6,18 @@ */ import { cloneDeep } from 'lodash'; -import axios from 'axios'; import { URL } from 'url'; import { transformDataToNdjson } from '@kbn/securitysolution-utils'; import { Logger } from 'src/core/server'; import { TelemetryPluginStart, TelemetryPluginSetup } from 'src/plugins/telemetry/server'; import { UsageCounter } from 'src/plugins/usage_collection/server'; +import axios, { AxiosInstance } from 'axios'; import { TaskManagerSetupContract, TaskManagerStartContract, } from '../../../../task_manager/server'; -import { TelemetryReceiver } from './receiver'; +import { ITelemetryReceiver } from './receiver'; import { allowlistEventFields, copyAllowlistedFields } from './filters'; import { createTelemetryTaskConfigs } from './tasks'; import { createUsageCounterLabel } from './helpers'; @@ -27,7 +27,32 @@ import { SecurityTelemetryTask, SecurityTelemetryTaskConfig } from './task'; const usageLabelPrefix: string[] = ['security_telemetry', 'sender']; -export class TelemetryEventsSender { +export interface ITelemetryEventsSender { + setup( + telemetryReceiver: ITelemetryReceiver, + telemetrySetup?: TelemetryPluginSetup, + taskManager?: TaskManagerSetupContract, + telemetryUsageCounter?: UsageCounter + ): void; + + getClusterID(): string | undefined; + + start( + telemetryStart?: TelemetryPluginStart, + taskManager?: TaskManagerStartContract, + receiver?: ITelemetryReceiver + ): void; + + stop(): void; + queueTelemetryEvents(events: TelemetryEvent[]): void; + isTelemetryOptedIn(): Promise; + sendIfDue(axiosInstance?: AxiosInstance): Promise; + processEvents(events: TelemetryEvent[]): TelemetryEvent[]; + sendOnDemand(channel: string, toSend: unknown[], axiosInstance?: AxiosInstance): Promise; + getV3UrlFromV2(v2url: string, channel: string): string; +} + +export class TelemetryEventsSender implements ITelemetryEventsSender { private readonly initialCheckDelayMs = 10 * 1000; private readonly checkIntervalMs = 60 * 1000; private readonly logger: Logger; @@ -36,7 +61,7 @@ export class TelemetryEventsSender { private telemetrySetup?: TelemetryPluginSetup; private intervalId?: NodeJS.Timeout; private isSending = false; - private receiver: TelemetryReceiver | undefined; + private receiver: ITelemetryReceiver | undefined; private queue: TelemetryEvent[] = []; private isOptedIn?: boolean = true; // Assume true until the first check @@ -48,7 +73,7 @@ export class TelemetryEventsSender { } public setup( - telemetryReceiver: TelemetryReceiver, + telemetryReceiver: ITelemetryReceiver, telemetrySetup?: TelemetryPluginSetup, taskManager?: TaskManagerSetupContract, telemetryUsageCounter?: UsageCounter @@ -74,7 +99,7 @@ export class TelemetryEventsSender { public start( telemetryStart?: TelemetryPluginStart, taskManager?: TaskManagerStartContract, - receiver?: TelemetryReceiver + receiver?: ITelemetryReceiver ) { this.telemetryStart = telemetryStart; this.receiver = receiver; @@ -133,7 +158,7 @@ export class TelemetryEventsSender { return this.isOptedIn === true; } - private async sendIfDue() { + public async sendIfDue(axiosInstance: AxiosInstance = axios) { if (this.isSending) { return; } @@ -180,7 +205,8 @@ export class TelemetryEventsSender { clusterInfo?.cluster_uuid, clusterInfo?.cluster_name, clusterInfo?.version?.number, - licenseInfo?.uid + licenseInfo?.uid, + axiosInstance ); } catch (err) { this.logger.warn(`Error sending telemetry events data: ${err}`); @@ -203,7 +229,11 @@ export class TelemetryEventsSender { * @param channel the elastic telemetry channel * @param toSend telemetry events */ - public async sendOnDemand(channel: string, toSend: unknown[]) { + public async sendOnDemand( + channel: string, + toSend: unknown[], + axiosInstance: AxiosInstance = axios + ) { const clusterInfo = this.receiver?.getClusterInfo(); try { const [telemetryUrl, licenseInfo] = await Promise.all([ @@ -223,7 +253,8 @@ export class TelemetryEventsSender { clusterInfo?.cluster_uuid, clusterInfo?.cluster_name, clusterInfo?.version?.number, - licenseInfo?.uid + licenseInfo?.uid, + axiosInstance ); } catch (err) { this.logger.warn(`Error sending telemetry events data: ${err}`); @@ -258,13 +289,14 @@ export class TelemetryEventsSender { clusterUuid: string | undefined, clusterName: string | undefined, clusterVersionNumber: string | undefined, - licenseId: string | undefined + licenseId: string | undefined, + axiosInstance: AxiosInstance = axios ) { const ndjson = transformDataToNdjson(events); try { this.logger.debug(`Sending ${events.length} telemetry events to ${channel}`); - const resp = await axios.post(telemetryUrl, ndjson, { + const resp = await axiosInstance.post(telemetryUrl, ndjson, { headers: { 'Content-Type': 'application/x-ndjson', 'X-Elastic-Cluster-ID': clusterUuid, diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/task.ts b/x-pack/plugins/security_solution/server/lib/telemetry/task.ts index 15ae5cdb39e50..c3d674f721491 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/task.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/task.ts @@ -12,8 +12,8 @@ import { TaskManagerSetupContract, TaskManagerStartContract, } from '../../../../task_manager/server'; -import { TelemetryReceiver } from './receiver'; -import { TelemetryEventsSender } from './sender'; +import { ITelemetryReceiver } from './receiver'; +import { ITelemetryEventsSender } from './sender'; export interface SecurityTelemetryTaskConfig { type: string; @@ -28,8 +28,8 @@ export interface SecurityTelemetryTaskConfig { export type SecurityTelemetryTaskRunner = ( taskId: string, logger: Logger, - receiver: TelemetryReceiver, - sender: TelemetryEventsSender, + receiver: ITelemetryReceiver, + sender: ITelemetryEventsSender, taskExecutionPeriod: TaskExecutionPeriod ) => Promise; @@ -46,14 +46,14 @@ export type LastExecutionTimestampCalculator = ( export class SecurityTelemetryTask { private readonly config: SecurityTelemetryTaskConfig; private readonly logger: Logger; - private readonly sender: TelemetryEventsSender; - private readonly receiver: TelemetryReceiver; + private readonly sender: ITelemetryEventsSender; + private readonly receiver: ITelemetryReceiver; constructor( config: SecurityTelemetryTaskConfig, logger: Logger, - sender: TelemetryEventsSender, - receiver: TelemetryReceiver + sender: ITelemetryEventsSender, + receiver: ITelemetryReceiver ) { this.config = config; this.logger = logger; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/detection_rule.ts b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/detection_rule.ts index baca71d100cf0..73931f856a336 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/detection_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/detection_rule.ts @@ -8,8 +8,8 @@ import { Logger } from 'src/core/server'; import { LIST_DETECTION_RULE_EXCEPTION, TELEMETRY_CHANNEL_LISTS } from '../constants'; import { batchTelemetryRecords, templateExceptionList } from '../helpers'; -import { TelemetryEventsSender } from '../sender'; -import { TelemetryReceiver } from '../receiver'; +import { ITelemetryEventsSender } from '../sender'; +import { ITelemetryReceiver } from '../receiver'; import type { ExceptionListItem, ESClusterInfo, ESLicense, RuleSearchResult } from '../types'; import { TaskExecutionPeriod } from '../task'; @@ -23,8 +23,8 @@ export function createTelemetryDetectionRuleListsTaskConfig(maxTelemetryBatch: n runTask: async ( taskId: string, logger: Logger, - receiver: TelemetryReceiver, - sender: TelemetryEventsSender, + receiver: ITelemetryReceiver, + sender: ITelemetryEventsSender, taskExecutionPeriod: TaskExecutionPeriod ) => { const [clusterInfoPromise, licenseInfoPromise] = await Promise.allSettled([ @@ -88,9 +88,10 @@ export function createTelemetryDetectionRuleListsTaskConfig(maxTelemetryBatch: n LIST_DETECTION_RULE_EXCEPTION ); - batchTelemetryRecords(detectionRuleExceptionsJson, maxTelemetryBatch).forEach((batch) => { - sender.sendOnDemand(TELEMETRY_CHANNEL_LISTS, batch); - }); + const batches = batchTelemetryRecords(detectionRuleExceptionsJson, maxTelemetryBatch); + for (const batch of batches) { + await sender.sendOnDemand(TELEMETRY_CHANNEL_LISTS, batch); + } return detectionRuleExceptions.length; }, diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/diagnostic.ts b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/diagnostic.ts index fae6172c268f4..a798077e62aff 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/diagnostic.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/diagnostic.ts @@ -7,9 +7,9 @@ import { Logger } from 'src/core/server'; import { getPreviousDiagTaskTimestamp } from '../helpers'; -import { TelemetryEventsSender } from '../sender'; +import { ITelemetryEventsSender } from '../sender'; import type { TelemetryEvent } from '../types'; -import { TelemetryReceiver } from '../receiver'; +import { ITelemetryReceiver } from '../receiver'; import { TaskExecutionPeriod } from '../task'; export function createTelemetryDiagnosticsTaskConfig() { @@ -23,8 +23,8 @@ export function createTelemetryDiagnosticsTaskConfig() { runTask: async ( taskId: string, logger: Logger, - receiver: TelemetryReceiver, - sender: TelemetryEventsSender, + receiver: ITelemetryReceiver, + sender: ITelemetryEventsSender, taskExecutionPeriod: TaskExecutionPeriod ) => { if (!taskExecutionPeriod.last) { diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts index 000182a3d7a9a..6be174cdf33e7 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts @@ -6,7 +6,7 @@ */ import { Logger } from 'src/core/server'; -import { TelemetryEventsSender } from '../sender'; +import { ITelemetryEventsSender } from '../sender'; import type { EndpointMetricsAggregation, EndpointPolicyResponseAggregation, @@ -14,7 +14,7 @@ import type { ESClusterInfo, ESLicense, } from '../types'; -import { TelemetryReceiver } from '../receiver'; +import { ITelemetryReceiver } from '../receiver'; import { TaskExecutionPeriod } from '../task'; import { batchTelemetryRecords, @@ -47,8 +47,8 @@ export function createTelemetryEndpointTaskConfig(maxTelemetryBatch: number) { runTask: async ( taskId: string, logger: Logger, - receiver: TelemetryReceiver, - sender: TelemetryEventsSender, + receiver: ITelemetryReceiver, + sender: ITelemetryEventsSender, taskExecutionPeriod: TaskExecutionPeriod ) => { if (!taskExecutionPeriod.last) { @@ -255,9 +255,10 @@ export function createTelemetryEndpointTaskConfig(maxTelemetryBatch: number) { * * Send the documents in a batches of maxTelemetryBatch */ - batchTelemetryRecords(telemetryPayloads, maxTelemetryBatch).forEach((telemetryBatch) => - sender.sendOnDemand(TELEMETRY_CHANNEL_ENDPOINT_META, telemetryBatch) - ); + const batches = batchTelemetryRecords(telemetryPayloads, maxTelemetryBatch); + for (const batch of batches) { + await sender.sendOnDemand(TELEMETRY_CHANNEL_ENDPOINT_META, batch); + } return telemetryPayloads.length; } catch (err) { logger.warn('could not complete endpoint alert telemetry task'); @@ -268,7 +269,7 @@ export function createTelemetryEndpointTaskConfig(maxTelemetryBatch: number) { } async function fetchEndpointData( - receiver: TelemetryReceiver, + receiver: ITelemetryReceiver, executeFrom: string, executeTo: string ) { diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/security_lists.ts b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/security_lists.ts index d27ab801197d6..f655674b6ca96 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/security_lists.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/security_lists.ts @@ -18,8 +18,8 @@ import { } from '../constants'; import type { ESClusterInfo, ESLicense } from '../types'; import { batchTelemetryRecords, templateExceptionList } from '../helpers'; -import { TelemetryEventsSender } from '../sender'; -import { TelemetryReceiver } from '../receiver'; +import { ITelemetryEventsSender } from '../sender'; +import { ITelemetryReceiver } from '../receiver'; import { TaskExecutionPeriod } from '../task'; export function createTelemetrySecurityListTaskConfig(maxTelemetryBatch: number) { @@ -32,8 +32,8 @@ export function createTelemetrySecurityListTaskConfig(maxTelemetryBatch: number) runTask: async ( taskId: string, logger: Logger, - receiver: TelemetryReceiver, - sender: TelemetryEventsSender, + receiver: ITelemetryReceiver, + sender: ITelemetryEventsSender, taskExecutionPeriod: TaskExecutionPeriod ) => { let count = 0; @@ -65,9 +65,10 @@ export function createTelemetrySecurityListTaskConfig(maxTelemetryBatch: number) logger.debug(`Trusted Apps: ${trustedAppsJson}`); count += trustedAppsJson.length; - batchTelemetryRecords(trustedAppsJson, maxTelemetryBatch).forEach((batch) => - sender.sendOnDemand(TELEMETRY_CHANNEL_LISTS, batch) - ); + const batches = batchTelemetryRecords(trustedAppsJson, maxTelemetryBatch); + for (const batch of batches) { + await sender.sendOnDemand(TELEMETRY_CHANNEL_LISTS, batch); + } } // Lists Telemetry: Endpoint Exceptions @@ -83,9 +84,10 @@ export function createTelemetrySecurityListTaskConfig(maxTelemetryBatch: number) logger.debug(`EP Exceptions: ${epExceptionsJson}`); count += epExceptionsJson.length; - batchTelemetryRecords(epExceptionsJson, maxTelemetryBatch).forEach((batch) => - sender.sendOnDemand(TELEMETRY_CHANNEL_LISTS, batch) - ); + const batches = batchTelemetryRecords(epExceptionsJson, maxTelemetryBatch); + for (const batch of batches) { + await sender.sendOnDemand(TELEMETRY_CHANNEL_LISTS, batch); + } } // Lists Telemetry: Endpoint Event Filters @@ -101,9 +103,10 @@ export function createTelemetrySecurityListTaskConfig(maxTelemetryBatch: number) logger.debug(`EP Event Filters: ${epFiltersJson}`); count += epFiltersJson.length; - batchTelemetryRecords(epFiltersJson, maxTelemetryBatch).forEach((batch) => - sender.sendOnDemand(TELEMETRY_CHANNEL_LISTS, batch) - ); + const batches = batchTelemetryRecords(epFiltersJson, maxTelemetryBatch); + for (const batch of batches) { + await sender.sendOnDemand(TELEMETRY_CHANNEL_LISTS, batch); + } } return count; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 3d7fb8a8743c1..8292f7cec1abe 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -60,8 +60,8 @@ import { EndpointAppContext } from './endpoint/types'; import { initUsageCollectors } from './usage'; import type { SecuritySolutionRequestHandlerContext } from './types'; import { securitySolutionSearchStrategyProvider } from './search_strategy/security_solution'; -import { TelemetryEventsSender } from './lib/telemetry/sender'; -import { TelemetryReceiver } from './lib/telemetry/receiver'; +import { ITelemetryEventsSender, TelemetryEventsSender } from './lib/telemetry/sender'; +import { ITelemetryReceiver, TelemetryReceiver } from './lib/telemetry/receiver'; import { licenseService } from './lib/license'; import { PolicyWatcher } from './endpoint/lib/policy/license_watch'; import { migrateArtifactsToFleet } from './endpoint/lib/artifacts/migrate_artifacts_to_fleet'; @@ -104,8 +104,8 @@ export class Plugin implements ISecuritySolutionPlugin { private readonly appClientFactory: AppClientFactory; private readonly endpointAppContextService = new EndpointAppContextService(); - private readonly telemetryReceiver: TelemetryReceiver; - private readonly telemetryEventsSender: TelemetryEventsSender; + private readonly telemetryReceiver: ITelemetryReceiver; + private readonly telemetryEventsSender: ITelemetryEventsSender; private lists: ListPluginSetup | undefined; // TODO: can we create ListPluginStart? private licensing$!: Observable; @@ -260,7 +260,8 @@ export class Plugin implements ISecuritySolutionPlugin { ruleOptions, core.getStartServices, securityRuleTypeOptions, - previewRuleDataClient + previewRuleDataClient, + this.telemetryReceiver ); registerEndpointRoutes(router, endpointContext); registerLimitedConcurrencyRoutes(core); diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index 13788cbae9fde..935948d6d5938 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -58,7 +58,7 @@ import { persistPinnedEventRoute } from '../lib/timeline/routes/pinned_events'; import { SetupPlugins, StartPlugins } from '../plugin'; import { ConfigType } from '../config'; -import { TelemetryEventsSender } from '../lib/telemetry/sender'; +import { ITelemetryEventsSender } from '../lib/telemetry/sender'; import { installPrepackedTimelinesRoute } from '../lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines'; import { previewRulesRoute } from '../lib/detection_engine/routes/rules/preview_rules_route'; import { @@ -68,13 +68,15 @@ import { // eslint-disable-next-line no-restricted-imports import { legacyCreateLegacyNotificationRoute } from '../lib/detection_engine/routes/rules/legacy_create_legacy_notification'; import { createSourcererDataViewRoute, getSourcererDataViewRoute } from '../lib/sourcerer/routes'; +import { ITelemetryReceiver } from '../lib/telemetry/receiver'; +import { telemetryDetectionRulesPreviewRoute } from '../lib/detection_engine/routes/telemetry/telemetry_detection_rules_preview_route'; export const initRoutes = ( router: SecuritySolutionPluginRouter, config: ConfigType, hasEncryptionKey: boolean, security: SetupPlugins['security'], - telemetrySender: TelemetryEventsSender, + telemetrySender: ITelemetryEventsSender, ml: SetupPlugins['ml'], ruleDataService: RuleDataPluginService, logger: Logger, @@ -82,7 +84,8 @@ export const initRoutes = ( ruleOptions: CreateRuleOptions, getStartServices: StartServicesAccessor, securityRuleTypeOptions: CreateSecurityRuleTypeWrapperProps, - previewRuleDataClient: IRuleDataClient + previewRuleDataClient: IRuleDataClient, + previewTelemetryReceiver: ITelemetryReceiver ) => { const isRuleRegistryEnabled = ruleDataClient != null; // Detection Engine Rule routes that have the REST endpoints of /api/detection_engine/rules @@ -161,4 +164,10 @@ export const initRoutes = ( // Sourcerer API to generate default pattern createSourcererDataViewRoute(router, getStartServices); getSourcererDataViewRoute(router, getStartServices); + + const { previewTelemetryUrlEnabled } = config.experimentalFeatures; + if (previewTelemetryUrlEnabled) { + // telemetry preview endpoint for e2e integration tests only at the moment. + telemetryDetectionRulesPreviewRoute(router, logger, previewTelemetryReceiver, telemetrySender); + } }; diff --git a/x-pack/test/detection_engine_api_integration/common/config.ts b/x-pack/test/detection_engine_api_integration/common/config.ts index fe4d4f63f3e75..950ba3e8e2ce7 100644 --- a/x-pack/test/detection_engine_api_integration/common/config.ts +++ b/x-pack/test/detection_engine_api_integration/common/config.ts @@ -74,7 +74,10 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) '--xpack.ruleRegistry.write.cache.enabled=false', '--xpack.ruleRegistry.unsafe.indexUpgrade.enabled=true', '--xpack.ruleRegistry.unsafe.legacyMultiTenancy.enabled=true', - `--xpack.securitySolution.enableExperimental=${JSON.stringify(['ruleRegistryEnabled'])}`, + `--xpack.securitySolution.enableExperimental=${JSON.stringify([ + 'ruleRegistryEnabled', + 'previewTelemetryUrlEnabled', + ])}`, ...(ssl ? [ `--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/README.md b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/README.md index 2760001a20626..55048f550876a 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/README.md +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/README.md @@ -1,6 +1,8 @@ -These are tests for the telemetry rules within "security_solution/server/usage" -* detection_rules +These are tests for the telemetry rules within "security_solution/server/usage" and "security_solution/server/lib/telemetry" +* task_based (security_solution/server/lib/telemetry) +* usage_collector (security_solution/server/usage) -Detection rules are tests around each of the rule types to affirm they work such as query, eql, etc... This includes -legacy notifications. Once legacy notifications are moved, those tests can be removed too. +Under usage_collector, these are tests around each of the rule types to affirm they work such as query, eql, etc... This includes +legacy notifications. Once legacy notifications are moved, tests specific to it can be removed. +Under task_based, these are tests around task based types such as "detection_rules" and "security_lists" diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/index.ts index cf9db6373033a..ce1966c3175a9 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/index.ts @@ -12,7 +12,12 @@ export default ({ loadTestFile }: FtrProviderContext): void => { describe('Detection rule type telemetry', function () { describe('', function () { this.tags('ciGroup11'); - loadTestFile(require.resolve('./detection_rules')); + loadTestFile(require.resolve('./usage_collector/all_types')); + loadTestFile(require.resolve('./usage_collector/detection_rules')); + + loadTestFile(require.resolve('./task_based/all_types')); + loadTestFile(require.resolve('./task_based/detection_rules')); + loadTestFile(require.resolve('./task_based/security_lists')); }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/task_based/all_types.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/task_based/all_types.ts new file mode 100644 index 0000000000000..323fe9041e1b6 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/task_based/all_types.ts @@ -0,0 +1,56 @@ +/* + * 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 '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSecurityTelemetryStats, +} from '../../../../utils'; +import { deleteAllExceptions } from '../../../../../lists_api_integration/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const log = getService('log'); + const retry = getService('retry'); + + describe('All task telemetry types generically', async () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/security_solution/telemetry'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/security_solution/telemetry'); + }); + + beforeEach(async () => { + await createSignalsIndex(supertest, log); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest, log); + await deleteAllAlerts(supertest, log); + await deleteAllExceptions(supertest, log); + }); + + it('should have initialized empty/zero values when no rules are running', async () => { + await retry.try(async () => { + const stats = await getSecurityTelemetryStats(supertest, log); + expect(stats).to.eql({ + detection_rules: [], + security_lists: [], + endpoints: [], + diagnostics: [], + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/task_based/detection_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/task_based/detection_rules.ts new file mode 100644 index 0000000000000..b80cb9d478ca3 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/task_based/detection_rules.ts @@ -0,0 +1,833 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { + createRule, + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getRule, + getRuleForSignalTesting, + installPrePackagedRules, + getSecurityTelemetryStats, + createExceptionList, + createExceptionListItem, +} from '../../../../utils'; +import { deleteAllExceptions } from '../../../../../lists_api_integration/utils'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../../plugins/security_solution/common/constants'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const log = getService('log'); + const retry = getService('retry'); + + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security.json + // This rule has an existing exceptions_list that we are going to use. + const IMMUTABLE_RULE_ID = '9a1a2dae-0b5f-4c3d-8305-a268d404c306'; + + describe('Detection rule task telemetry', async () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/security_solution/telemetry'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/security_solution/telemetry'); + }); + + beforeEach(async () => { + await createSignalsIndex(supertest, log); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest, log); + await deleteAllAlerts(supertest, log); + await deleteAllExceptions(supertest, log); + }); + + describe('custom rules should never show any detection_rules telemetry data for each list type', () => { + it('should NOT give telemetry/stats for an exception list of type "detection"', async () => { + const rule = getRuleForSignalTesting(['telemetry'], 'rule-1', false); + + // create an exception list container of type "detection" + const { id, list_id, namespace_type, type } = await createExceptionList(supertest, log, { + description: 'description', + list_id: '123', + name: 'test list', + type: 'detection', + }); + + // add 1 item to the exception list + await createExceptionListItem(supertest, log, { + description: 'endpoint description', + entries: [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'something', + }, + ], + list_id: '123', + name: 'endpoint_list', + os_types: [], + type: 'simple', + }); + + // Create the rule with the exception added to it + await createRule(supertest, log, { + ...rule, + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }); + + // Get the stats and ensure they're empty + await retry.try(async () => { + const stats = await getSecurityTelemetryStats(supertest, log); + expect(stats.detection_rules).to.eql([]); + }); + }); + + it('should NOT give telemetry/stats for an exception list of type "endpoint"', async () => { + const rule = getRuleForSignalTesting(['telemetry'], 'rule-1', false); + + // create an exception list container of type "detection" + const { id, list_id, namespace_type, type } = await createExceptionList(supertest, log, { + description: 'description', + list_id: '123', + name: 'test list', + type: 'endpoint', + }); + + // add 1 item to the exception list + await createExceptionListItem(supertest, log, { + description: 'endpoint description', + entries: [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'something', + }, + ], + list_id: '123', + name: 'endpoint_list', + os_types: [], + type: 'simple', + }); + + // Create the rule with the exception added to it + await createRule(supertest, log, { + ...rule, + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }); + + // Get the stats and ensure they're empty + await retry.try(async () => { + const stats = await getSecurityTelemetryStats(supertest, log); + expect(stats.detection_rules).to.eql([]); + }); + }); + + it('should NOT give telemetry/stats for an exception list of type "endpoint_trusted_apps"', async () => { + const rule = getRuleForSignalTesting(['telemetry'], 'rule-1', false); + + // create an exception list container of type "detection" + const { id, list_id, namespace_type, type } = await createExceptionList(supertest, log, { + description: 'description', + list_id: '123', + name: 'test list', + type: 'endpoint_trusted_apps', + }); + + // add 1 item to the exception list + await createExceptionListItem(supertest, log, { + description: 'endpoint description', + entries: [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'something', + }, + ], + list_id: '123', + name: 'endpoint_list', + os_types: [], + type: 'simple', + }); + + // Create the rule with the exception added to it + await createRule(supertest, log, { + ...rule, + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }); + + // Get the stats and ensure they're empty + await retry.try(async () => { + const stats = await getSecurityTelemetryStats(supertest, log); + expect(stats.detection_rules).to.eql([]); + }); + }); + + it('should NOT give telemetry/stats for an exception list of type "endpoint_events"', async () => { + const rule = getRuleForSignalTesting(['telemetry'], 'rule-1', false); + + // create an exception list container of type "detection" + const { id, list_id, namespace_type, type } = await createExceptionList(supertest, log, { + description: 'description', + list_id: '123', + name: 'test list', + type: 'endpoint_events', + }); + + // add 1 item to the exception list + await createExceptionListItem(supertest, log, { + description: 'endpoint description', + entries: [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'something', + }, + ], + list_id: '123', + name: 'endpoint_list', + os_types: [], + type: 'simple', + }); + + // Create the rule with the exception added to it + await createRule(supertest, log, { + ...rule, + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }); + + // Get the stats and ensure they're empty + await retry.try(async () => { + const stats = await getSecurityTelemetryStats(supertest, log); + expect(stats.detection_rules).to.eql([]); + }); + }); + + it('should NOT give telemetry/stats for an exception list of type "endpoint_host_isolation_exceptions"', async () => { + const rule = getRuleForSignalTesting(['telemetry'], 'rule-1', false); + + // create an exception list container of type "detection" + const { id, list_id, namespace_type, type } = await createExceptionList(supertest, log, { + description: 'description', + list_id: '123', + name: 'test list', + type: 'endpoint_host_isolation_exceptions', + }); + + // add 1 item to the exception list + await createExceptionListItem(supertest, log, { + description: 'endpoint description', + entries: [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'something', + }, + ], + list_id: '123', + name: 'endpoint_list', + os_types: [], + type: 'simple', + }); + + // Create the rule with the exception added to it + await createRule(supertest, log, { + ...rule, + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }); + + // Get the stats and ensure they're empty + await retry.try(async () => { + const stats = await getSecurityTelemetryStats(supertest, log); + expect(stats.detection_rules).to.eql([]); + }); + }); + }); + + describe('pre-built/immutable/elastic rules should show detection_rules telemetry data for each list type', () => { + beforeEach(async () => { + // install prepackaged rules to get immutable rules for testing + await installPrePackagedRules(supertest, log); + }); + + it('should return mutating types such as "id", "@timestamp", etc... for list of type "detection"', async () => { + // create an exception list container of type "detection" + const { id, list_id, namespace_type, type } = await createExceptionList(supertest, log, { + description: 'description', + list_id: '123', + name: 'test list', + type: 'detection', + }); + + // add 1 item to the exception list + await createExceptionListItem(supertest, log, { + description: 'endpoint description', + entries: [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'something', + }, + ], + list_id: '123', + name: 'endpoint_list', + os_types: [], + type: 'simple', + }); + + // add the exception list to the pre-built/immutable/elastic rule using "PATCH" endpoint + const { exceptions_list } = await getRule(supertest, log, IMMUTABLE_RULE_ID); + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ + rule_id: IMMUTABLE_RULE_ID, + exceptions_list: [ + ...exceptions_list, + { + id, + list_id, + namespace_type, + type, + }, + ], + }) + .expect(200); + + await retry.try(async () => { + const stats = await getSecurityTelemetryStats(supertest, log); + expect(stats.detection_rules).length(1); + const detectionRule = stats.detection_rules[0][0]; + expect(detectionRule['@timestamp']).to.be.a('string'); + expect(detectionRule.cluster_uuid).to.be.a('string'); + expect(detectionRule.cluster_name).to.be.a('string'); + expect(detectionRule.license_id).to.be.a('string'); + expect(detectionRule.detection_rule.created_at).to.be.a('string'); + expect(detectionRule.detection_rule.id).to.be.a('string'); + }); + }); + + it('should give telemetry/stats for an exception list of type "detection"', async () => { + // create an exception list container of type "detection" + const { id, list_id, namespace_type, type } = await createExceptionList(supertest, log, { + description: 'description', + list_id: '123', + name: 'test list', + type: 'detection', + }); + + // add 1 item to the exception list + await createExceptionListItem(supertest, log, { + description: 'endpoint description', + entries: [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'something', + }, + ], + list_id: '123', + name: 'endpoint_list', + os_types: [], + type: 'simple', + }); + + // add the exception list to the pre-built/immutable/elastic rule + const immutableRule = await getRule(supertest, log, IMMUTABLE_RULE_ID); + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ + rule_id: IMMUTABLE_RULE_ID, + exceptions_list: [ + ...immutableRule.exceptions_list, + { + id, + list_id, + namespace_type, + type, + }, + ], + }) + .expect(200); + + await retry.try(async () => { + const stats = await getSecurityTelemetryStats(supertest, log); + const detectionRules = stats.detection_rules + .flat() + .map((obj: { detection_rule: any }) => obj.detection_rule); + + expect(detectionRules).to.eql([ + { + created_at: detectionRules[0].created_at, + entries: [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'something', + }, + ], + id: detectionRules[0].id, + name: 'endpoint description', + os_types: [], + rule_version: detectionRules[0].rule_version, + }, + ]); + }); + }); + + it('should give telemetry/stats for an exception list of type "endpoint"', async () => { + // create an exception list container of type "detection" + const { id, list_id, namespace_type, type } = await createExceptionList(supertest, log, { + description: 'description', + list_id: '123', + name: 'test list', + type: 'endpoint', + }); + + // add 1 item to the exception list + await createExceptionListItem(supertest, log, { + description: 'endpoint description', + entries: [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'something', + }, + ], + list_id: '123', + name: 'endpoint_list', + os_types: [], + type: 'simple', + }); + + // add the exception list to the pre-built/immutable/elastic rule + const immutableRule = await getRule(supertest, log, IMMUTABLE_RULE_ID); + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ + rule_id: IMMUTABLE_RULE_ID, + exceptions_list: [ + ...immutableRule.exceptions_list, + { + id, + list_id, + namespace_type, + type, + }, + ], + }) + .expect(200); + + await retry.try(async () => { + const stats = await getSecurityTelemetryStats(supertest, log); + const detectionRules = stats.detection_rules + .flat() + .map((obj: { detection_rule: any }) => obj.detection_rule); + + expect(detectionRules).to.eql([ + { + created_at: detectionRules[0].created_at, + entries: [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'something', + }, + ], + id: detectionRules[0].id, + name: 'endpoint description', + os_types: [], + rule_version: detectionRules[0].rule_version, + }, + ]); + }); + }); + + it('should give telemetry/stats for an exception list of type "endpoint_trusted_apps"', async () => { + // create an exception list container of type "detection" + const { id, list_id, namespace_type, type } = await createExceptionList(supertest, log, { + description: 'description', + list_id: '123', + name: 'test list', + type: 'endpoint_trusted_apps', + }); + + // add 1 item to the exception list + await createExceptionListItem(supertest, log, { + description: 'endpoint description', + entries: [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'something', + }, + ], + list_id: '123', + name: 'endpoint_list', + os_types: [], + type: 'simple', + }); + + // add the exception list to the pre-built/immutable/elastic rule + const immutableRule = await getRule(supertest, log, IMMUTABLE_RULE_ID); + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ + rule_id: IMMUTABLE_RULE_ID, + exceptions_list: [ + ...immutableRule.exceptions_list, + { + id, + list_id, + namespace_type, + type, + }, + ], + }) + .expect(200); + + await retry.try(async () => { + const stats = await getSecurityTelemetryStats(supertest, log); + const detectionRules = stats.detection_rules + .flat() + .map((obj: { detection_rule: any }) => obj.detection_rule); + + expect(detectionRules).to.eql([ + { + created_at: detectionRules[0].created_at, + entries: [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'something', + }, + ], + id: detectionRules[0].id, + name: 'endpoint description', + os_types: [], + rule_version: detectionRules[0].rule_version, + }, + ]); + }); + }); + + it('should give telemetry/stats for an exception list of type "endpoint_events"', async () => { + // create an exception list container of type "detection" + const { id, list_id, namespace_type, type } = await createExceptionList(supertest, log, { + description: 'description', + list_id: '123', + name: 'test list', + type: 'endpoint_events', + }); + + // add 1 item to the exception list + await createExceptionListItem(supertest, log, { + description: 'endpoint description', + entries: [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'something', + }, + ], + list_id: '123', + name: 'endpoint_list', + os_types: [], + type: 'simple', + }); + + // add the exception list to the pre-built/immutable/elastic rule + const immutableRule = await getRule(supertest, log, IMMUTABLE_RULE_ID); + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ + rule_id: IMMUTABLE_RULE_ID, + exceptions_list: [ + ...immutableRule.exceptions_list, + { + id, + list_id, + namespace_type, + type, + }, + ], + }) + .expect(200); + + await retry.try(async () => { + const stats = await getSecurityTelemetryStats(supertest, log); + const detectionRules = stats.detection_rules + .flat() + .map((obj: { detection_rule: any }) => obj.detection_rule); + + expect(detectionRules).to.eql([ + { + created_at: detectionRules[0].created_at, + entries: [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'something', + }, + ], + id: detectionRules[0].id, + name: 'endpoint description', + os_types: [], + rule_version: detectionRules[0].rule_version, + }, + ]); + }); + }); + + it('should give telemetry/stats for an exception list of type "endpoint_host_isolation_exceptions"', async () => { + // create an exception list container of type "detection" + const { id, list_id, namespace_type, type } = await createExceptionList(supertest, log, { + description: 'description', + list_id: '123', + name: 'test list', + type: 'endpoint_host_isolation_exceptions', + }); + + // add 1 item to the exception list + await createExceptionListItem(supertest, log, { + description: 'endpoint description', + entries: [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'something', + }, + ], + list_id: '123', + name: 'endpoint_list', + os_types: [], + type: 'simple', + }); + + // add the exception list to the pre-built/immutable/elastic rule + const immutableRule = await getRule(supertest, log, IMMUTABLE_RULE_ID); + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ + rule_id: IMMUTABLE_RULE_ID, + exceptions_list: [ + ...immutableRule.exceptions_list, + { + id, + list_id, + namespace_type, + type, + }, + ], + }) + .expect(200); + + await retry.try(async () => { + const stats = await getSecurityTelemetryStats(supertest, log); + const detectionRules = stats.detection_rules + .flat() + .map((obj: { detection_rule: any }) => obj.detection_rule); + + expect(detectionRules).to.eql([ + { + created_at: detectionRules[0].created_at, + entries: [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'something', + }, + ], + id: detectionRules[0].id, + name: 'endpoint description', + os_types: [], + rule_version: detectionRules[0].rule_version, + }, + ]); + }); + }); + }); + + describe('pre-built/immutable/elastic rules should show detection_rules telemetry data for multiple list items and types', () => { + beforeEach(async () => { + // install prepackaged rules to get immutable rules for testing + await installPrePackagedRules(supertest, log); + }); + + it('should give telemetry/stats for 2 exception lists to the type of "detection"', async () => { + // create an exception list container of type "detection" + const { id, list_id, namespace_type, type } = await createExceptionList(supertest, log, { + description: 'description', + list_id: '123', + name: 'test list', + type: 'detection', + }); + + // add 1st item to the exception list + await createExceptionListItem(supertest, log, { + description: 'endpoint description 1', + entries: [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'something 1', + }, + ], + list_id: '123', + name: 'endpoint_list', + os_types: [], + type: 'simple', + }); + + // add 2nd item to the exception list + await createExceptionListItem(supertest, log, { + description: 'endpoint description 2', + entries: [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'something 2', + }, + ], + list_id: '123', + name: 'endpoint_list', + os_types: [], + type: 'simple', + }); + + // add the exception list to the pre-built/immutable/elastic rule + const immutableRule = await getRule(supertest, log, IMMUTABLE_RULE_ID); + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ + rule_id: IMMUTABLE_RULE_ID, + exceptions_list: [ + ...immutableRule.exceptions_list, + { + id, + list_id, + namespace_type, + type, + }, + ], + }) + .expect(200); + + await retry.try(async () => { + const stats = await getSecurityTelemetryStats(supertest, log); + const detectionRules = stats.detection_rules + .flat() + .map((obj: { detection_rule: any }) => obj.detection_rule) + .sort((obj1: { entries: { name: number } }, obj2: { entries: { name: number } }) => { + return obj1.entries.name - obj2.entries.name; + }); + + expect(detectionRules).to.eql([ + { + created_at: detectionRules[0].created_at, + entries: [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'something 2', + }, + ], + id: detectionRules[0].id, + name: 'endpoint description 2', + os_types: [], + rule_version: detectionRules[0].rule_version, + }, + { + created_at: detectionRules[1].created_at, + entries: [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'something 1', + }, + ], + id: detectionRules[1].id, + name: 'endpoint description 1', + os_types: [], + rule_version: detectionRules[1].rule_version, + }, + ]); + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/task_based/security_lists.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/task_based/security_lists.ts new file mode 100644 index 0000000000000..c56936f016b58 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/task_based/security_lists.ts @@ -0,0 +1,451 @@ +/* + * 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 '@kbn/expect'; +import { + ENDPOINT_EVENT_FILTERS_LIST_ID, + ENDPOINT_LIST_ID, + ENDPOINT_TRUSTED_APPS_LIST_ID, +} from '@kbn/securitysolution-list-constants'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSecurityTelemetryStats, + createExceptionListItem, + createExceptionList, +} from '../../../../utils'; +import { deleteAllExceptions } from '../../../../../lists_api_integration/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const log = getService('log'); + const retry = getService('retry'); + + describe('Security lists task telemetry', async () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/security_solution/telemetry'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/security_solution/telemetry'); + }); + + beforeEach(async () => { + await createSignalsIndex(supertest, log); + // Calling stats endpoint once like this guarantees that the trusted applications and exceptions lists are created for us. + await getSecurityTelemetryStats(supertest, log); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest, log); + await deleteAllAlerts(supertest, log); + await deleteAllExceptions(supertest, log); + }); + + describe('Trusted Applications lists', () => { + it('should give telemetry/stats for 1 exception list', async () => { + // add 1 item to the existing trusted application exception list + await createExceptionListItem(supertest, log, { + description: 'endpoint description', + entries: [ + { + field: 'process.hash.md5', + operator: 'included', + type: 'match', + value: 'ae27a4b4821b13cad2a17a75d219853e', + }, + ], + list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, + name: ENDPOINT_TRUSTED_APPS_LIST_ID, + os_types: ['linux'], + type: 'simple', + namespace_type: 'agnostic', + }); + + await retry.try(async () => { + const stats = await getSecurityTelemetryStats(supertest, log); + + const trustedApplication = stats.security_lists + .flat() + .map((obj: { trusted_application: any }) => obj.trusted_application); + expect(trustedApplication).to.eql([ + { + created_at: trustedApplication[0].created_at, + entries: [ + { + field: 'process.hash.md5', + operator: 'included', + type: 'match', + value: 'ae27a4b4821b13cad2a17a75d219853e', + }, + ], + id: trustedApplication[0].id, + name: ENDPOINT_TRUSTED_APPS_LIST_ID, + os_types: ['linux'], + scope: { + type: 'policy', + policies: [], + }, + }, + ]); + }); + }); + + it('should give telemetry/stats for 2 exception list', async () => { + // add 1 item to the existing trusted applications exception list + await createExceptionListItem(supertest, log, { + description: 'endpoint description', + entries: [ + { + field: 'process.hash.md5', + operator: 'included', + type: 'match', + value: 'ae27a4b4821b13cad2a17a75d219853e', + }, + ], + list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, + name: 'something 1', + os_types: ['linux'], + type: 'simple', + namespace_type: 'agnostic', + }); + + // add 2nd item to the existing trusted application exception list + await createExceptionListItem(supertest, log, { + description: 'endpoint description', + entries: [ + { + field: 'process.hash.md5', + operator: 'included', + type: 'match', + value: '437b930db84b8079c2dd804a71936b5f', + }, + ], + list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, + name: 'something 2', + os_types: ['macos'], + type: 'simple', + namespace_type: 'agnostic', + }); + + await retry.try(async () => { + const stats = await getSecurityTelemetryStats(supertest, log); + + const trustedApplication = stats.security_lists + .flat() + .map((obj: { trusted_application: any }) => obj.trusted_application) + .sort((obj1: { entries: { name: number } }, obj2: { entries: { name: number } }) => { + return obj1.entries.name - obj2.entries.name; + }); + + expect(trustedApplication).to.eql([ + { + created_at: trustedApplication[0].created_at, + entries: [ + { + field: 'process.hash.md5', + operator: 'included', + type: 'match', + value: 'ae27a4b4821b13cad2a17a75d219853e', + }, + ], + id: trustedApplication[0].id, + name: 'something 1', + os_types: ['linux'], + scope: { + type: 'policy', + policies: [], + }, + }, + { + created_at: trustedApplication[1].created_at, + entries: [ + { + field: 'process.hash.md5', + operator: 'included', + type: 'match', + value: '437b930db84b8079c2dd804a71936b5f', + }, + ], + id: trustedApplication[1].id, + name: 'something 2', + os_types: ['macos'], + scope: { + type: 'policy', + policies: [], + }, + }, + ]); + }); + }); + }); + + describe('Endpoint Exception lists', () => { + it('should give telemetry/stats for 1 exception list', async () => { + // add 1 item to the existing endpoint exception list + await createExceptionListItem(supertest, log, { + description: 'endpoint description', + entries: [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'something', + }, + ], + list_id: ENDPOINT_LIST_ID, + name: ENDPOINT_LIST_ID, + os_types: [], + type: 'simple', + namespace_type: 'agnostic', + }); + + await retry.try(async () => { + const stats = await getSecurityTelemetryStats(supertest, log); + const securityLists = stats.security_lists + .flat() + .map((obj: { endpoint_exception: any }) => obj.endpoint_exception); + expect(securityLists).to.eql([ + { + created_at: securityLists[0].created_at, + entries: [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'something', + }, + ], + id: securityLists[0].id, + name: ENDPOINT_LIST_ID, + os_types: [], + }, + ]); + }); + }); + + it('should give telemetry/stats for 2 exception lists', async () => { + // add 1st item to the existing endpoint exception list + await createExceptionListItem(supertest, log, { + description: 'endpoint description', + entries: [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'something 1', + }, + ], + list_id: ENDPOINT_LIST_ID, + name: ENDPOINT_LIST_ID, + os_types: [], + type: 'simple', + namespace_type: 'agnostic', + }); + + // add 2nd item to the existing endpoint exception list + await createExceptionListItem(supertest, log, { + description: 'endpoint description', + entries: [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'something 2', + }, + ], + list_id: 'endpoint_list', + name: ENDPOINT_LIST_ID, + os_types: [], + type: 'simple', + namespace_type: 'agnostic', + }); + + await retry.try(async () => { + const stats = await getSecurityTelemetryStats(supertest, log); + const securityLists = stats.security_lists + .flat() + .map((obj: { endpoint_exception: any }) => obj.endpoint_exception) + .sort((obj1: { entries: { name: number } }, obj2: { entries: { name: number } }) => { + return obj1.entries.name - obj2.entries.name; + }); + + expect(securityLists).to.eql([ + { + created_at: securityLists[0].created_at, + entries: [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'something 1', + }, + ], + id: securityLists[0].id, + name: ENDPOINT_LIST_ID, + os_types: [], + }, + { + created_at: securityLists[1].created_at, + entries: [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'something 2', + }, + ], + id: securityLists[1].id, + name: ENDPOINT_LIST_ID, + os_types: [], + }, + ]); + }); + }); + }); + + describe('Endpoint Event Filters Exception lists', () => { + beforeEach(async () => { + // We have to manually create this event filter exception list. It does not look + // like there is an auto-create for it within Kibana. It must exist somewhere else. + await createExceptionList(supertest, log, { + description: 'endpoint description', + list_id: ENDPOINT_EVENT_FILTERS_LIST_ID, + name: ENDPOINT_EVENT_FILTERS_LIST_ID, + type: 'detection', + namespace_type: 'agnostic', + }); + }); + + it('should give telemetry/stats for 1 exception list', async () => { + // add 1 item to the existing endpoint exception list + await createExceptionListItem(supertest, log, { + description: 'endpoint description', + entries: [ + { + field: 'host.name', + operator: 'included', + type: 'match', + value: 'something', + }, + ], + list_id: ENDPOINT_EVENT_FILTERS_LIST_ID, + name: ENDPOINT_EVENT_FILTERS_LIST_ID, + os_types: ['linux'], + type: 'simple', + namespace_type: 'agnostic', + }); + + await retry.try(async () => { + const stats = await getSecurityTelemetryStats(supertest, log); + const endPointEventFilter = stats.security_lists + .flat() + .map((obj: { endpoint_event_filter: any }) => obj.endpoint_event_filter); + expect(endPointEventFilter).to.eql([ + { + created_at: endPointEventFilter[0].created_at, + entries: [ + { + field: 'host.name', + operator: 'included', + type: 'match', + value: 'something', + }, + ], + id: endPointEventFilter[0].id, + name: ENDPOINT_EVENT_FILTERS_LIST_ID, + os_types: ['linux'], + }, + ]); + }); + }); + + it('should give telemetry/stats for 2 exception lists', async () => { + // add 1st item to the existing endpoint exception list + await createExceptionListItem(supertest, log, { + description: 'endpoint description', + entries: [ + { + field: 'host.name', + operator: 'included', + type: 'match', + value: 'something 1', + }, + ], + list_id: ENDPOINT_EVENT_FILTERS_LIST_ID, + name: ENDPOINT_EVENT_FILTERS_LIST_ID, + os_types: ['linux'], + type: 'simple', + namespace_type: 'agnostic', + }); + + // add 2nd item to the existing endpoint exception list + await createExceptionListItem(supertest, log, { + description: 'endpoint description', + entries: [ + { + field: 'host.name', + operator: 'included', + type: 'match', + value: 'something 2', + }, + ], + list_id: ENDPOINT_EVENT_FILTERS_LIST_ID, + name: ENDPOINT_EVENT_FILTERS_LIST_ID, + os_types: ['macos'], + type: 'simple', + namespace_type: 'agnostic', + }); + + await retry.try(async () => { + const stats = await getSecurityTelemetryStats(supertest, log); + const endPointEventFilter = stats.security_lists + .flat() + .map((obj: { endpoint_event_filter: any }) => obj.endpoint_event_filter) + .sort((obj1: { entries: { name: number } }, obj2: { entries: { name: number } }) => { + return obj1.entries.name - obj2.entries.name; + }); + + expect(endPointEventFilter).to.eql([ + { + created_at: endPointEventFilter[0].created_at, + entries: [ + { + field: 'host.name', + operator: 'included', + type: 'match', + value: 'something 1', + }, + ], + id: endPointEventFilter[0].id, + name: ENDPOINT_EVENT_FILTERS_LIST_ID, + os_types: ['linux'], + }, + { + created_at: endPointEventFilter[1].created_at, + entries: [ + { + field: 'host.name', + operator: 'included', + type: 'match', + value: 'something 2', + }, + ], + id: endPointEventFilter[1].id, + name: ENDPOINT_EVENT_FILTERS_LIST_ID, + os_types: ['macos'], + }, + ]); + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/all_types.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/all_types.ts new file mode 100644 index 0000000000000..a8d473597a461 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/all_types.ts @@ -0,0 +1,50 @@ +/* + * 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 '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getStats, +} from '../../../../utils'; +import { getInitialDetectionMetrics } from '../../../../../../plugins/security_solution/server/usage/detections/detection_rule_helpers'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const log = getService('log'); + const retry = getService('retry'); + + describe('Detection rule telemetry', async () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/security_solution/telemetry'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/security_solution/telemetry'); + }); + + beforeEach(async () => { + await createSignalsIndex(supertest, log); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest, log); + await deleteAllAlerts(supertest, log); + }); + + it('should have initialized empty/zero values when no rules are running', async () => { + await retry.try(async () => { + const stats = await getStats(supertest, log); + expect(stats).to.eql(getInitialDetectionMetrics()); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/detection_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/detection_rules.ts similarity index 98% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/detection_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/detection_rules.ts index f4228ed31f279..8a956d456edec 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/detection_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/detection_rules.ts @@ -6,12 +6,12 @@ */ import expect from '@kbn/expect'; -import { DetectionMetrics } from '../../../../../plugins/security_solution/server/usage/detections/types'; +import { DetectionMetrics } from '../../../../../../plugins/security_solution/server/usage/detections/types'; import { ThreatMatchCreateSchema, ThresholdCreateSchema, -} from '../../../../../plugins/security_solution/common/detection_engine/schemas/request'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +} from '../../../../../../plugins/security_solution/common/detection_engine/schemas/request'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { createLegacyRuleAction, createNewAction, @@ -32,8 +32,8 @@ import { waitForRuleSuccessOrStatus, waitForSignalsToBePresent, updateRule, -} from '../../../utils'; -import { getInitialDetectionMetrics } from '../../../../../plugins/security_solution/server/usage/detections/detection_rule_helpers'; +} from '../../../../utils'; +import { getInitialDetectionMetrics } from '../../../../../../plugins/security_solution/server/usage/detections/detection_rule_helpers'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { @@ -60,13 +60,6 @@ export default ({ getService }: FtrProviderContext) => { await deleteAllAlerts(supertest, log); }); - it('should have initialized empty/zero values when no rules are running', async () => { - await retry.try(async () => { - const stats = await getStats(supertest, log); - expect(stats).to.eql(getInitialDetectionMetrics()); - }); - }); - describe('"kql" rule type', () => { it('should show "notifications_enabled", "notifications_disabled" "legacy_notifications_enabled", "legacy_notifications_disabled", all to be "0" for "disabled"/"in-active" rule that does not have any actions', async () => { const rule = getRuleForSignalTesting(['telemetry'], 'rule-1', false); diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index 04bf0eec6f5b8..6825657836184 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -52,6 +52,7 @@ import { DETECTION_ENGINE_SIGNALS_MIGRATION_URL, INTERNAL_IMMUTABLE_KEY, INTERNAL_RULE_ID_KEY, + SECURITY_TELEMETRY_URL, UPDATE_OR_CREATE_LEGACY_ACTIONS, } from '../../plugins/security_solution/common/constants'; import { RACAlert } from '../../plugins/security_solution/server/lib/detection_engine/rule_types/types'; @@ -1848,7 +1849,7 @@ export const getDetectionMetricsFromBody = ( }; /** - * Gets the stats from the stats endpoint + * Gets the stats from the stats endpoint. * @param supertest The supertest agent. * @returns The detection metrics */ @@ -1870,6 +1871,30 @@ export const getStats = async ( return getDetectionMetricsFromBody(response.body); }; +/** + * Gets the stats from the stats endpoint within specifically the security_solutions application. + * This is considered the "batch" telemetry. + * @param supertest The supertest agent. + * @returns The detection metrics + */ +export const getSecurityTelemetryStats = async ( + supertest: SuperTest.SuperTest, + log: ToolingLog +): Promise => { + const response = await supertest + .get(SECURITY_TELEMETRY_URL) + .set('kbn-xsrf', 'true') + .send({ unencrypted: true, refreshCache: true }); + if (response.status !== 200) { + log.error( + `Did not get an expected 200 "ok" when getting the batch stats for security_solutions. CI issues could happen. Suspect this line if you are seeing CI issues. body: ${JSON.stringify( + response.body + )}, status: ${JSON.stringify(response.status)}` + ); + } + return response.body; +}; + /** * This is a typical simple indicator match/threat match for testing that is easy for most basic testing * @param ruleId diff --git a/x-pack/test/lists_api_integration/utils.ts b/x-pack/test/lists_api_integration/utils.ts index 6e0c13b21596d..c0dc1d930a57e 100644 --- a/x-pack/test/lists_api_integration/utils.ts +++ b/x-pack/test/lists_api_integration/utils.ts @@ -14,6 +14,7 @@ import type { ExceptionListSchema, ExceptionListItemSchema, ExceptionList, + NamespaceType, } from '@kbn/securitysolution-io-ts-list-types'; import { EXCEPTION_LIST_URL, @@ -183,32 +184,48 @@ export const binaryToString = (res: any, callback: any): void => { }; /** - * Remove all exceptions + * Remove all exceptions from both the "single" and "agnostic" spaces. * This will retry 50 times before giving up and hopefully still not interfere with other tests * @param supertest The supertest handle */ export const deleteAllExceptions = async ( supertest: SuperTest.SuperTest, log: ToolingLog +): Promise => { + await deleteAllExceptionsByType(supertest, log, 'single'); + await deleteAllExceptionsByType(supertest, log, 'agnostic'); +}; + +/** + * Remove all exceptions by a given type such as "agnostic" or "single". + * This will retry 50 times before giving up and hopefully still not interfere with other tests + * @param supertest The supertest handle + */ +export const deleteAllExceptionsByType = async ( + supertest: SuperTest.SuperTest, + log: ToolingLog, + type: NamespaceType ): Promise => { await countDownTest( async () => { const { body } = await supertest - .get(`${EXCEPTION_LIST_URL}/_find?per_page=9999`) + .get(`${EXCEPTION_LIST_URL}/_find?per_page=9999&namespace_type=${type}`) .set('kbn-xsrf', 'true') .send(); - const ids: string[] = body.data.map((exception: ExceptionList) => exception.id); for await (const id of ids) { - await supertest.delete(`${EXCEPTION_LIST_URL}?id=${id}`).set('kbn-xsrf', 'true').send(); + await supertest + .delete(`${EXCEPTION_LIST_URL}?id=${id}&namespace_type=${type}`) + .set('kbn-xsrf', 'true') + .send(); } const { body: finalCheck } = await supertest - .get(`${EXCEPTION_LIST_URL}/_find`) + .get(`${EXCEPTION_LIST_URL}/_find?namespace_type=${type}`) .set('kbn-xsrf', 'true') .send(); return finalCheck.data.length === 0; }, - 'deleteAllExceptions', + `deleteAllExceptions by type: "${type}"`, log, 50, 1000