Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7.11][Telemetry] Diagnostic Alert Telemetry #84422

Merged
merged 24 commits into from
Dec 10, 2020
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e091be3
Port @tsg's work on task manager.
pjhampton Nov 26, 2020
e5c72c1
Update on agreed method. Fixes race condition.
pjhampton Dec 4, 2020
f0f5438
Merge branch 'master' into pjhampton/diagnostic-alert-telemetry
kibanamachine Dec 4, 2020
ae8e2f4
Expand wildcards.
pjhampton Dec 4, 2020
49a480d
Merge branch 'pjhampton/diagnostic-alert-telemetry' of github.com:ela…
pjhampton Dec 4, 2020
76cb626
stage.
pjhampton Dec 7, 2020
49da9f4
Merge branch 'master' into pjhampton/diagnostic-alert-telemetry
pjhampton Dec 7, 2020
2d685a4
Add rule.ruleset collection.
pjhampton Dec 7, 2020
7becfb7
Update telemetry sender with correct query for loading diag alerts.
pjhampton Dec 7, 2020
1974c94
Add similar task tests to endpont artifact work.
pjhampton Dec 8, 2020
0a13f63
Fix broken import statement.
pjhampton Dec 8, 2020
c709fc6
Create sender mocks.
pjhampton Dec 8, 2020
b8d9ddb
Update test to check for func call.
pjhampton Dec 8, 2020
535098e
Merge branch 'master' into pjhampton/diagnostic-alert-telemetry
kibanamachine Dec 8, 2020
f0d226e
Update unused reference.
pjhampton Dec 8, 2020
5638da9
record last run.
pjhampton Dec 9, 2020
2018132
Update index.
pjhampton Dec 9, 2020
43558f7
fix import
pjhampton Dec 9, 2020
07cdf34
Fix test.
pjhampton Dec 9, 2020
1ddd2ef
test fix.
pjhampton Dec 9, 2020
d14f8f9
Pass unit to time diff calc.
pjhampton Dec 9, 2020
58672b3
Merge branch 'master' into pjhampton/diagnostic-alert-telemetry
kibanamachine Dec 9, 2020
4c17199
Tests should pass now hopefully.
pjhampton Dec 9, 2020
b707729
Add additional process fields to allowlist.
pjhampton Dec 9, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions x-pack/plugins/security_solution/server/lib/telemetry/mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* 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';
import { TelemetryDiagTask } from './task';

/**
* Creates a mocked Telemetry Events Sender
*/
export const createMockTelemetryEventsSender = (
enableTelemtry: boolean
): jest.Mocked<TelemetryEventsSender> => {
return ({
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
fetchDiagnosticAlerts: jest.fn(),
queueTelemetryEvents: jest.fn(),
processEvents: jest.fn(),
isTelemetryOptedIn: jest.fn().mockReturnValue(enableTelemtry ?? jest.fn()),
sendIfDue: jest.fn(),
fetchClusterInfo: jest.fn(),
fetchTelemetryUrl: jest.fn(),
fetchLicenseInfo: jest.fn(),
copyLicenseFields: jest.fn(),
sendEvents: jest.fn(),
} as unknown) as jest.Mocked<TelemetryEventsSender>;
};

/**
* Creates a mocked Telemetry Diagnostic Task
*/
export class MockTelemetryDiagnosticTask extends TelemetryDiagTask {
public runTask = jest.fn();
}
71 changes: 65 additions & 6 deletions x-pack/plugins/security_solution/server/lib/telemetry/sender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ import {
TelemetryPluginStart,
TelemetryPluginSetup,
} from '../../../../../../src/plugins/telemetry/server';
import {
TaskManagerSetupContract,
TaskManagerStartContract,
} from '../../../../task_manager/server';
import { TelemetryDiagTask } from './task';

