From e2250a93e25f04d11344d9dab304fde38c260d49 Mon Sep 17 00:00:00 2001 From: Tudor Golubenco Date: Mon, 17 Aug 2020 13:37:32 +0200 Subject: [PATCH 01/21] Empty recurring task --- .../server/lib/telemetry/sender.ts | 49 +++++++++++++++++++ .../security_solution/server/plugin.ts | 7 +++ 2 files changed, 56 insertions(+) create mode 100644 x-pack/plugins/security_solution/server/lib/telemetry/sender.ts diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts new file mode 100644 index 0000000000000..05a9f80daa9bc --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializerContext, Logger } from '../../../../../../core/server'; + +export interface TelemetryEventsSenderContract { + logger: Logger; +} + +export class TelemetryEventsSender { + private readonly initialCheckDelayMs = 10 * 1000; + private readonly checkIntervalMs = 5 * 1000; + private readonly logger: Logger; + private intervalId?: NodeJS.Timeout; + private isSending = false; + + public start(startContract: TelemetryEventsSenderContract) { + this.logger = startContract.logger.get('telemetry_events'); + + this.logger.debug(`Starting task`); + setTimeout(() => { + this.sendIfDue(); + this.intervalId = setInterval(() => this.sendIfDue(), this.checkIntervalMs); + }, this.initialCheckDelayMs); + } + + public stop() { + if (this.intervalId) { + clearInterval(this.intervalId); + } + } + + private async sendIfDue() { + if (this.isSending) { + return; + } + + try { + this.isSending = true; + this.logger.debug(`Sending...`); + } catch (err) { + this.logger.warn(`Error sending telemetry events data: ${err}`); + } + this.isSending = false; + } +} diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 24cf1f8746d89..224b6b0e05466 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -62,6 +62,7 @@ import { AppRequestContext } from './types'; import { registerTrustedAppsRoutes } from './endpoint/routes/trusted_apps'; import { securitySolutionSearchStrategyProvider } from './search_strategy/security_solution'; import { securitySolutionTimelineSearchStrategyProvider } from './search_strategy/timeline'; +import { TelemetryEventsSender } from './lib/telemetry/sender'; export interface SetupPlugins { alerts: AlertingSetup; @@ -105,6 +106,7 @@ export class Plugin implements IPlugin Date: Tue, 18 Aug 2020 16:13:20 +0200 Subject: [PATCH 02/21] Added processEvents with tests --- .../server/lib/telemetry/sender.test.ts | 74 +++++++++++++++++++ .../server/lib/telemetry/sender.ts | 54 ++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts new file mode 100644 index 0000000000000..21c7b0e97f750 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TelemetryEventsSender } from './sender.ts'; + +describe('TelemetryEventsSender', () => { + describe('processEvents', () => { + it('returns empty array when empty array is passed', async () => { + const sender = new TelemetryEventsSender(); + const result = sender.processEvents([]); + expect(result).toStrictEqual([]); + }); + + it('applies the allowlist', () => { + const sender = new TelemetryEventsSender(); + const input = [ + { + event: { + kind: 'alert', + something_else: 'nope', + }, + agent: { + name: 'test', + }, + file: { + size: 3, + path: 'X', + test: 'me', + another: 'nope', + Ext: { + code_signature: 'X', + malware_classification: 'X', + something_else: 'nope', + }, + }, + host: { + os: { + name: 'windows', + }, + something_else: 'nope', + }, + }, + ]; + + const result = sender.processEvents(input); + expect(result).toStrictEqual([ + { + event: { + kind: 'alert', + }, + agent: { + name: 'test', + }, + file: { + size: 3, + path: 'X', + Ext: { + code_signature: 'X', + malware_classification: 'X', + }, + }, + host: { + os: { + name: 'windows', + }, + }, + }, + ]); + }); + }); +}); 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 05a9f80daa9bc..6e560facb3084 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -4,18 +4,35 @@ * you may not use this file except in compliance with the Elastic License. */ +import { pick } from 'lodash'; + import { PluginInitializerContext, Logger } from '../../../../../../core/server'; export interface TelemetryEventsSenderContract { logger: Logger; } +// Allowlist for the fields that we copy from the original event to the +// telemetry event. +// Top level fields: +const allowlistTop = ['@timestamp', 'agent', 'endpoint', 'ecs', 'elastic']; +// file.* fields: +const allowlistFile = ['path', 'size', 'created', 'accessed', 'mtime', 'directory', 'hash']; +// file.Ext.* fields: +const allowlistFileExt = ['code_signature', 'malware_classification']; +// file.* fields: +const allowlistHost = ['os']; +// event.* fields: +const allowlistEvent = ['kind']; + export class TelemetryEventsSender { private readonly initialCheckDelayMs = 10 * 1000; private readonly checkIntervalMs = 5 * 1000; + private readonly maxQueueSize = 100; private readonly logger: Logger; private intervalId?: NodeJS.Timeout; private isSending = false; + private queue: object[] = []; public start(startContract: TelemetryEventsSenderContract) { this.logger = startContract.logger.get('telemetry_events'); @@ -46,4 +63,41 @@ export class TelemetryEventsSender { } this.isSending = false; } + + public queueTelemetryEvents(events: object[]) { + const qlength = this.queue.length; + + if (qlength > this.maxQueueSize) { + // we're full already + return; + } + + // TODO check that telemetry is opted-in + + if (events.length > this.maxQueueSize - qlength) { + this.queue.push(this.processEvents(events.slice(0, this.maxQueueSize - qlength))); + } else { + this.queue.push(this.processEvents(events)); + } + } + + private processEvents(events: object[]): object[] { + return events.map(function (obj: object): object { + const newObj = pick(obj, allowlistTop); + if ('file' in obj) { + newObj.file = pick(obj.file, allowlistFile); + if ('Ext' in obj.file) { + newObj.file.Ext = pick(obj.file.Ext, allowlistFileExt); + } + } + if ('host' in obj) { + newObj.host = pick(obj.host, allowlistHost); + } + if ('event' in obj) { + newObj.event = pick(obj.event, allowlistEvent); + } + + return newObj; + }); + } } From 63358572c88b1474d269e40c80bd69ab61c4ad55 Mon Sep 17 00:00:00 2001 From: Tudor Golubenco Date: Wed, 19 Aug 2020 12:27:37 +0200 Subject: [PATCH 03/21] SendIfDue + tests --- .../server/lib/telemetry/sender.test.ts | 39 ++++++++++++++++- .../server/lib/telemetry/sender.ts | 42 +++++++++++-------- 2 files changed, 62 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts index 21c7b0e97f750..1b495833a6618 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts @@ -5,10 +5,11 @@ */ import { TelemetryEventsSender } from './sender.ts'; +import { loggingSystemMock } from 'src/core/server/mocks'; describe('TelemetryEventsSender', () => { describe('processEvents', () => { - it('returns empty array when empty array is passed', async () => { + it('returns empty array when empty array is passed', () => { const sender = new TelemetryEventsSender(); const result = sender.processEvents([]); expect(result).toStrictEqual([]); @@ -71,4 +72,40 @@ describe('TelemetryEventsSender', () => { ]); }); }); + + describe('queueTelemetryEvents', () => { + it('queues two events', () => { + const sender = new TelemetryEventsSender(); + sender.queueTelemetryEvents([{ 'event.kind': '1' }, { 'event.kind': '2' }]); + expect(sender.queue.length).toBe(2); + }); + + it('queues more than maxQueueSize events', () => { + const sender = new TelemetryEventsSender(); + sender.maxQueueSize = 5; + sender.queueTelemetryEvents([{ 'event.kind': '1' }, { 'event.kind': '2' }]); + sender.queueTelemetryEvents([{ 'event.kind': '3' }, { 'event.kind': '4' }]); + sender.queueTelemetryEvents([{ 'event.kind': '5' }, { 'event.kind': '6' }]); + sender.queueTelemetryEvents([{ 'event.kind': '7' }, { 'event.kind': '8' }]); + expect(sender.queue.length).toBe(5); + }); + + it('empties the queue when sending', async () => { + const sender = new TelemetryEventsSender(); + sender.logger = loggingSystemMock.create().get(); + sender.sendEvents = jest.fn(); + + sender.queueTelemetryEvents([{ 'event.kind': '1' }, { 'event.kind': '2' }]); + expect(sender.queue.length).toBe(2); + await sender.sendIfDue(); + expect(sender.queue.length).toBe(0); + expect(sender.sendEvents).toBeCalledTimes(1); + sender.queueTelemetryEvents([{ 'event.kind': '3' }, { 'event.kind': '4' }]); + sender.queueTelemetryEvents([{ 'event.kind': '5' }, { 'event.kind': '6' }]); + expect(sender.queue.length).toBe(4); + await sender.sendIfDue(); + expect(sender.queue.length).toBe(0); + expect(sender.sendEvents).toBeCalledTimes(2); + }); + }); }); 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 6e560facb3084..f6b788c7c4aaa 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { pick } from 'lodash'; +import { pick, cloneDeep } from 'lodash'; import { PluginInitializerContext, Logger } from '../../../../../../core/server'; @@ -50,24 +50,10 @@ export class TelemetryEventsSender { } } - private async sendIfDue() { - if (this.isSending) { - return; - } - - try { - this.isSending = true; - this.logger.debug(`Sending...`); - } catch (err) { - this.logger.warn(`Error sending telemetry events data: ${err}`); - } - this.isSending = false; - } - public queueTelemetryEvents(events: object[]) { const qlength = this.queue.length; - if (qlength > this.maxQueueSize) { + if (qlength >= this.maxQueueSize) { // we're full already return; } @@ -75,9 +61,9 @@ export class TelemetryEventsSender { // TODO check that telemetry is opted-in if (events.length > this.maxQueueSize - qlength) { - this.queue.push(this.processEvents(events.slice(0, this.maxQueueSize - qlength))); + this.queue.push(...this.processEvents(events.slice(0, this.maxQueueSize - qlength))); } else { - this.queue.push(this.processEvents(events)); + this.queue.push(...this.processEvents(events)); } } @@ -100,4 +86,24 @@ export class TelemetryEventsSender { return newObj; }); } + + private async sendIfDue() { + if (this.isSending) { + return; + } + + try { + this.isSending = true; + const toSend: object[] = cloneDeep(this.queue); + this.queue = []; + this.sendEvents(toSend); + } catch (err) { + this.logger.warn(`Error sending telemetry events data: ${err}`); + } + this.isSending = false; + } + + private async sendEvents(events: object[]) { + // TODO + } } From c302fdd4a25099eb5bc8f1f4aa18f808b78c6994 Mon Sep 17 00:00:00 2001 From: Tudor Golubenco Date: Mon, 7 Sep 2020 21:55:11 +0200 Subject: [PATCH 04/21] Connect telemetry in the detection engine --- .../signals/search_after_bulk_create.ts | 12 ++++++ .../signals/send_telemetry_events.test.ts | 42 +++++++++++++++++++ .../signals/send_telemetry_events.ts | 40 ++++++++++++++++++ .../signals/signal_rule_alert_type.ts | 4 ++ .../server/lib/telemetry/sender.ts | 15 ++++++- .../security_solution/server/plugin.ts | 1 + 6 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.ts diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index e90e5996877f8..85fbfd2fbad0f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -19,6 +19,8 @@ import { SignalSearchResponse } from './types'; import { filterEventsAgainstList } from './filter_events_with_list'; import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { getSignalTimeTuples } from './utils'; +import { TelemetryEventsSender } from './lib/telemetry/sender'; +import { sendAlertTelemetryEvents } from './send_telemetry_events'; interface SearchAfterAndBulkCreateParams { gap: moment.Duration | null; @@ -28,6 +30,7 @@ interface SearchAfterAndBulkCreateParams { listClient: ListClient | undefined; // TODO: undefined is for temporary development, remove before merged exceptionsList: ExceptionListItemSchema[]; logger: Logger; + eventsTelemetry: TelemetryEventsSender; id: string; inputIndexPattern: string[]; signalsIndex: string; @@ -64,6 +67,7 @@ export const searchAfterAndBulkCreate = async ({ services, listClient, logger, + eventsTelemetry, id, inputIndexPattern, signalsIndex, @@ -231,6 +235,14 @@ export const searchAfterAndBulkCreate = async ({ toReturn.bulkCreateTimes.push(bulkDuration); } + sendAlertTelemetryEvents( + logger, + eventsTelemetry, + filteredEvents, + ruleParams, + buildRuleMessage + ); + logger.debug( buildRuleMessage(`filteredEvents.hits.hits: ${filteredEvents.hits.hits.length}`) ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.test.ts new file mode 100644 index 0000000000000..206d1579303bf --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { selectEvents } from './send_telemetry_events'; +import { loggingSystemMock } from 'src/core/server/mocks'; + +describe('sendAlertTelemetry', () => { + it('selectEvents', () => { + const filteredEvents = { + hits: { + hits: [ + { + _source: { + key1: 'hello', + }, + }, + { + key2: 'hello', + }, + { + _source: { + key3: 'hello', + }, + }, + ], + }, + }; + + const sources = selectEvents(filteredEvents); + expect(sources).toStrictEqual([ + { + key1: 'hello', + }, + { + key3: 'hello', + }, + ]); + }); +}); 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 new file mode 100644 index 0000000000000..b4a3a91123722 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TelemetryEventsSender } from './lib/telemetry/sender'; +import { RuleTypeParams } from '../types'; +import { BuildRuleMessage } from './rule_messages'; + +export function selectEvents(filteredEvents: object[]): object[] { + const sources = filteredEvents.hits.hits.map(function (obj: object): object { + if (!('_source' in obj)) { + return undefined; + } + // TODO: filter out non-endpoint alerts + + return obj._source; + }); + + return sources.filter(function (obj) { + return obj !== undefined; + }); +} + +export function sendAlertTelemetryEvents( + logger: Logger, + eventsTelemetry: TelemetryEventsSender, + filteredEvents: object[], + ruleParams: RuleTypeParams, + buildRuleMessage: BuildRuleMessage +) { + const sources = selectEvents(filteredEvents); + + try { + eventsTelemetry.queueTelemetryEvents(sources); + } catch (exc) { + logger.error(buildRuleMessage(`[-] queing telemetry events failed ${exc}`)); + } +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index da17d4a1f123a..40f29988c510b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -44,14 +44,17 @@ import { ruleStatusServiceFactory } from './rule_status_service'; import { buildRuleMessageFactory } from './rule_messages'; import { ruleStatusSavedObjectsClientFactory } from './rule_status_saved_objects_client'; import { getNotificationResultsLink } from '../notifications/utils'; +import { TelemetryEventsSender } from './lib/telemetry/sender'; export const signalRulesAlertType = ({ logger, + eventsTelemetry, version, ml, lists, }: { logger: Logger; + eventsTelemetry: TelemetryEventsSender; version: string; ml: SetupPlugins['ml']; lists: SetupPlugins['lists'] | undefined; @@ -320,6 +323,7 @@ export const signalRulesAlertType = ({ ruleParams: params, services, logger, + eventsTelemetry, id: alertId, inputIndexPattern: inputIndex, signalsIndex: outputIndex, 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 f6b788c7c4aaa..4801297f95b2a 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -15,7 +15,7 @@ export interface TelemetryEventsSenderContract { // Allowlist for the fields that we copy from the original event to the // telemetry event. // Top level fields: -const allowlistTop = ['@timestamp', 'agent', 'endpoint', 'ecs', 'elastic']; +const allowlistTop = ['@timestamp', 'agent', 'Endpoint', 'ecs', 'elastic']; // file.* fields: const allowlistFile = ['path', 'size', 'created', 'accessed', 'mtime', 'directory', 'hash']; // file.Ext.* fields: @@ -53,6 +53,12 @@ export class TelemetryEventsSender { public queueTelemetryEvents(events: object[]) { const qlength = this.queue.length; + if (events.length === 0) { + return; + } + + this.logger.debug(`Queue events`); + if (qlength >= this.maxQueueSize) { // we're full already return; @@ -68,6 +74,7 @@ export class TelemetryEventsSender { } private processEvents(events: object[]): object[] { + this.logger.debug(`Before processing events: ${JSON.stringify(events, null, 2)}`); return events.map(function (obj: object): object { const newObj = pick(obj, allowlistTop); if ('file' in obj) { @@ -88,10 +95,15 @@ export class TelemetryEventsSender { } private async sendIfDue() { + // this.logger.debug(`Send if due`); if (this.isSending) { return; } + if (this.queue.length === 0) { + return; + } + try { this.isSending = true; const toSend: object[] = cloneDeep(this.queue); @@ -105,5 +117,6 @@ export class TelemetryEventsSender { private async sendEvents(events: object[]) { // TODO + this.logger.debug(`Sending events: ${JSON.stringify(events, null, 2)}`); } } diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 224b6b0e05466..5a27ca9747408 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -240,6 +240,7 @@ export class Plugin implements IPlugin Date: Thu, 10 Sep 2020 14:43:12 +0200 Subject: [PATCH 05/21] Respect opt-in status --- x-pack/plugins/security_solution/kibana.json | 3 ++- .../server/lib/telemetry/sender.ts | 27 ++++++++++++++++--- .../security_solution/server/plugin.ts | 10 +++++++ 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json index 7b5c3b5337c02..7bd76838c7559 100644 --- a/x-pack/plugins/security_solution/kibana.json +++ b/x-pack/plugins/security_solution/kibana.json @@ -27,7 +27,8 @@ "spaces", "usageCollection", "lists", - "home" + "home", + "telemetry" ], "server": true, "ui": true, 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 4801297f95b2a..63633b050af21 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -7,9 +7,15 @@ import { pick, cloneDeep } from 'lodash'; import { PluginInitializerContext, Logger } from '../../../../../../core/server'; +import { + TelemetryPluginsStart, + TelemetryPluginsSetup, +} from '../../../../../../src/plugins/telemetry/server'; export interface TelemetryEventsSenderContract { logger: Logger; + telemetryStart: TelemetryPluginsStart; + telemetrySetup: TelemetryPluginsSetup; } // Allowlist for the fields that we copy from the original event to the @@ -30,12 +36,17 @@ export class TelemetryEventsSender { private readonly checkIntervalMs = 5 * 1000; private readonly maxQueueSize = 100; private readonly logger: Logger; + private readonly telemetryStart: TelemetryPluginsStart; + private readonly telemetrySetup: TelemetryPluginsSetup; private intervalId?: NodeJS.Timeout; private isSending = false; private queue: object[] = []; + private isOptedIn = true; // Assume true until the first check public start(startContract: TelemetryEventsSenderContract) { this.logger = startContract.logger.get('telemetry_events'); + this.telemetrySetup = startContract.telemetrySetup; + this.telemetryStart = startContract.telemetryStart; this.logger.debug(`Starting task`); setTimeout(() => { @@ -64,8 +75,6 @@ export class TelemetryEventsSender { return; } - // TODO check that telemetry is opted-in - if (events.length > this.maxQueueSize - qlength) { this.queue.push(...this.processEvents(events.slice(0, this.maxQueueSize - qlength))); } else { @@ -74,7 +83,6 @@ export class TelemetryEventsSender { } private processEvents(events: object[]): object[] { - this.logger.debug(`Before processing events: ${JSON.stringify(events, null, 2)}`); return events.map(function (obj: object): object { const newObj = pick(obj, allowlistTop); if ('file' in obj) { @@ -104,6 +112,16 @@ export class TelemetryEventsSender { return; } + this.isOptedIn = await this.telemetryStart?.getIsOptedIn(); + if (!this.isOptedIn) { + this.logger.debug(`Telemetry is not opted-in.`); + this.queue = []; + return; + } + + const telemetryUrl = await this.telemetrySetup?.getTelemetryUrl(); + this.logger.debug(`Telemetry URL: ${telemetryUrl}`); + try { this.isSending = true; const toSend: object[] = cloneDeep(this.queue); @@ -117,6 +135,7 @@ export class TelemetryEventsSender { private async sendEvents(events: object[]) { // TODO - this.logger.debug(`Sending events: ${JSON.stringify(events, null, 2)}`); + this.logger.debug(`Events sent!`); + // this.logger.debug(`Sending events: ${JSON.stringify(events, null, 2)}`); } } diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 5a27ca9747408..42ab8babcf4e1 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -63,6 +63,10 @@ import { registerTrustedAppsRoutes } from './endpoint/routes/trusted_apps'; import { securitySolutionSearchStrategyProvider } from './search_strategy/security_solution'; import { securitySolutionTimelineSearchStrategyProvider } from './search_strategy/timeline'; import { TelemetryEventsSender } from './lib/telemetry/sender'; +import { + TelemetryPluginsStart, + TelemetryPluginsSetup, +} from '../../../../src/plugins/telemetry/server'; export interface SetupPlugins { alerts: AlertingSetup; @@ -76,12 +80,14 @@ export interface SetupPlugins { spaces?: SpacesSetup; taskManager?: TaskManagerSetupContract; usageCollection?: UsageCollectionSetup; + telemetry?: TelemetryPluginsSetup; } export interface StartPlugins { data: DataPluginStart; ingestManager?: IngestManagerStartContract; taskManager?: TaskManagerStartContract; + telemetry?: TelemetryPluginsStart; } // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -107,6 +113,7 @@ export class Plugin implements IPlugin Date: Thu, 10 Sep 2020 17:39:58 +0200 Subject: [PATCH 06/21] test fixes + test for telemetry disabled --- .../server/lib/telemetry/sender.test.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts index 1b495833a6618..3223d97328bb0 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts @@ -76,12 +76,14 @@ describe('TelemetryEventsSender', () => { describe('queueTelemetryEvents', () => { it('queues two events', () => { const sender = new TelemetryEventsSender(); + sender.logger = loggingSystemMock.create().get(); sender.queueTelemetryEvents([{ 'event.kind': '1' }, { 'event.kind': '2' }]); expect(sender.queue.length).toBe(2); }); it('queues more than maxQueueSize events', () => { const sender = new TelemetryEventsSender(); + sender.logger = loggingSystemMock.create().get(); sender.maxQueueSize = 5; sender.queueTelemetryEvents([{ 'event.kind': '1' }, { 'event.kind': '2' }]); sender.queueTelemetryEvents([{ 'event.kind': '3' }, { 'event.kind': '4' }]); @@ -94,6 +96,10 @@ describe('TelemetryEventsSender', () => { const sender = new TelemetryEventsSender(); sender.logger = loggingSystemMock.create().get(); sender.sendEvents = jest.fn(); + const telemetryStart = { + getIsOptedIn: jest.fn(() => true), + }; + sender.telemetryStart = telemetryStart; sender.queueTelemetryEvents([{ 'event.kind': '1' }, { 'event.kind': '2' }]); expect(sender.queue.length).toBe(2); @@ -107,5 +113,22 @@ describe('TelemetryEventsSender', () => { expect(sender.queue.length).toBe(0); expect(sender.sendEvents).toBeCalledTimes(2); }); + + it("shouldn't send when telemetry is disabled", async () => { + const sender = new TelemetryEventsSender(); + sender.logger = loggingSystemMock.create().get(); + sender.sendEvents = jest.fn(); + const telemetryStart = { + getIsOptedIn: jest.fn(() => false), + }; + sender.telemetryStart = telemetryStart; + + sender.queueTelemetryEvents([{ 'event.kind': '1' }, { 'event.kind': '2' }]); + expect(sender.queue.length).toBe(2); + await sender.sendIfDue(); + + expect(sender.queue.length).toBe(0); + expect(sender.sendEvents).toBeCalledTimes(0); + }); }); }); From 483e3cf0b7a5e7c9d4cec8125e3aec1f12c4a155 Mon Sep 17 00:00:00 2001 From: Tudor Golubenco Date: Fri, 18 Sep 2020 12:52:50 +0200 Subject: [PATCH 07/21] Various type fixes --- .../signals/search_after_bulk_create.test.ts | 10 +++ .../signals/search_after_bulk_create.ts | 20 +++-- .../signals/send_telemetry_events.test.ts | 36 +++++++- .../signals/send_telemetry_events.ts | 22 +++-- .../signals/signal_rule_alert_type.test.ts | 2 + .../signals/signal_rule_alert_type.ts | 4 +- .../server/lib/telemetry/sender.test.ts | 85 +++++++++++-------- .../server/lib/telemetry/sender.ts | 72 ++++++++++------ .../security_solution/server/plugin.ts | 21 ++--- 9 files changed, 178 insertions(+), 94 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index 58dcd7f6bd1c1..dac3c5ddf3844 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -129,6 +129,7 @@ describe('searchAfterAndBulkCreate', () => { exceptionsList: [exceptionItem], services: mockService, logger: mockLogger, + eventsTelemetry: undefined, id: sampleRuleGuid, inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, @@ -254,6 +255,7 @@ describe('searchAfterAndBulkCreate', () => { exceptionsList: [exceptionItem], services: mockService, logger: mockLogger, + eventsTelemetry: undefined, id: sampleRuleGuid, inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, @@ -333,6 +335,7 @@ describe('searchAfterAndBulkCreate', () => { exceptionsList: [exceptionItem], services: mockService, logger: mockLogger, + eventsTelemetry: undefined, id: sampleRuleGuid, inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, @@ -393,6 +396,7 @@ describe('searchAfterAndBulkCreate', () => { exceptionsList: [exceptionItem], services: mockService, logger: mockLogger, + eventsTelemetry: undefined, id: sampleRuleGuid, inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, @@ -451,6 +455,7 @@ describe('searchAfterAndBulkCreate', () => { exceptionsList: [exceptionItem], services: mockService, logger: mockLogger, + eventsTelemetry: undefined, id: sampleRuleGuid, inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, @@ -534,6 +539,7 @@ describe('searchAfterAndBulkCreate', () => { exceptionsList: [exceptionItem], services: mockService, logger: mockLogger, + eventsTelemetry: undefined, id: sampleRuleGuid, inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, @@ -614,6 +620,7 @@ describe('searchAfterAndBulkCreate', () => { exceptionsList: [], services: mockService, logger: mockLogger, + eventsTelemetry: undefined, id: sampleRuleGuid, inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, @@ -663,6 +670,7 @@ describe('searchAfterAndBulkCreate', () => { ruleParams: sampleParams, services: mockService, logger: mockLogger, + eventsTelemetry: undefined, id: sampleRuleGuid, inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, @@ -718,6 +726,7 @@ describe('searchAfterAndBulkCreate', () => { ruleParams: sampleParams, services: mockService, logger: mockLogger, + eventsTelemetry: undefined, id: sampleRuleGuid, inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, @@ -789,6 +798,7 @@ describe('searchAfterAndBulkCreate', () => { ruleParams: sampleParams, services: mockService, logger: mockLogger, + eventsTelemetry: undefined, id: sampleRuleGuid, inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index 85fbfd2fbad0f..5b68e3bf7402d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -19,7 +19,7 @@ import { SignalSearchResponse } from './types'; import { filterEventsAgainstList } from './filter_events_with_list'; import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { getSignalTimeTuples } from './utils'; -import { TelemetryEventsSender } from './lib/telemetry/sender'; +import { TelemetryEventsSender } from '../../telemetry/sender'; import { sendAlertTelemetryEvents } from './send_telemetry_events'; interface SearchAfterAndBulkCreateParams { @@ -30,7 +30,7 @@ interface SearchAfterAndBulkCreateParams { listClient: ListClient | undefined; // TODO: undefined is for temporary development, remove before merged exceptionsList: ExceptionListItemSchema[]; logger: Logger; - eventsTelemetry: TelemetryEventsSender; + eventsTelemetry: TelemetryEventsSender | undefined; id: string; inputIndexPattern: string[]; signalsIndex: string; @@ -235,13 +235,15 @@ export const searchAfterAndBulkCreate = async ({ toReturn.bulkCreateTimes.push(bulkDuration); } - sendAlertTelemetryEvents( - logger, - eventsTelemetry, - filteredEvents, - ruleParams, - buildRuleMessage - ); + if (eventsTelemetry !== undefined) { + sendAlertTelemetryEvents( + logger, + eventsTelemetry, + filteredEvents, + ruleParams, + buildRuleMessage + ); + } logger.debug( buildRuleMessage(`filteredEvents.hits.hits: ${filteredEvents.hits.hits.length}`) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.test.ts index 206d1579303bf..bff1be66db39b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.test.ts @@ -5,23 +5,49 @@ */ import { selectEvents } from './send_telemetry_events'; -import { loggingSystemMock } from 'src/core/server/mocks'; describe('sendAlertTelemetry', () => { it('selectEvents', () => { const filteredEvents = { + took: 0, + timed_out: false, + _shards: { + total: 1, + successful: 1, + failed: 0, + skipped: 0, + }, hits: { + total: 2, + max_score: 0, hits: [ { + _index: 'x', + _type: 'x', + _id: 'x', + _score: 0, _source: { + '@timestamp': 'x', key1: 'hello', }, }, { - key2: 'hello', + _index: 'x', + _type: 'x', + _id: 'x', + _score: 0, + _source: { + '@timestamp': 'x', + key2: 'hello', + }, }, { + _index: 'x', + _type: 'x', + _id: 'x', + _score: 0, _source: { + '@timestamp': 'x', key3: 'hello', }, }, @@ -32,9 +58,15 @@ describe('sendAlertTelemetry', () => { const sources = selectEvents(filteredEvents); expect(sources).toStrictEqual([ { + '@timestamp': 'x', key1: 'hello', }, { + '@timestamp': 'x', + key2: 'hello', + }, + { + '@timestamp': 'x', key3: 'hello', }, ]); 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 b4a3a91123722..b1c621172174e 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 @@ -4,21 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TelemetryEventsSender } from './lib/telemetry/sender'; +import { TelemetryEventsSender, TelemetryEvent } from '../../telemetry/sender'; import { RuleTypeParams } from '../types'; import { BuildRuleMessage } from './rule_messages'; +import { SignalSearchResponse, SignalSource } from './types'; +import { Logger } from '../../../../../../../src/core/server'; -export function selectEvents(filteredEvents: object[]): object[] { - const sources = filteredEvents.hits.hits.map(function (obj: object): object { - if (!('_source' in obj)) { - return undefined; - } - // TODO: filter out non-endpoint alerts +export interface SearchResultWithSource { + _source: SignalSource; +} +export function selectEvents(filteredEvents: SignalSearchResponse): TelemetryEvent[] { + const sources = filteredEvents.hits.hits.map(function ( + obj: SearchResultWithSource + ): TelemetryEvent { return obj._source; }); - return sources.filter(function (obj) { + return sources.filter(function (obj: TelemetryEvent) { + // TODO: filter out non-endpoint alerts return obj !== undefined; }); } @@ -26,7 +30,7 @@ export function selectEvents(filteredEvents: object[]): object[] { export function sendAlertTelemetryEvents( logger: Logger, eventsTelemetry: TelemetryEventsSender, - filteredEvents: object[], + filteredEvents: SignalSearchResponse, ruleParams: RuleTypeParams, buildRuleMessage: BuildRuleMessage ) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index b8311182f3ca8..42cb05e4173fd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -129,6 +129,7 @@ describe('rules_notification_alert_type', () => { alert = signalRulesAlertType({ logger, + eventsTelemetry: undefined, version, ml: mlMock, lists: listMock.createSetup(), @@ -335,6 +336,7 @@ describe('rules_notification_alert_type', () => { payload = getPayload(ruleAlert, alertServices) as jest.Mocked; alert = signalRulesAlertType({ logger, + eventsTelemetry: undefined, version, ml: undefined, lists: undefined, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 40f29988c510b..d93c4dba00549 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -44,7 +44,7 @@ import { ruleStatusServiceFactory } from './rule_status_service'; import { buildRuleMessageFactory } from './rule_messages'; import { ruleStatusSavedObjectsClientFactory } from './rule_status_saved_objects_client'; import { getNotificationResultsLink } from '../notifications/utils'; -import { TelemetryEventsSender } from './lib/telemetry/sender'; +import { TelemetryEventsSender } from '../../telemetry/sender'; export const signalRulesAlertType = ({ logger, @@ -54,7 +54,7 @@ export const signalRulesAlertType = ({ lists, }: { logger: Logger; - eventsTelemetry: TelemetryEventsSender; + eventsTelemetry: TelemetryEventsSender | undefined; version: string; ml: SetupPlugins['ml']; lists: SetupPlugins['lists'] | undefined; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts index 3223d97328bb0..aa519ba82d48b 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts @@ -4,19 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TelemetryEventsSender } from './sender.ts'; +/* eslint-disable dot-notation */ +import { TelemetryEventsSender } from './sender'; import { loggingSystemMock } from 'src/core/server/mocks'; describe('TelemetryEventsSender', () => { + let logger: ReturnType; + + beforeEach(() => { + logger = loggingSystemMock.createLogger(); + }); + describe('processEvents', () => { it('returns empty array when empty array is passed', () => { - const sender = new TelemetryEventsSender(); + const sender = new TelemetryEventsSender(logger); const result = sender.processEvents([]); expect(result).toStrictEqual([]); }); it('applies the allowlist', () => { - const sender = new TelemetryEventsSender(); + const sender = new TelemetryEventsSender(logger); const input = [ { event: { @@ -32,8 +39,13 @@ describe('TelemetryEventsSender', () => { test: 'me', another: 'nope', Ext: { - code_signature: 'X', - malware_classification: 'X', + code_signature: { + key1: 'X', + key2: 'Y', + }, + malware_classification: { + key1: 'X', + }, something_else: 'nope', }, }, @@ -59,8 +71,13 @@ describe('TelemetryEventsSender', () => { size: 3, path: 'X', Ext: { - code_signature: 'X', - malware_classification: 'X', + code_signature: { + key1: 'X', + key2: 'Y', + }, + malware_classification: { + key1: 'X', + }, }, }, host: { @@ -75,60 +92,56 @@ describe('TelemetryEventsSender', () => { describe('queueTelemetryEvents', () => { it('queues two events', () => { - const sender = new TelemetryEventsSender(); - sender.logger = loggingSystemMock.create().get(); + const sender = new TelemetryEventsSender(logger); sender.queueTelemetryEvents([{ 'event.kind': '1' }, { 'event.kind': '2' }]); - expect(sender.queue.length).toBe(2); + expect(sender['queue'].length).toBe(2); }); it('queues more than maxQueueSize events', () => { - const sender = new TelemetryEventsSender(); - sender.logger = loggingSystemMock.create().get(); - sender.maxQueueSize = 5; + const sender = new TelemetryEventsSender(logger); + sender['maxQueueSize'] = 5; sender.queueTelemetryEvents([{ 'event.kind': '1' }, { 'event.kind': '2' }]); sender.queueTelemetryEvents([{ 'event.kind': '3' }, { 'event.kind': '4' }]); sender.queueTelemetryEvents([{ 'event.kind': '5' }, { 'event.kind': '6' }]); sender.queueTelemetryEvents([{ 'event.kind': '7' }, { 'event.kind': '8' }]); - expect(sender.queue.length).toBe(5); + expect(sender['queue'].length).toBe(5); }); it('empties the queue when sending', async () => { - const sender = new TelemetryEventsSender(); - sender.logger = loggingSystemMock.create().get(); - sender.sendEvents = jest.fn(); + const sender = new TelemetryEventsSender(logger); + sender['sendEvents'] = jest.fn(); const telemetryStart = { - getIsOptedIn: jest.fn(() => true), + getIsOptedIn: jest.fn(async () => true), }; - sender.telemetryStart = telemetryStart; + sender['telemetryStart'] = telemetryStart; sender.queueTelemetryEvents([{ 'event.kind': '1' }, { 'event.kind': '2' }]); - expect(sender.queue.length).toBe(2); - await sender.sendIfDue(); - expect(sender.queue.length).toBe(0); - expect(sender.sendEvents).toBeCalledTimes(1); + expect(sender['queue'].length).toBe(2); + await sender['sendIfDue'](); + expect(sender['queue'].length).toBe(0); + expect(sender['sendEvents']).toBeCalledTimes(1); sender.queueTelemetryEvents([{ 'event.kind': '3' }, { 'event.kind': '4' }]); sender.queueTelemetryEvents([{ 'event.kind': '5' }, { 'event.kind': '6' }]); - expect(sender.queue.length).toBe(4); - await sender.sendIfDue(); - expect(sender.queue.length).toBe(0); - expect(sender.sendEvents).toBeCalledTimes(2); + expect(sender['queue'].length).toBe(4); + await sender['sendIfDue'](); + expect(sender['queue'].length).toBe(0); + expect(sender['sendEvents']).toBeCalledTimes(2); }); it("shouldn't send when telemetry is disabled", async () => { - const sender = new TelemetryEventsSender(); - sender.logger = loggingSystemMock.create().get(); - sender.sendEvents = jest.fn(); + const sender = new TelemetryEventsSender(logger); + sender['sendEvents'] = jest.fn(); const telemetryStart = { - getIsOptedIn: jest.fn(() => false), + getIsOptedIn: jest.fn(async () => false), }; - sender.telemetryStart = telemetryStart; + sender['telemetryStart'] = telemetryStart; sender.queueTelemetryEvents([{ 'event.kind': '1' }, { 'event.kind': '2' }]); - expect(sender.queue.length).toBe(2); - await sender.sendIfDue(); + expect(sender['queue'].length).toBe(2); + await sender['sendIfDue'](); - expect(sender.queue.length).toBe(0); - expect(sender.sendEvents).toBeCalledTimes(0); + expect(sender['queue'].length).toBe(0); + expect(sender['sendEvents']).toBeCalledTimes(0); }); }); }); 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 63633b050af21..ac997296b5e1f 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,12 @@ import { pick, cloneDeep } from 'lodash'; -import { PluginInitializerContext, Logger } from '../../../../../../core/server'; +import { Logger } from 'src/core/server'; import { - TelemetryPluginsStart, - TelemetryPluginsSetup, + TelemetryPluginStart, + TelemetryPluginSetup, } from '../../../../../../src/plugins/telemetry/server'; -export interface TelemetryEventsSenderContract { - logger: Logger; - telemetryStart: TelemetryPluginsStart; - telemetrySetup: TelemetryPluginsSetup; -} - // Allowlist for the fields that we copy from the original event to the // telemetry event. // Top level fields: @@ -31,22 +25,50 @@ const allowlistHost = ['os']; // event.* fields: const allowlistEvent = ['kind']; +export type SearchTypes = + | string + | string[] + | number + | number[] + | boolean + | boolean[] + | object + | object[] + | undefined; + +export interface TelemetryEvent { + [key: string]: SearchTypes; + '@timestamp'?: string; + file?: { + [key: string]: SearchTypes; + Ext?: { + [key: string]: SearchTypes; + }; + }; +} + export class TelemetryEventsSender { private readonly initialCheckDelayMs = 10 * 1000; - private readonly checkIntervalMs = 5 * 1000; - private readonly maxQueueSize = 100; + private readonly checkIntervalMs = 5 * 1000; // TODO: change to 60s before merging private readonly logger: Logger; - private readonly telemetryStart: TelemetryPluginsStart; - private readonly telemetrySetup: TelemetryPluginsSetup; + private maxQueueSize = 100; + private telemetryStart?: TelemetryPluginStart; + private telemetrySetup?: TelemetryPluginSetup; private intervalId?: NodeJS.Timeout; private isSending = false; - private queue: object[] = []; - private isOptedIn = true; // Assume true until the first check + private queue: TelemetryEvent[] = []; + private isOptedIn?: boolean = true; // Assume true until the first check + + constructor(logger: Logger) { + this.logger = logger.get('telemetry_events'); + } + + public setup(telemetrySetup?: TelemetryPluginSetup) { + this.telemetrySetup = telemetrySetup; + } - public start(startContract: TelemetryEventsSenderContract) { - this.logger = startContract.logger.get('telemetry_events'); - this.telemetrySetup = startContract.telemetrySetup; - this.telemetryStart = startContract.telemetryStart; + public start(telemetryStart?: TelemetryPluginStart) { + this.telemetryStart = telemetryStart; this.logger.debug(`Starting task`); setTimeout(() => { @@ -61,7 +83,7 @@ export class TelemetryEventsSender { } } - public queueTelemetryEvents(events: object[]) { + public queueTelemetryEvents(events: TelemetryEvent[]) { const qlength = this.queue.length; if (events.length === 0) { @@ -82,12 +104,12 @@ export class TelemetryEventsSender { } } - private processEvents(events: object[]): object[] { - return events.map(function (obj: object): object { - const newObj = pick(obj, allowlistTop); + public processEvents(events: TelemetryEvent[]): TelemetryEvent[] { + return events.map(function (obj: TelemetryEvent): TelemetryEvent { + const newObj: TelemetryEvent = pick(obj, allowlistTop); if ('file' in obj) { newObj.file = pick(obj.file, allowlistFile); - if ('Ext' in obj.file) { + if (obj.file?.Ext !== undefined) { newObj.file.Ext = pick(obj.file.Ext, allowlistFileExt); } } @@ -112,6 +134,8 @@ export class TelemetryEventsSender { return; } + // Checking opt-in status is relatively expensive (calls a saved-object), so + // we only check it when we have things to send. this.isOptedIn = await this.telemetryStart?.getIsOptedIn(); if (!this.isOptedIn) { this.logger.debug(`Telemetry is not opted-in.`); diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 42ab8babcf4e1..cbd7a2757da17 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -64,8 +64,8 @@ import { securitySolutionSearchStrategyProvider } from './search_strategy/securi import { securitySolutionTimelineSearchStrategyProvider } from './search_strategy/timeline'; import { TelemetryEventsSender } from './lib/telemetry/sender'; import { - TelemetryPluginsStart, - TelemetryPluginsSetup, + TelemetryPluginStart, + TelemetryPluginSetup, } from '../../../../src/plugins/telemetry/server'; export interface SetupPlugins { @@ -80,14 +80,14 @@ export interface SetupPlugins { spaces?: SpacesSetup; taskManager?: TaskManagerSetupContract; usageCollection?: UsageCollectionSetup; - telemetry?: TelemetryPluginsSetup; + telemetry?: TelemetryPluginSetup; } export interface StartPlugins { data: DataPluginStart; ingestManager?: IngestManagerStartContract; taskManager?: TaskManagerStartContract; - telemetry?: TelemetryPluginsStart; + telemetry?: TelemetryPluginStart; } // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -112,8 +112,7 @@ export class Plugin implements IPlugin({ max: 3, maxAge: 1000 * 60 * 5 }); + this.telemetryEventsSender = new TelemetryEventsSender(this.logger); this.logger.debug('plugin initialized'); } @@ -136,7 +136,6 @@ export class Plugin implements IPlugin Date: Mon, 21 Sep 2020 15:09:53 +0200 Subject: [PATCH 08/21] Add cluster_uuid and cluster_name --- .../server/lib/telemetry/sender.ts | 60 ++++++++++++++++++- .../security_solution/server/plugin.ts | 2 +- 2 files changed, 58 insertions(+), 4 deletions(-) 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 ac997296b5e1f..accb3f4046c2e 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -6,7 +6,8 @@ import { pick, cloneDeep } from 'lodash'; -import { Logger } from 'src/core/server'; +import { LegacyAPICaller } from 'kibana/server'; +import { Logger, CoreStart } from '../../../../../../src/core/server'; import { TelemetryPluginStart, TelemetryPluginSetup, @@ -39,6 +40,8 @@ export type SearchTypes = export interface TelemetryEvent { [key: string]: SearchTypes; '@timestamp'?: string; + cluster_name?: string; + cluster_uuid?: string; file?: { [key: string]: SearchTypes; Ext?: { @@ -47,10 +50,39 @@ export interface TelemetryEvent { }; } +// Copied from telemetry_collection/get_cluster_info.ts +export interface ESClusterInfo { + cluster_uuid: string; + cluster_name: string; + version: { + number: string; + build_flavor: string; + build_type: string; + build_hash: string; + build_date: string; + build_snapshot?: boolean; + lucene_version: string; + minimum_wire_compatibility_version: string; + minimum_index_compatibility_version: string; + }; +} + +/** + * Get the cluster info from the connected cluster. + * + * This is the equivalent to GET / + * + * @param {function} callCluster The callWithInternalUser handler (exposed for testing) + */ +export function getClusterInfo(callCluster: LegacyAPICaller) { + return callCluster('info'); +} + export class TelemetryEventsSender { private readonly initialCheckDelayMs = 10 * 1000; private readonly checkIntervalMs = 5 * 1000; // TODO: change to 60s before merging private readonly logger: Logger; + private core?: CoreStart; private maxQueueSize = 100; private telemetryStart?: TelemetryPluginStart; private telemetrySetup?: TelemetryPluginSetup; @@ -67,8 +99,9 @@ export class TelemetryEventsSender { this.telemetrySetup = telemetrySetup; } - public start(telemetryStart?: TelemetryPluginStart) { + public start(core?: CoreStart, telemetryStart?: TelemetryPluginStart) { this.telemetryStart = telemetryStart; + this.core = core; this.logger.debug(`Starting task`); setTimeout(() => { @@ -146,10 +179,23 @@ export class TelemetryEventsSender { const telemetryUrl = await this.telemetrySetup?.getTelemetryUrl(); this.logger.debug(`Telemetry URL: ${telemetryUrl}`); + const clusterInfo = await this.fetchClusterInfo(); + this.logger.debug( + `cluster_uuid: ${clusterInfo?.cluster_uuid} cluster_name: ${clusterInfo?.cluster_name}` + ); + try { this.isSending = true; const toSend: object[] = cloneDeep(this.queue); this.queue = []; + + if (clusterInfo) { + toSend.forEach((event) => { + event.cluster_uuid = clusterInfo.cluster_uuid; + event.cluster_name = clusterInfo.cluster_name; + }); + } + this.sendEvents(toSend); } catch (err) { this.logger.warn(`Error sending telemetry events data: ${err}`); @@ -160,6 +206,14 @@ export class TelemetryEventsSender { private async sendEvents(events: object[]) { // TODO this.logger.debug(`Events sent!`); - // this.logger.debug(`Sending events: ${JSON.stringify(events, null, 2)}`); + this.logger.debug(`Sending events: ${JSON.stringify(events, null, 2)}`); + } + + private async fetchClusterInfo(): Promise { + if (!this.core) { + return undefined; + } + const callCluster = this.core.elasticsearch.legacy.client.callAsInternalUser; + return getClusterInfo(callCluster); } } diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 137dccf5f3261..d3be1a73d3dcc 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -342,7 +342,7 @@ export class Plugin implements IPlugin Date: Tue, 22 Sep 2020 18:03:19 +0200 Subject: [PATCH 09/21] Filter by endpoint alerts --- .../lib/detection_engine/signals/send_telemetry_events.ts | 2 +- .../plugins/security_solution/server/lib/telemetry/sender.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) 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 b1c621172174e..aa2c2989cc1bc 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 @@ -23,7 +23,7 @@ export function selectEvents(filteredEvents: SignalSearchResponse): TelemetryEve return sources.filter(function (obj: TelemetryEvent) { // TODO: filter out non-endpoint alerts - return obj !== undefined; + return obj.datastream.dataset === 'endpoint.alerts'; }); } 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 accb3f4046c2e..3347a66e15a26 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -40,6 +40,10 @@ export type SearchTypes = export interface TelemetryEvent { [key: string]: SearchTypes; '@timestamp'?: string; + datastream?: { + [key: string]: SearchTypes; + dataset?: string; + }; cluster_name?: string; cluster_uuid?: string; file?: { From 2fdc70c1364644feb7c5e0f2a38905938c84856d Mon Sep 17 00:00:00 2001 From: Tudor Golubenco Date: Wed, 23 Sep 2020 12:44:05 +0200 Subject: [PATCH 10/21] type fixes + tests --- .../signals/send_telemetry_events.test.ts | 20 +++++++++++-------- .../signals/send_telemetry_events.ts | 4 ++-- .../server/lib/telemetry/sender.ts | 2 +- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.test.ts index bff1be66db39b..5b41fd9d38c5d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.test.ts @@ -29,6 +29,9 @@ describe('sendAlertTelemetry', () => { _source: { '@timestamp': 'x', key1: 'hello', + datastream: { + dataset: 'endpoint.events', + }, }, }, { @@ -39,6 +42,10 @@ describe('sendAlertTelemetry', () => { _source: { '@timestamp': 'x', key2: 'hello', + datastream: { + dataset: 'endpoint.alerts', + other: 'x', + }, }, }, { @@ -49,6 +56,7 @@ describe('sendAlertTelemetry', () => { _source: { '@timestamp': 'x', key3: 'hello', + datastream: {}, }, }, ], @@ -57,17 +65,13 @@ describe('sendAlertTelemetry', () => { const sources = selectEvents(filteredEvents); expect(sources).toStrictEqual([ - { - '@timestamp': 'x', - key1: 'hello', - }, { '@timestamp': 'x', key2: 'hello', - }, - { - '@timestamp': 'x', - key3: 'hello', + datastream: { + dataset: 'endpoint.alerts', + other: 'x', + }, }, ]); }); 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 aa2c2989cc1bc..8cc1bff837887 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 @@ -22,8 +22,8 @@ export function selectEvents(filteredEvents: SignalSearchResponse): TelemetryEve }); return sources.filter(function (obj: TelemetryEvent) { - // TODO: filter out non-endpoint alerts - return obj.datastream.dataset === 'endpoint.alerts'; + // Filter out non-endpoint alerts + return obj.datastream?.dataset === 'endpoint.alerts'; }); } 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 3347a66e15a26..3df3feed4fae4 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -190,7 +190,7 @@ export class TelemetryEventsSender { try { this.isSending = true; - const toSend: object[] = cloneDeep(this.queue); + const toSend: TelemetryEvent[] = cloneDeep(this.queue); this.queue = []; if (clusterInfo) { From 069147f71b96a0ba89aa0f7f6318fb7a5f83fde8 Mon Sep 17 00:00:00 2001 From: Tudor Golubenco Date: Wed, 23 Sep 2020 21:57:19 +0200 Subject: [PATCH 11/21] fix types --- .../detection_engine/signals/search_after_bulk_create.test.ts | 1 + .../lib/detection_engine/signals/search_after_bulk_create.ts | 1 - .../lib/detection_engine/signals/signal_rule_alert_type.ts | 1 + .../signals/threat_mapping/create_threat_signal.ts | 2 ++ .../signals/threat_mapping/create_threat_signals.ts | 2 ++ .../lib/detection_engine/signals/threat_mapping/types.ts | 3 +++ .../server/lib/detection_engine/signals/types.ts | 1 + 7 files changed, 10 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index 2d2e178a22155..c82c1fe969ee3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -908,6 +908,7 @@ describe('searchAfterAndBulkCreate', () => { exceptionsList: [], services: mockService, logger: mockLogger, + eventsTelemetry: undefined, id: sampleRuleGuid, inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index 01e5e44d4a366..fde441639f41a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -7,7 +7,6 @@ import { singleSearchAfter } from './single_search_after'; import { singleBulkCreate } from './single_bulk_create'; import { filterEventsAgainstList } from './filter_events_with_list'; -import { TelemetryEventsSender } from '../../telemetry/sender'; import { sendAlertTelemetryEvents } from './send_telemetry_events'; import { createSearchAfterReturnType, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index cc623304e9620..d5eaa082acf41 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -362,6 +362,7 @@ export const signalRulesAlertType = ({ previousStartedAt, listClient, logger, + eventsTelemetry, alertId, outputIndex, params, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts index a6d4a2ba58ddd..560e7ad7fe2cb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts @@ -28,6 +28,7 @@ export const createThreatSignal = async ({ previousStartedAt, listClient, logger, + eventsTelemetry, alertId, outputIndex, params, @@ -77,6 +78,7 @@ export const createThreatSignal = async ({ ruleParams: params, services, logger, + eventsTelemetry, id: alertId, inputIndexPattern: inputIndex, signalsIndex: outputIndex, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts index f416ae6703b66..f44c7a9684457 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts @@ -24,6 +24,7 @@ export const createThreatSignals = async ({ previousStartedAt, listClient, logger, + eventsTelemetry, alertId, outputIndex, params, @@ -79,6 +80,7 @@ export const createThreatSignals = async ({ previousStartedAt, listClient, logger, + eventsTelemetry, alertId, outputIndex, params, 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 d63f2d2b3b6aa..7cd6e5196ea68 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 @@ -21,6 +21,7 @@ import { AlertServices } from '../../../../../../alerts/server'; import { ExceptionListItemSchema } from '../../../../../../lists/common/schemas'; import { ILegacyScopedClusterClient, Logger } from '../../../../../../../../src/core/server'; import { RuleAlertAction } from '../../../../../common/detection_engine/types'; +import { TelemetryEventsSender } from '../../../telemetry/sender'; import { BuildRuleMessage } from '../rule_messages'; import { SearchAfterAndBulkCreateReturnType } from '../types'; @@ -38,6 +39,7 @@ export interface CreateThreatSignalsOptions { previousStartedAt: Date | null; listClient: ListClient; logger: Logger; + eventsTelemetry: TelemetryEventsSender | undefined; alertId: string; outputIndex: string; params: RuleTypeParams; @@ -73,6 +75,7 @@ export interface CreateThreatSignalOptions { previousStartedAt: Date | null; listClient: ListClient; logger: Logger; + eventsTelemetry: TelemetryEventsSender | undefined; alertId: string; outputIndex: string; params: RuleTypeParams; 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 da07e31dfbfe2..55aeb63640014 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 @@ -21,6 +21,7 @@ import { ListClient } from '../../../../../lists/server'; import { Logger } from '../../../../../../../src/core/server'; import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { BuildRuleMessage } from './rule_messages'; +import { TelemetryEventsSender } from '../../telemetry/sender'; // used for gap detection code // eslint-disable-next-line @typescript-eslint/naming-convention From 0ae40468c2e3a1998e434f8e7efbb6a83b6ff18a Mon Sep 17 00:00:00 2001 From: Tudor Golubenco Date: Mon, 28 Sep 2020 15:54:48 +0200 Subject: [PATCH 12/21] Refactor processEvents This is using recursion now. Also, based on Xavier's review, moved up the try and the isSending check to avoid building up queries. --- .../server/lib/telemetry/sender.test.ts | 69 ++++++++- .../server/lib/telemetry/sender.ts | 140 ++++++++++++------ 2 files changed, 162 insertions(+), 47 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts index aa519ba82d48b..2f347271dfa91 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts @@ -5,7 +5,7 @@ */ /* eslint-disable dot-notation */ -import { TelemetryEventsSender } from './sender'; +import { TelemetryEventsSender, copyAllowlistedFields } from './sender'; import { loggingSystemMock } from 'src/core/server/mocks'; describe('TelemetryEventsSender', () => { @@ -28,7 +28,6 @@ describe('TelemetryEventsSender', () => { { event: { kind: 'alert', - something_else: 'nope', }, agent: { name: 'test', @@ -145,3 +144,69 @@ describe('TelemetryEventsSender', () => { }); }); }); + +describe('allowlistEventFields', () => { + const allowlist = { + a: true, + b: true, + c: { + d: true, + }, + }; + + it('filters top level', () => { + const event = { + a: 'a', + a1: 'a1', + b: 'b', + b1: 'b1', + }; + expect(copyAllowlistedFields(allowlist, event)).toStrictEqual({ + a: 'a', + b: 'b', + }); + }); + + it('filters nested', () => { + const event = { + a: { + a1: 'a1', + }, + a1: 'a1', + b: { + b1: 'b1', + }, + b1: 'b1', + c: { + d: 'd', + e: 'e', + f: 'f', + }, + }; + expect(copyAllowlistedFields(allowlist, event)).toStrictEqual({ + a: { + a1: 'a1', + }, + b: { + b1: 'b1', + }, + c: { + d: 'd', + }, + }); + }); + + it("doesn't create empty objects", () => { + const event = { + a: 'a', + b: 'b', + c: { + e: 'e', + }, + }; + expect(copyAllowlistedFields(allowlist, event)).toStrictEqual({ + a: 'a', + b: 'b', + }); + }); +}); 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 3df3feed4fae4..804d934e4b03a 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { pick, cloneDeep } from 'lodash'; +import { cloneDeep } from 'lodash'; import { LegacyAPICaller } from 'kibana/server'; import { Logger, CoreStart } from '../../../../../../src/core/server'; @@ -13,19 +13,6 @@ import { TelemetryPluginSetup, } from '../../../../../../src/plugins/telemetry/server'; -// Allowlist for the fields that we copy from the original event to the -// telemetry event. -// Top level fields: -const allowlistTop = ['@timestamp', 'agent', 'Endpoint', 'ecs', 'elastic']; -// file.* fields: -const allowlistFile = ['path', 'size', 'created', 'accessed', 'mtime', 'directory', 'hash']; -// file.Ext.* fields: -const allowlistFileExt = ['code_signature', 'malware_classification']; -// file.* fields: -const allowlistHost = ['os']; -// event.* fields: -const allowlistEvent = ['kind']; - export type SearchTypes = | string | string[] @@ -82,6 +69,80 @@ export function getClusterInfo(callCluster: LegacyAPICaller) { return callCluster('info'); } +interface AllowlistFields { + [key: string]: boolean | AllowlistFields; +} + +// Allow list for the data we include in the events. True means that it is deep-cloned +// blindly. Object contents means that we only copy the fields that appear explicitly in +// the sub-object. +const allowlistEventFields: AllowlistFields = { + '@timestamp': true, + agent: true, + Endpoint: true, + ecs: true, + elastic: true, + event: true, + file: { + name: true, + path: true, + size: true, + created: true, + accessed: true, + mtime: true, + directory: true, + hash: true, + Ext: { + code_signature: true, + malware_classification: true, + }, + }, + host: { + os: true, + }, + process: { + name: true, + executable: true, + command_line: true, + hash: true, + Ext: { + code_signature: true, + }, + parent: { + name: true, + executable: true, + command_line: true, + hash: true, + Ext: { + code_signature: true, + }, + }, + }, +}; + +export function copyAllowlistedFields( + allowlist: AllowlistFields, + event: TelemetryEvent +): TelemetryEvent { + const newEvent: TelemetryEvent = {}; + for (const key in allowlist) { + if (key in event) { + if (allowlist[key] === true) { + newEvent[key] = cloneDeep(event[key]); + } else if (typeof allowlist[key] === 'object' && typeof event[key] === 'object') { + const values = copyAllowlistedFields( + allowlist[key] as AllowlistFields, + event[key] as TelemetryEvent + ); + if (Object.keys(values).length > 0) { + newEvent[key] = values; + } + } + } + } + return newEvent; +} + export class TelemetryEventsSender { private readonly initialCheckDelayMs = 10 * 1000; private readonly checkIntervalMs = 5 * 1000; // TODO: change to 60s before merging @@ -143,21 +204,7 @@ export class TelemetryEventsSender { public processEvents(events: TelemetryEvent[]): TelemetryEvent[] { return events.map(function (obj: TelemetryEvent): TelemetryEvent { - const newObj: TelemetryEvent = pick(obj, allowlistTop); - if ('file' in obj) { - newObj.file = pick(obj.file, allowlistFile); - if (obj.file?.Ext !== undefined) { - newObj.file.Ext = pick(obj.file.Ext, allowlistFileExt); - } - } - if ('host' in obj) { - newObj.host = pick(obj.host, allowlistHost); - } - if ('event' in obj) { - newObj.event = pick(obj.event, allowlistEvent); - } - - return newObj; + return copyAllowlistedFields(allowlistEventFields, obj); }); } @@ -171,25 +218,27 @@ export class TelemetryEventsSender { return; } - // Checking opt-in status is relatively expensive (calls a saved-object), so - // we only check it when we have things to send. - this.isOptedIn = await this.telemetryStart?.getIsOptedIn(); - if (!this.isOptedIn) { - this.logger.debug(`Telemetry is not opted-in.`); - this.queue = []; - return; - } + try { + this.isSending = true; - const telemetryUrl = await this.telemetrySetup?.getTelemetryUrl(); - this.logger.debug(`Telemetry URL: ${telemetryUrl}`); + // Checking opt-in status is relatively expensive (calls a saved-object), so + // we only check it when we have things to send. + this.isOptedIn = await this.telemetryStart?.getIsOptedIn(); + if (!this.isOptedIn) { + this.logger.debug(`Telemetry is not opted-in.`); + this.queue = []; + this.isSending = false; + return; + } - const clusterInfo = await this.fetchClusterInfo(); - this.logger.debug( - `cluster_uuid: ${clusterInfo?.cluster_uuid} cluster_name: ${clusterInfo?.cluster_name}` - ); + const telemetryUrl = await this.telemetrySetup?.getTelemetryUrl(); + this.logger.debug(`Telemetry URL: ${telemetryUrl}`); + + const clusterInfo = await this.fetchClusterInfo(); + this.logger.debug( + `cluster_uuid: ${clusterInfo?.cluster_uuid} cluster_name: ${clusterInfo?.cluster_name}` + ); - try { - this.isSending = true; const toSend: TelemetryEvent[] = cloneDeep(this.queue); this.queue = []; @@ -203,6 +252,7 @@ export class TelemetryEventsSender { this.sendEvents(toSend); } catch (err) { this.logger.warn(`Error sending telemetry events data: ${err}`); + this.queue = []; } this.isSending = false; } From 2f31f49d3909af6fbbfa955caafa75a2ba8c4324 Mon Sep 17 00:00:00 2001 From: Tudor Golubenco Date: Tue, 29 Sep 2020 12:55:07 +0200 Subject: [PATCH 13/21] Send events to the telemetry server --- .../server/lib/telemetry/sender.test.ts | 34 ++- .../server/lib/telemetry/sender.ts | 266 ++++++++++-------- 2 files changed, 182 insertions(+), 118 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts index 2f347271dfa91..459ef6590840e 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts @@ -5,7 +5,7 @@ */ /* eslint-disable dot-notation */ -import { TelemetryEventsSender, copyAllowlistedFields } from './sender'; +import { TelemetryEventsSender, copyAllowlistedFields, getV3UrlFromV2 } from './sender'; import { loggingSystemMock } from 'src/core/server/mocks'; describe('TelemetryEventsSender', () => { @@ -109,10 +109,18 @@ describe('TelemetryEventsSender', () => { it('empties the queue when sending', async () => { const sender = new TelemetryEventsSender(logger); sender['sendEvents'] = jest.fn(); - const telemetryStart = { + sender['telemetryStart'] = { getIsOptedIn: jest.fn(async () => true), }; - sender['telemetryStart'] = telemetryStart; + sender['telemetrySetup'] = { + getTelemetryUrl: jest.fn(async () => 'https://telemetry.elastic.co'), + }; + sender['fetchClusterInfo'] = jest.fn(async () => { + return { + cluster_name: 'test', + cluster_uuid: 'test-uuid', + }; + }); sender.queueTelemetryEvents([{ 'event.kind': '1' }, { 'event.kind': '2' }]); expect(sender['queue'].length).toBe(2); @@ -210,3 +218,23 @@ describe('allowlistEventFields', () => { }); }); }); + +describe('getV3UrlFromV2', () => { + it('should return prod url', () => { + expect(getV3UrlFromV2('https://telemetry.elastic.co/xpack/v2/send', 'alerts-endpoint')).toBe( + 'https://telemetry.elastic.co/v3/send/alerts-endpoint' + ); + }); + + it('should return staging url', () => { + expect( + getV3UrlFromV2('https://telemetry-staging.elastic.co/xpack/v2/send', 'alerts-endpoint') + ).toBe('https://telemetry-staging.elastic.co/v3-dev/send/alerts-endpoint'); + }); + + it('should support ports and auth', () => { + expect( + getV3UrlFromV2('http://user:pass@myproxy.local:1337/xpack/v2/send', 'alerts-endpoint') + ).toBe('http://user:pass@myproxy.local:1337/v3/send/alerts-endpoint'); + }); +}); 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 804d934e4b03a..9eed88a25abde 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -5,9 +5,11 @@ */ import { cloneDeep } from 'lodash'; - +import axios from 'axios'; import { LegacyAPICaller } from 'kibana/server'; +import { URL } from 'url'; import { Logger, CoreStart } from '../../../../../../src/core/server'; +import { transformDataToNdjson } from '../../utils/read_stream/create_stream_from_ndjson'; import { TelemetryPluginStart, TelemetryPluginSetup, @@ -41,108 +43,6 @@ export interface TelemetryEvent { }; } -// Copied from telemetry_collection/get_cluster_info.ts -export interface ESClusterInfo { - cluster_uuid: string; - cluster_name: string; - version: { - number: string; - build_flavor: string; - build_type: string; - build_hash: string; - build_date: string; - build_snapshot?: boolean; - lucene_version: string; - minimum_wire_compatibility_version: string; - minimum_index_compatibility_version: string; - }; -} - -/** - * Get the cluster info from the connected cluster. - * - * This is the equivalent to GET / - * - * @param {function} callCluster The callWithInternalUser handler (exposed for testing) - */ -export function getClusterInfo(callCluster: LegacyAPICaller) { - return callCluster('info'); -} - -interface AllowlistFields { - [key: string]: boolean | AllowlistFields; -} - -// Allow list for the data we include in the events. True means that it is deep-cloned -// blindly. Object contents means that we only copy the fields that appear explicitly in -// the sub-object. -const allowlistEventFields: AllowlistFields = { - '@timestamp': true, - agent: true, - Endpoint: true, - ecs: true, - elastic: true, - event: true, - file: { - name: true, - path: true, - size: true, - created: true, - accessed: true, - mtime: true, - directory: true, - hash: true, - Ext: { - code_signature: true, - malware_classification: true, - }, - }, - host: { - os: true, - }, - process: { - name: true, - executable: true, - command_line: true, - hash: true, - Ext: { - code_signature: true, - }, - parent: { - name: true, - executable: true, - command_line: true, - hash: true, - Ext: { - code_signature: true, - }, - }, - }, -}; - -export function copyAllowlistedFields( - allowlist: AllowlistFields, - event: TelemetryEvent -): TelemetryEvent { - const newEvent: TelemetryEvent = {}; - for (const key in allowlist) { - if (key in event) { - if (allowlist[key] === true) { - newEvent[key] = cloneDeep(event[key]); - } else if (typeof allowlist[key] === 'object' && typeof event[key] === 'object') { - const values = copyAllowlistedFields( - allowlist[key] as AllowlistFields, - event[key] as TelemetryEvent - ); - if (Object.keys(values).length > 0) { - newEvent[key] = values; - } - } - } - } - return newEvent; -} - export class TelemetryEventsSender { private readonly initialCheckDelayMs = 10 * 1000; private readonly checkIntervalMs = 5 * 1000; // TODO: change to 60s before merging @@ -232,9 +132,16 @@ export class TelemetryEventsSender { } const telemetryUrl = await this.telemetrySetup?.getTelemetryUrl(); - this.logger.debug(`Telemetry URL: ${telemetryUrl}`); + if (!telemetryUrl) { + throw new Error("Couldn't get telemetry URL"); + } + const v3TelemetryUrl = getV3UrlFromV2(telemetryUrl.toString(), 'alerts-debug'); // TODO: update + this.logger.debug(`Telemetry URL: ${v3TelemetryUrl}`); const clusterInfo = await this.fetchClusterInfo(); + if (!clusterInfo) { + throw new Error("Couldn't get cluster Info"); + } this.logger.debug( `cluster_uuid: ${clusterInfo?.cluster_uuid} cluster_name: ${clusterInfo?.cluster_name}` ); @@ -242,25 +149,38 @@ export class TelemetryEventsSender { const toSend: TelemetryEvent[] = cloneDeep(this.queue); this.queue = []; - if (clusterInfo) { - toSend.forEach((event) => { - event.cluster_uuid = clusterInfo.cluster_uuid; - event.cluster_name = clusterInfo.cluster_name; - }); - } + toSend.forEach((event) => { + event.cluster_uuid = clusterInfo.cluster_uuid; + event.cluster_name = clusterInfo.cluster_name; + }); - this.sendEvents(toSend); + await this.sendEvents(toSend, v3TelemetryUrl, clusterInfo.cluster_uuid); } catch (err) { this.logger.warn(`Error sending telemetry events data: ${err}`); + // throw err; this.queue = []; } this.isSending = false; } - private async sendEvents(events: object[]) { - // TODO - this.logger.debug(`Events sent!`); - this.logger.debug(`Sending events: ${JSON.stringify(events, null, 2)}`); + private async sendEvents(events: unknown[], telemetryUrl: string, clusterUuid: string) { + // this.logger.debug(`Sending events: ${JSON.stringify(events, null, 2)}`); + const ndjson = transformDataToNdjson(events); + // this.logger.debug(`NDJSON: ${ndjson}`); + + try { + const resp = await axios.post(`${telemetryUrl}?debug=true`, ndjson, { + // TODO: remove the debug + headers: { + 'Content-Type': 'application/x-ndjson', + 'X-Elastic-Cluster-ID': clusterUuid, + 'X-Elastic-Telemetry': '1', // TODO: no longer needed? + }, + }); + this.logger.debug(`Events sent!. Response: ${resp.status} ${resp.data}`); + } catch (err) { + this.logger.warn(`Error sending events: ${err.response.status} ${err.response.data}`); + } } private async fetchClusterInfo(): Promise { @@ -271,3 +191,119 @@ export class TelemetryEventsSender { return getClusterInfo(callCluster); } } + +// For the Allowlist definition. +interface AllowlistFields { + [key: string]: boolean | AllowlistFields; +} + +// Allow list for the data we include in the events. True means that it is deep-cloned +// blindly. Object contents means that we only copy the fields that appear explicitly in +// the sub-object. +const allowlistEventFields: AllowlistFields = { + '@timestamp': true, + agent: true, + Endpoint: true, + ecs: true, + elastic: true, + event: true, + file: { + name: true, + path: true, + size: true, + created: true, + accessed: true, + mtime: true, + directory: true, + hash: true, + Ext: { + code_signature: true, + malware_classification: true, + }, + }, + host: { + os: true, + }, + process: { + name: true, + executable: true, + command_line: true, + hash: true, + Ext: { + code_signature: true, + }, + parent: { + name: true, + executable: true, + command_line: true, + hash: true, + Ext: { + code_signature: true, + }, + }, + }, +}; + +export function copyAllowlistedFields( + allowlist: AllowlistFields, + event: TelemetryEvent +): TelemetryEvent { + const newEvent: TelemetryEvent = {}; + for (const key in allowlist) { + if (key in event) { + if (allowlist[key] === true) { + newEvent[key] = cloneDeep(event[key]); + } else if (typeof allowlist[key] === 'object' && typeof event[key] === 'object') { + const values = copyAllowlistedFields( + allowlist[key] as AllowlistFields, + event[key] as TelemetryEvent + ); + if (Object.keys(values).length > 0) { + newEvent[key] = values; + } + } + } + } + return newEvent; +} + +// Forms URLs like: +// https://telemetry.elastic.co/v3/send/my-channel-name or +// https://telemetry-staging.elastic.co/v3-dev/send/my-channel-name +export function getV3UrlFromV2(v2url: string, channel: string): string { + const url = new URL(v2url); + if (url.hostname.search('staging') < 0) { + url.pathname = `/v3/send/${channel}`; + } else { + url.pathname = `/v3-dev/send/${channel}`; + } + return url.toString(); +} + +// For getting cluster info. Copied from telemetry_collection/get_cluster_info.ts +export interface ESClusterInfo { + cluster_uuid: string; + cluster_name: string; + version: { + number: string; + build_flavor: string; + build_type: string; + build_hash: string; + build_date: string; + build_snapshot?: boolean; + lucene_version: string; + minimum_wire_compatibility_version: string; + minimum_index_compatibility_version: string; + }; +} + +/** + * Get the cluster info from the connected cluster. + * + * This is the equivalent to GET / + * + * @param {function} callCluster The callWithInternalUser handler (exposed for testing) + */ +export function getClusterInfo(callCluster: LegacyAPICaller) { + return callCluster('info'); +} From f8309c932ddb678ae606b0e889b8e96523e748f6 Mon Sep 17 00:00:00 2001 From: Tudor Golubenco Date: Tue, 29 Sep 2020 13:17:26 +0200 Subject: [PATCH 14/21] Small refactoring --- .../server/lib/telemetry/sender.test.ts | 3 +- .../server/lib/telemetry/sender.ts | 39 ++++++++++--------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts index 459ef6590840e..1ebdcb6b9d3f4 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts @@ -7,6 +7,7 @@ /* eslint-disable dot-notation */ import { TelemetryEventsSender, copyAllowlistedFields, getV3UrlFromV2 } from './sender'; import { loggingSystemMock } from 'src/core/server/mocks'; +import { URL } from 'url'; describe('TelemetryEventsSender', () => { let logger: ReturnType; @@ -113,7 +114,7 @@ describe('TelemetryEventsSender', () => { getIsOptedIn: jest.fn(async () => true), }; sender['telemetrySetup'] = { - getTelemetryUrl: jest.fn(async () => 'https://telemetry.elastic.co'), + getTelemetryUrl: jest.fn(async () => new URL('https://telemetry.elastic.co')), }; sender['fetchClusterInfo'] = jest.fn(async () => { return { 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 9eed88a25abde..2833716782320 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -131,17 +131,10 @@ export class TelemetryEventsSender { return; } - const telemetryUrl = await this.telemetrySetup?.getTelemetryUrl(); - if (!telemetryUrl) { - throw new Error("Couldn't get telemetry URL"); - } - const v3TelemetryUrl = getV3UrlFromV2(telemetryUrl.toString(), 'alerts-debug'); // TODO: update - this.logger.debug(`Telemetry URL: ${v3TelemetryUrl}`); + const telemetryUrl = await this.fetchTelemetryUrl(); + this.logger.debug(`Telemetry URL: ${telemetryUrl}`); const clusterInfo = await this.fetchClusterInfo(); - if (!clusterInfo) { - throw new Error("Couldn't get cluster Info"); - } this.logger.debug( `cluster_uuid: ${clusterInfo?.cluster_uuid} cluster_name: ${clusterInfo?.cluster_name}` ); @@ -154,7 +147,7 @@ export class TelemetryEventsSender { event.cluster_name = clusterInfo.cluster_name; }); - await this.sendEvents(toSend, v3TelemetryUrl, clusterInfo.cluster_uuid); + await this.sendEvents(toSend, telemetryUrl, clusterInfo.cluster_uuid); } catch (err) { this.logger.warn(`Error sending telemetry events data: ${err}`); // throw err; @@ -163,6 +156,22 @@ export class TelemetryEventsSender { this.isSending = false; } + private async fetchClusterInfo(): Promise { + if (!this.core) { + throw Error("Couldn't fetch cluster info because core is not available"); + } + const callCluster = this.core.elasticsearch.legacy.client.callAsInternalUser; + return getClusterInfo(callCluster); + } + + private async fetchTelemetryUrl(): Promise { + const telemetryUrl = await this.telemetrySetup?.getTelemetryUrl(); + if (!telemetryUrl) { + throw Error("Couldn't get telemetry URL"); + } + return getV3UrlFromV2(telemetryUrl.toString(), 'alerts-debug'); // TODO: update + } + private async sendEvents(events: unknown[], telemetryUrl: string, clusterUuid: string) { // this.logger.debug(`Sending events: ${JSON.stringify(events, null, 2)}`); const ndjson = transformDataToNdjson(events); @@ -182,14 +191,6 @@ export class TelemetryEventsSender { this.logger.warn(`Error sending events: ${err.response.status} ${err.response.data}`); } } - - private async fetchClusterInfo(): Promise { - if (!this.core) { - return undefined; - } - const callCluster = this.core.elasticsearch.legacy.client.callAsInternalUser; - return getClusterInfo(callCluster); - } } // For the Allowlist definition. @@ -284,7 +285,7 @@ export function getV3UrlFromV2(v2url: string, channel: string): string { export interface ESClusterInfo { cluster_uuid: string; cluster_name: string; - version: { + version?: { number: string; build_flavor: string; build_type: string; From b2df3a8733ca0425d206428fe3c94d7921a37d07 Mon Sep 17 00:00:00 2001 From: Tudor Golubenco Date: Tue, 29 Sep 2020 14:07:05 +0200 Subject: [PATCH 15/21] Add license fields --- .../server/lib/telemetry/sender.ts | 63 ++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) 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 2833716782320..c9be8857cc721 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -41,6 +41,7 @@ export interface TelemetryEvent { [key: string]: SearchTypes; }; }; + license?: ESLicense; } export class TelemetryEventsSender { @@ -139,12 +140,15 @@ export class TelemetryEventsSender { `cluster_uuid: ${clusterInfo?.cluster_uuid} cluster_name: ${clusterInfo?.cluster_name}` ); + const licenseInfo = await this.fetchLicenseInfo(); + const toSend: TelemetryEvent[] = cloneDeep(this.queue); this.queue = []; toSend.forEach((event) => { event.cluster_uuid = clusterInfo.cluster_uuid; event.cluster_name = clusterInfo.cluster_name; + this.copyLicenseFields(event, licenseInfo); }); await this.sendEvents(toSend, telemetryUrl, clusterInfo.cluster_uuid); @@ -172,6 +176,36 @@ export class TelemetryEventsSender { return getV3UrlFromV2(telemetryUrl.toString(), 'alerts-debug'); // TODO: update } + private async fetchLicenseInfo(): Promise { + if (!this.core) { + return undefined; + } + try { + const callCluster = this.core.elasticsearch.legacy.client.callAsInternalUser; + const ret = await getLicense(callCluster, true); + return ret.license; + } catch (err) { + this.logger.warn(`Error retrieving license: ${err}`); + return undefined; + } + } + + private copyLicenseFields(event: TelemetryEvent, lic: ESLicense | undefined) { + if (lic) { + event.license = { + uid: lic.uid, + status: lic.status, + type: lic.type, + }; + if (lic.issued_to) { + event.license.issued_to = lic.issued_to; + } + if (lic.issuer) { + event.license.issuer = lic.issuer; + } + } + } + private async sendEvents(events: unknown[], telemetryUrl: string, clusterUuid: string) { // this.logger.debug(`Sending events: ${JSON.stringify(events, null, 2)}`); const ndjson = transformDataToNdjson(events); @@ -305,6 +339,33 @@ export interface ESClusterInfo { * * @param {function} callCluster The callWithInternalUser handler (exposed for testing) */ -export function getClusterInfo(callCluster: LegacyAPICaller) { +function getClusterInfo(callCluster: LegacyAPICaller) { return callCluster('info'); } + +// From https://www.elastic.co/guide/en/elasticsearch/reference/current/get-license.html +export interface ESLicense { + status: string; + uid: string; + type: string; + issue_date?: string; + issue_date_in_millis?: number; + expiry_date?: string; + expirty_date_in_millis?: number; + max_nodes?: number; + issued_to?: string; + issuer?: string; + start_date_in_millis?: number; +} + +function getLicense(callCluster: LegacyAPICaller, local: boolean) { + return callCluster<{ license: ESLicense }>('transport.request', { + method: 'GET', + path: '/_license', + query: { + local, + // For versions >= 7.6 and < 8.0, this flag is needed otherwise 'platinum' is returned for 'enterprise' license. + accept_enterprise: 'true', + }, + }); +} From e6a2d7156ca6833358841fc16affbf30e9c81f73 Mon Sep 17 00:00:00 2001 From: Tudor Golubenco Date: Tue, 29 Sep 2020 14:09:14 +0200 Subject: [PATCH 16/21] Update x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.ts Co-authored-by: Garrett Spong --- .../lib/detection_engine/signals/send_telemetry_events.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 8cc1bff837887..1b9219aba4046 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 @@ -21,10 +21,8 @@ export function selectEvents(filteredEvents: SignalSearchResponse): TelemetryEve return obj._source; }); - return sources.filter(function (obj: TelemetryEvent) { - // Filter out non-endpoint alerts - return obj.datastream?.dataset === 'endpoint.alerts'; - }); + // Filter out non-endpoint alerts + return sources.filter((obj: TelemetryEvent) => obj.datastream?.dataset === 'endpoint.alerts'); } export function sendAlertTelemetryEvents( From 722e68db43f2edcdfde75af2559cc5e225dedec1 Mon Sep 17 00:00:00 2001 From: Tudor Golubenco Date: Tue, 29 Sep 2020 14:21:46 +0200 Subject: [PATCH 17/21] Move undefined check in the function to simplify top level code --- .../signals/search_after_bulk_create.ts | 16 +++++++--------- .../signals/send_telemetry_events.ts | 6 +++++- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index fde441639f41a..096f63b968cba 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -189,15 +189,13 @@ export const searchAfterAndBulkCreate = async ({ buildRuleMessage(`filteredEvents.hits.hits: ${filteredEvents.hits.hits.length}`) ); - if (eventsTelemetry !== undefined) { - sendAlertTelemetryEvents( - logger, - eventsTelemetry, - filteredEvents, - ruleParams, - buildRuleMessage - ); - } + sendAlertTelemetryEvents( + logger, + eventsTelemetry, + filteredEvents, + ruleParams, + buildRuleMessage + ); } // we are guaranteed to have searchResult hits at this point 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 1b9219aba4046..84a18627ce378 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 @@ -27,11 +27,15 @@ export function selectEvents(filteredEvents: SignalSearchResponse): TelemetryEve export function sendAlertTelemetryEvents( logger: Logger, - eventsTelemetry: TelemetryEventsSender, + eventsTelemetry: TelemetryEventsSender | undefined, filteredEvents: SignalSearchResponse, ruleParams: RuleTypeParams, buildRuleMessage: BuildRuleMessage ) { + if (eventsTelemetry === undefined) { + return; + } + const sources = selectEvents(filteredEvents); try { From 5f254358d8be1c723ef5aabaa7e8a48361b6dd3a Mon Sep 17 00:00:00 2001 From: Tudor Golubenco Date: Tue, 29 Sep 2020 14:56:17 +0200 Subject: [PATCH 18/21] Correct datastream to data_stream --- .../lib/detection_engine/signals/send_telemetry_events.ts | 2 +- .../security_solution/server/lib/telemetry/sender.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) 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 84a18627ce378..5963d31bda8a6 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 @@ -22,7 +22,7 @@ export function selectEvents(filteredEvents: SignalSearchResponse): TelemetryEve }); // Filter out non-endpoint alerts - return sources.filter((obj: TelemetryEvent) => obj.datastream?.dataset === 'endpoint.alerts'); + return sources.filter((obj: TelemetryEvent) => obj.data_stream?.dataset === 'endpoint.alerts'); } export function sendAlertTelemetryEvents( 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 c9be8857cc721..e1e8f7309c1f9 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -29,7 +29,7 @@ export type SearchTypes = export interface TelemetryEvent { [key: string]: SearchTypes; '@timestamp'?: string; - datastream?: { + data_stream?: { [key: string]: SearchTypes; dataset?: string; }; @@ -173,7 +173,7 @@ export class TelemetryEventsSender { if (!telemetryUrl) { throw Error("Couldn't get telemetry URL"); } - return getV3UrlFromV2(telemetryUrl.toString(), 'alerts-debug'); // TODO: update + return getV3UrlFromV2(telemetryUrl.toString(), 'alerts-endpoint'); // TODO: update } private async fetchLicenseInfo(): Promise { @@ -207,7 +207,7 @@ export class TelemetryEventsSender { } private async sendEvents(events: unknown[], telemetryUrl: string, clusterUuid: string) { - // this.logger.debug(`Sending events: ${JSON.stringify(events, null, 2)}`); + this.logger.debug(`Sending events: ${JSON.stringify(events, null, 2)}`); const ndjson = transformDataToNdjson(events); // this.logger.debug(`NDJSON: ${ndjson}`); From 1a29cbd2eedcfd6f943fa63b55244acc8185531d Mon Sep 17 00:00:00 2001 From: Tudor Golubenco Date: Wed, 30 Sep 2020 13:06:22 +0200 Subject: [PATCH 19/21] Incorporated Xavier's feedback + add license header --- .../server/lib/telemetry/sender.ts | 94 +++++++++---------- 1 file changed, 45 insertions(+), 49 deletions(-) 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 e1e8f7309c1f9..b30918da857f7 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -46,7 +46,7 @@ export interface TelemetryEvent { export class TelemetryEventsSender { private readonly initialCheckDelayMs = 10 * 1000; - private readonly checkIntervalMs = 5 * 1000; // TODO: change to 60s before merging + private readonly checkIntervalMs = 60 * 1000; private readonly logger: Logger; private core?: CoreStart; private maxQueueSize = 100; @@ -110,7 +110,6 @@ export class TelemetryEventsSender { } private async sendIfDue() { - // this.logger.debug(`Send if due`); if (this.isSending) { return; } @@ -132,26 +131,26 @@ export class TelemetryEventsSender { return; } - const telemetryUrl = await this.fetchTelemetryUrl(); - this.logger.debug(`Telemetry URL: ${telemetryUrl}`); + const [telemetryUrl, clusterInfo, licenseInfo] = await Promise.all([ + this.fetchTelemetryUrl(), + this.fetchClusterInfo(), + this.fetchLicenseInfo(), + ]); - const clusterInfo = await this.fetchClusterInfo(); + this.logger.debug(`Telemetry URL: ${telemetryUrl}`); this.logger.debug( `cluster_uuid: ${clusterInfo?.cluster_uuid} cluster_name: ${clusterInfo?.cluster_name}` ); - const licenseInfo = await this.fetchLicenseInfo(); - - const toSend: TelemetryEvent[] = cloneDeep(this.queue); + const toSend: TelemetryEvent[] = cloneDeep(this.queue).map((event) => ({ + ...event, + ...(licenseInfo ? { license: this.copyLicenseFields(licenseInfo) } : {}), + cluster_uuid: clusterInfo.cluster_uuid, + cluster_name: clusterInfo.cluster_name, + })); this.queue = []; - toSend.forEach((event) => { - event.cluster_uuid = clusterInfo.cluster_uuid; - event.cluster_name = clusterInfo.cluster_name; - this.copyLicenseFields(event, licenseInfo); - }); - - await this.sendEvents(toSend, telemetryUrl, clusterInfo.cluster_uuid); + await this.sendEvents(toSend, telemetryUrl, clusterInfo.cluster_uuid, licenseInfo?.uid); } catch (err) { this.logger.warn(`Error sending telemetry events data: ${err}`); // throw err; @@ -173,7 +172,7 @@ export class TelemetryEventsSender { if (!telemetryUrl) { throw Error("Couldn't get telemetry URL"); } - return getV3UrlFromV2(telemetryUrl.toString(), 'alerts-endpoint'); // TODO: update + return getV3UrlFromV2(telemetryUrl.toString(), 'alerts-endpoint'); } private async fetchLicenseInfo(): Promise { @@ -190,33 +189,32 @@ export class TelemetryEventsSender { } } - private copyLicenseFields(event: TelemetryEvent, lic: ESLicense | undefined) { - if (lic) { - event.license = { - uid: lic.uid, - status: lic.status, - type: lic.type, - }; - if (lic.issued_to) { - event.license.issued_to = lic.issued_to; - } - if (lic.issuer) { - event.license.issuer = lic.issuer; - } - } + private copyLicenseFields(lic: ESLicense) { + return { + uid: lic.uid, + status: lic.status, + type: lic.type, + ...(lic.issued_to ? { issued_to: lic.issued_to } : {}), + ...(lic.issuer ? { issuer: lic.issuer } : {}), + }; } - private async sendEvents(events: unknown[], telemetryUrl: string, clusterUuid: string) { + private async sendEvents( + events: unknown[], + telemetryUrl: string, + clusterUuid: string, + licenseId: string | undefined + ) { this.logger.debug(`Sending events: ${JSON.stringify(events, null, 2)}`); const ndjson = transformDataToNdjson(events); // this.logger.debug(`NDJSON: ${ndjson}`); try { - const resp = await axios.post(`${telemetryUrl}?debug=true`, ndjson, { - // TODO: remove the debug + const resp = await axios.post(`${telemetryUrl}`, ndjson, { headers: { 'Content-Type': 'application/x-ndjson', 'X-Elastic-Cluster-ID': clusterUuid, + ...(licenseId ? { 'X-Elastic-License-ID': licenseId } : {}), 'X-Elastic-Telemetry': '1', // TODO: no longer needed? }, }); @@ -283,23 +281,21 @@ export function copyAllowlistedFields( allowlist: AllowlistFields, event: TelemetryEvent ): TelemetryEvent { - const newEvent: TelemetryEvent = {}; - for (const key in allowlist) { - if (key in event) { - if (allowlist[key] === true) { - newEvent[key] = cloneDeep(event[key]); - } else if (typeof allowlist[key] === 'object' && typeof event[key] === 'object') { - const values = copyAllowlistedFields( - allowlist[key] as AllowlistFields, - event[key] as TelemetryEvent - ); - if (Object.keys(values).length > 0) { - newEvent[key] = values; - } + return Object.entries(allowlist).reduce((newEvent, [allowKey, allowValue]) => { + const eventValue = event[allowKey]; + if (eventValue) { + if (allowValue === true) { + return { ...newEvent, [allowKey]: eventValue }; + } else if (typeof allowValue === 'object' && typeof eventValue === 'object') { + const values = copyAllowlistedFields(allowValue, eventValue as TelemetryEvent); + return { + ...newEvent, + ...(Object.keys(values).length > 0 ? { [allowKey]: values } : {}), + }; } } - } - return newEvent; + return newEvent; + }, {}); } // Forms URLs like: @@ -307,7 +303,7 @@ export function copyAllowlistedFields( // https://telemetry-staging.elastic.co/v3-dev/send/my-channel-name export function getV3UrlFromV2(v2url: string, channel: string): string { const url = new URL(v2url); - if (url.hostname.search('staging') < 0) { + if (!url.hostname.includes('staging')) { url.pathname = `/v3/send/${channel}`; } else { url.pathname = `/v3-dev/send/${channel}`; From 0dda88cd640324123878643637f4dd0385b867c3 Mon Sep 17 00:00:00 2001 From: Tudor Golubenco Date: Wed, 30 Sep 2020 13:52:41 +0200 Subject: [PATCH 20/21] Test fix + minor changes --- .../signals/send_telemetry_events.test.ts | 8 ++++---- .../security_solution/server/lib/telemetry/sender.ts | 7 ++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.test.ts index 5b41fd9d38c5d..2a531998ff8a6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.test.ts @@ -29,7 +29,7 @@ describe('sendAlertTelemetry', () => { _source: { '@timestamp': 'x', key1: 'hello', - datastream: { + data_stream: { dataset: 'endpoint.events', }, }, @@ -42,7 +42,7 @@ describe('sendAlertTelemetry', () => { _source: { '@timestamp': 'x', key2: 'hello', - datastream: { + data_stream: { dataset: 'endpoint.alerts', other: 'x', }, @@ -56,7 +56,7 @@ describe('sendAlertTelemetry', () => { _source: { '@timestamp': 'x', key3: 'hello', - datastream: {}, + data_stream: {}, }, }, ], @@ -68,7 +68,7 @@ describe('sendAlertTelemetry', () => { { '@timestamp': 'x', key2: 'hello', - datastream: { + data_stream: { dataset: 'endpoint.alerts', other: 'x', }, 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 b30918da857f7..a1bfb9910c5f8 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -153,7 +153,6 @@ export class TelemetryEventsSender { await this.sendEvents(toSend, telemetryUrl, clusterInfo.cluster_uuid, licenseInfo?.uid); } catch (err) { this.logger.warn(`Error sending telemetry events data: ${err}`); - // throw err; this.queue = []; } this.isSending = false; @@ -218,9 +217,11 @@ export class TelemetryEventsSender { 'X-Elastic-Telemetry': '1', // TODO: no longer needed? }, }); - this.logger.debug(`Events sent!. Response: ${resp.status} ${resp.data}`); + this.logger.debug(`Events sent!. Response: ${resp.status} ${JSON.stringify(resp.data)}`); } catch (err) { - this.logger.warn(`Error sending events: ${err.response.status} ${err.response.data}`); + this.logger.warn( + `Error sending events: ${err.response.status} ${JSON.stringify(err.response.data)}` + ); } } } From 23fbe32f29c49599a78c28f4aa54a3e723ae44fb Mon Sep 17 00:00:00 2001 From: Tudor Golubenco Date: Wed, 30 Sep 2020 14:25:27 +0200 Subject: [PATCH 21/21] Commented out verbose debug logs --- .../plugins/security_solution/server/lib/telemetry/sender.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 a1bfb9910c5f8..acee75abddcd9 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -204,12 +204,12 @@ export class TelemetryEventsSender { clusterUuid: string, licenseId: string | undefined ) { - this.logger.debug(`Sending events: ${JSON.stringify(events, null, 2)}`); + // this.logger.debug(`Sending events: ${JSON.stringify(events, null, 2)}`); const ndjson = transformDataToNdjson(events); // this.logger.debug(`NDJSON: ${ndjson}`); try { - const resp = await axios.post(`${telemetryUrl}`, ndjson, { + const resp = await axios.post(telemetryUrl, ndjson, { headers: { 'Content-Type': 'application/x-ndjson', 'X-Elastic-Cluster-ID': clusterUuid,