export type SearchTypes =
| string
Expand Down Expand Up @@ -56,20 +61,34 @@ export class TelemetryEventsSender {
private isSending = false;
private queue: TelemetryEvent[] = [];
private isOptedIn?: boolean = true; // Assume true until the first check
private diagTask?: TelemetryDiagTask;

constructor(logger: Logger) {
this.logger = logger.get('telemetry_events');
}

public setup(telemetrySetup?: TelemetryPluginSetup) {
public setup(telemetrySetup?: TelemetryPluginSetup, taskManager?: TaskManagerSetupContract) {
this.telemetrySetup = telemetrySetup;

if (taskManager) {
this.diagTask = new TelemetryDiagTask(this.logger, taskManager, this);
}
}

public start(core?: CoreStart, telemetryStart?: TelemetryPluginStart) {
public start(
core?: CoreStart,
telemetryStart?: TelemetryPluginStart,
taskManager?: TaskManagerStartContract
) {
this.telemetryStart = telemetryStart;
this.core = core;

this.logger.debug(`Starting task`);
if (taskManager && this.diagTask) {
this.logger.debug(`Starting diag task`);
this.diagTask.start(taskManager);
}

this.logger.debug(`Starting local task`);
setTimeout(() => {
this.sendIfDue();
this.intervalId = setInterval(() => this.sendIfDue(), this.checkIntervalMs);
Expand All @@ -82,6 +101,38 @@ export class TelemetryEventsSender {
}
}

public async fetchDiagnosticAlerts() {
const query = {
expand_wildcards: 'open,hidden',
index: 'logs-endpoint.diagnostic.collection-default*',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need logs-endpoint.diagnostic.collection-* here, because I think @ferullo was saying that the diagnostic alerts will respect the namespace setting, so they might come with something else than default.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Updated here: 2018132

ignore_unavailable: true,
size: this.maxQueueSize,
body: {
query: {
range: {
'event.ingested': {
gte: 'now-5m',
madirey marked this conversation as resolved.
Show resolved Hide resolved
lt: 'now',
},
},
},
sort: [
{
'event.ingested': {
order: 'asc',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want asc here so we get the most recent events?

Copy link
Contributor Author

@pjhampton pjhampton Dec 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got it wrong. Updated here 5638da9
desc will order by most recent I believe from my testing.

},
},
],
},
};

if (!this.core) {
throw Error('could not fetch diagnostic alerts. core is not available');
}
const callCluster = this.core.elasticsearch.legacy.client.callAsInternalUser;
return callCluster('search', query);
}

public queueTelemetryEvents(events: TelemetryEvent[]) {
const qlength = this.queue.length;

Expand Down Expand Up @@ -109,6 +160,11 @@ export class TelemetryEventsSender {
});
}

public async isTelemetryOptedIn() {
this.isOptedIn = await this.telemetryStart?.getIsOptedIn();
pjhampton marked this conversation as resolved.
Show resolved Hide resolved
return this.isOptedIn === true;
}

private async sendIfDue() {
if (this.isSending) {
return;
Expand All @@ -121,9 +177,7 @@ export class TelemetryEventsSender {
try {
this.isSending = true;

// 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();
this.isOptedIn = await this.isTelemetryOptedIn();
if (!this.isOptedIn) {
this.logger.debug(`Telemetry is not opted-in.`);
this.queue = [];
Expand Down Expand Up @@ -245,9 +299,14 @@ const allowlistEventFields: AllowlistFields = {
'@timestamp': true,
agent: true,
Endpoint: true,
Ransomware: true,
data_stream: true,
ecs: true,
elastic: true,
event: true,
rule: {
pjhampton marked this conversation as resolved.
Show resolved Hide resolved
ruleset: true,
},
file: {
name: true,
path: true,
Expand Down
104 changes: 104 additions & 0 deletions x-pack/plugins/security_solution/server/lib/telemetry/task.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* 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 { loggingSystemMock } from 'src/core/server/mocks';

import { taskManagerMock } from '../../../../task_manager/server/mocks';
import { TaskStatus } from '../../../../task_manager/server';

import { TelemetryDiagTask, TelemetryDiagTaskConstants } from './task';
import { createMockTelemetryEventsSender, MockTelemetryDiagnosticTask } from './mocks';

describe('test', () => {
let logger: ReturnType<typeof loggingSystemMock.createLogger>;

beforeEach(() => {
logger = loggingSystemMock.createLogger();
});

describe('basic diagnostic alert telemetry sanity checks', () => {
test('task can register', () => {
const telemetryDiagTask = new TelemetryDiagTask(
logger,
taskManagerMock.createSetup(),
createMockTelemetryEventsSender(true)
);

expect(telemetryDiagTask).toBeInstanceOf(TelemetryDiagTask);
});
});

test('diagnostic task should be registered', () => {
const mockTaskManager = taskManagerMock.createSetup();
new TelemetryDiagTask(logger, mockTaskManager, createMockTelemetryEventsSender(true));

expect(mockTaskManager.registerTaskDefinitions).toHaveBeenCalled();
});

test('task should be scheduled', async () => {
const mockTaskManagerSetup = taskManagerMock.createSetup();
const telemetryDiagTask = new TelemetryDiagTask(
logger,
mockTaskManagerSetup,
createMockTelemetryEventsSender(true)
);

const mockTaskManagerStart = taskManagerMock.createStart();
await telemetryDiagTask.start(mockTaskManagerStart);
expect(mockTaskManagerStart.ensureScheduled).toHaveBeenCalled();
});

test('task should run', async () => {
const mockContext = createMockTelemetryEventsSender(true);
const mockTaskManager = taskManagerMock.createSetup();
const telemetryDiagTask = new MockTelemetryDiagnosticTask(logger, mockTaskManager, mockContext);

const mockTaskInstance = {
id: TelemetryDiagTaskConstants.TYPE,
runAt: new Date(),
attempts: 0,
ownerId: '',
status: TaskStatus.Running,
startedAt: new Date(),
scheduledAt: new Date(),
retryAt: new Date(),
params: {},
state: {},
taskType: TelemetryDiagTaskConstants.TYPE,
};
const createTaskRunner =
mockTaskManager.registerTaskDefinitions.mock.calls[0][0][TelemetryDiagTaskConstants.TYPE]
.createTaskRunner;
const taskRunner = createTaskRunner({ taskInstance: mockTaskInstance });
await taskRunner.run();
expect(telemetryDiagTask.runTask).toHaveBeenCalled();
});

test('task should not query elastic if telemetry is not opted in', async () => {
const mockSender = createMockTelemetryEventsSender(false);
const mockTaskManager = taskManagerMock.createSetup();
const telemetryDiagTask = new MockTelemetryDiagnosticTask(logger, mockTaskManager, mockSender);

const mockTaskInstance = {
id: TelemetryDiagTaskConstants.TYPE,
runAt: new Date(),
attempts: 0,
ownerId: '',
status: TaskStatus.Running,
startedAt: new Date(),
scheduledAt: new Date(),
retryAt: new Date(),
params: {},
state: {},
taskType: TelemetryDiagTaskConstants.TYPE,
};
const createTaskRunner =
mockTaskManager.registerTaskDefinitions.mock.calls[0][0][TelemetryDiagTaskConstants.TYPE]
.createTaskRunner;
const taskRunner = createTaskRunner({ taskInstance: mockTaskInstance });
await taskRunner.run();
expect(mockSender.fetchDiagnosticAlerts).not.toHaveBeenCalled();
});
});
95 changes: 95 additions & 0 deletions x-pack/plugins/security_solution/server/lib/telemetry/task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* 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 { Logger } from 'src/core/server';
import {
ConcreteTaskInstance,
TaskManagerSetupContract,
TaskManagerStartContract,
} from '../../../../task_manager/server';
import { TelemetryEventsSender, TelemetryEvent } from './sender';

export const TelemetryDiagTaskConstants = {
TIMEOUT: '1m',
TYPE: 'security:endpoint-diagnostics',
INTERVAL: '5m',
VERSION: '1.0.0',
};

export class TelemetryDiagTask {
private readonly logger: Logger;
private readonly sender: TelemetryEventsSender;

constructor(
logger: Logger,
taskManager: TaskManagerSetupContract,
sender: TelemetryEventsSender
) {
this.logger = logger;
this.sender = sender;

taskManager.registerTaskDefinitions({
[TelemetryDiagTaskConstants.TYPE]: {
title: 'Security Solution Telemetry Diagnostics task',
timeout: TelemetryDiagTaskConstants.TIMEOUT,
createTaskRunner: ({ taskInstance }: { taskInstance: ConcreteTaskInstance }) => {
return {
run: async () => {
await this.runTask(taskInstance.id);
},
cancel: async () => {},
};
},
},
});
}

public start = async (taskManager: TaskManagerStartContract) => {
try {
await taskManager.ensureScheduled({
id: this.getTaskId(),
taskType: TelemetryDiagTaskConstants.TYPE,
scope: ['securitySolution'],
schedule: {
interval: TelemetryDiagTaskConstants.INTERVAL,
},
state: {},
params: { version: TelemetryDiagTaskConstants.VERSION },
});
} catch (e) {
this.logger.error(`Error scheduling task, received ${e.message}`);
}
};

private getTaskId = (): string => {
return `${TelemetryDiagTaskConstants.TYPE}:${TelemetryDiagTaskConstants.VERSION}`;
};

public runTask = async (taskId: string) => {
this.logger.debug(`Running task ${taskId}`);
if (taskId !== this.getTaskId()) {
this.logger.debug(`Outdated task running: ${taskId}`);
return;
}

const isOptedIn = await this.sender.isTelemetryOptedIn();
if (!isOptedIn) {
this.logger.debug(`Telemetry is not opted-in.`);
return;
}

const response = await this.sender.fetchDiagnosticAlerts();

const hits = response.hits?.hits || [];
if (!Array.isArray(hits) || !hits.length) {
this.logger.debug('no diagnostic alerts retrieved');
return;
}

const diagAlerts: TelemetryEvent[] = hits.map((h) => h._source);
this.logger.debug(`Received ${diagAlerts.length} diagnostic alerts`);
this.sender.queueTelemetryEvents(diagAlerts);
};
}
4 changes: 2 additions & 2 deletions x-pack/plugins/security_solution/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
);
});

this.telemetryEventsSender.setup(plugins.telemetry);
this.telemetryEventsSender.setup(plugins.telemetry, plugins.taskManager);

return {};
}
Expand Down Expand Up @@ -369,7 +369,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
this.logger.debug('User artifacts task not available.');
}

this.telemetryEventsSender.start(core, plugins.telemetry);
this.telemetryEventsSender.start(core, plugins.telemetry, plugins.taskManager);
this.licensing$ = plugins.licensing.license$;
licenseService.start(this.licensing$);
this.policyWatcher = new PolicyWatcher(
Expand Down