Skip to content

Commit

Permalink
[Security] Alert Telemetry for the Security app (#77200)
Browse files Browse the repository at this point in the history
This adds a `TelemetryEventsSender` component that can be used to publish Endpoint alerts to our Telemetry service. The alerts are filtered by a set of allowed fields (for PII) and batched in a queue to be sent once per minute. There is a cap of 100 alerts per minute to be sent. The component respects the telemetry opt-in status and enriches the alerts with the cluster ID and name.

The Detection Engine is slightly modified to send endpoint telemetry events via the `TelemetryEventsSender`. Only the "custom query" rule type is modified because that's the only one that can create Endpoint Alerts.

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: Garrett Spong <spong@users.noreply.github.com>
  • Loading branch information
3 people authored Sep 30, 2020
1 parent 935f634 commit 49c8ff3
Show file tree
Hide file tree
Showing 14 changed files with 787 additions and 1 deletion.
3 changes: 2 additions & 1 deletion x-pack/plugins/security_solution/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"spaces",
"usageCollection",
"lists",
"home"
"home",
"telemetry"
],
"server": true,
"ui": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ describe('searchAfterAndBulkCreate', () => {
exceptionsList: [exceptionItem],
services: mockService,
logger: mockLogger,
eventsTelemetry: undefined,
id: sampleRuleGuid,
inputIndexPattern,
signalsIndex: DEFAULT_SIGNALS_INDEX,
Expand Down Expand Up @@ -255,6 +256,7 @@ describe('searchAfterAndBulkCreate', () => {
exceptionsList: [exceptionItem],
services: mockService,
logger: mockLogger,
eventsTelemetry: undefined,
id: sampleRuleGuid,
inputIndexPattern,
signalsIndex: DEFAULT_SIGNALS_INDEX,
Expand Down Expand Up @@ -334,6 +336,7 @@ describe('searchAfterAndBulkCreate', () => {
exceptionsList: [exceptionItem],
services: mockService,
logger: mockLogger,
eventsTelemetry: undefined,
id: sampleRuleGuid,
inputIndexPattern,
signalsIndex: DEFAULT_SIGNALS_INDEX,
Expand Down Expand Up @@ -394,6 +397,7 @@ describe('searchAfterAndBulkCreate', () => {
exceptionsList: [exceptionItem],
services: mockService,
logger: mockLogger,
eventsTelemetry: undefined,
id: sampleRuleGuid,
inputIndexPattern,
signalsIndex: DEFAULT_SIGNALS_INDEX,
Expand Down Expand Up @@ -452,6 +456,7 @@ describe('searchAfterAndBulkCreate', () => {
exceptionsList: [exceptionItem],
services: mockService,
logger: mockLogger,
eventsTelemetry: undefined,
id: sampleRuleGuid,
inputIndexPattern,
signalsIndex: DEFAULT_SIGNALS_INDEX,
Expand Down Expand Up @@ -535,6 +540,7 @@ describe('searchAfterAndBulkCreate', () => {
exceptionsList: [exceptionItem],
services: mockService,
logger: mockLogger,
eventsTelemetry: undefined,
id: sampleRuleGuid,
inputIndexPattern,
signalsIndex: DEFAULT_SIGNALS_INDEX,
Expand Down Expand Up @@ -615,6 +621,7 @@ describe('searchAfterAndBulkCreate', () => {
exceptionsList: [],
services: mockService,
logger: mockLogger,
eventsTelemetry: undefined,
id: sampleRuleGuid,
inputIndexPattern,
signalsIndex: DEFAULT_SIGNALS_INDEX,
Expand Down Expand Up @@ -664,6 +671,7 @@ describe('searchAfterAndBulkCreate', () => {
ruleParams: sampleParams,
services: mockService,
logger: mockLogger,
eventsTelemetry: undefined,
id: sampleRuleGuid,
inputIndexPattern,
signalsIndex: DEFAULT_SIGNALS_INDEX,
Expand Down Expand Up @@ -719,6 +727,7 @@ describe('searchAfterAndBulkCreate', () => {
ruleParams: sampleParams,
services: mockService,
logger: mockLogger,
eventsTelemetry: undefined,
id: sampleRuleGuid,
inputIndexPattern,
signalsIndex: DEFAULT_SIGNALS_INDEX,
Expand Down Expand Up @@ -790,6 +799,7 @@ describe('searchAfterAndBulkCreate', () => {
ruleParams: sampleParams,
services: mockService,
logger: mockLogger,
eventsTelemetry: undefined,
id: sampleRuleGuid,
inputIndexPattern,
signalsIndex: DEFAULT_SIGNALS_INDEX,
Expand Down Expand Up @@ -898,6 +908,7 @@ describe('searchAfterAndBulkCreate', () => {
exceptionsList: [],
services: mockService,
logger: mockLogger,
eventsTelemetry: undefined,
id: sampleRuleGuid,
inputIndexPattern,
signalsIndex: DEFAULT_SIGNALS_INDEX,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import { singleSearchAfter } from './single_search_after';
import { singleBulkCreate } from './single_bulk_create';
import { filterEventsAgainstList } from './filter_events_with_list';
import { sendAlertTelemetryEvents } from './send_telemetry_events';
import {
createSearchAfterReturnType,
createSearchAfterReturnTypeFromResponse,
Expand All @@ -25,6 +26,7 @@ export const searchAfterAndBulkCreate = async ({
services,
listClient,
logger,
eventsTelemetry,
id,
inputIndexPattern,
signalsIndex,
Expand Down Expand Up @@ -188,6 +190,14 @@ export const searchAfterAndBulkCreate = async ({
logger.debug(
buildRuleMessage(`filteredEvents.hits.hits: ${filteredEvents.hits.hits.length}`)
);

sendAlertTelemetryEvents(
logger,
eventsTelemetry,
filteredEvents,
ruleParams,
buildRuleMessage
);
}

// we are guaranteed to have searchResult hits at this point
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* 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';

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',
data_stream: {
dataset: 'endpoint.events',
},
},
},
{
_index: 'x',
_type: 'x',
_id: 'x',
_score: 0,
_source: {
'@timestamp': 'x',
key2: 'hello',
data_stream: {
dataset: 'endpoint.alerts',
other: 'x',
},
},
},
{
_index: 'x',
_type: 'x',
_id: 'x',
_score: 0,
_source: {
'@timestamp': 'x',
key3: 'hello',
data_stream: {},
},
},
],
},
};

const sources = selectEvents(filteredEvents);
expect(sources).toStrictEqual([
{
'@timestamp': 'x',
key2: 'hello',
data_stream: {
dataset: 'endpoint.alerts',
other: 'x',
},
},
]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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, 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 interface SearchResultWithSource {
_source: SignalSource;
}

export function selectEvents(filteredEvents: SignalSearchResponse): TelemetryEvent[] {
const sources = filteredEvents.hits.hits.map(function (
obj: SearchResultWithSource
): TelemetryEvent {
return obj._source;
});

// Filter out non-endpoint alerts
return sources.filter((obj: TelemetryEvent) => obj.data_stream?.dataset === 'endpoint.alerts');
}

export function sendAlertTelemetryEvents(
logger: Logger,
eventsTelemetry: TelemetryEventsSender | undefined,
filteredEvents: SignalSearchResponse,
ruleParams: RuleTypeParams,
buildRuleMessage: BuildRuleMessage
) {
if (eventsTelemetry === undefined) {
return;
}

const sources = selectEvents(filteredEvents);

try {
eventsTelemetry.queueTelemetryEvents(sources);
} catch (exc) {
logger.error(buildRuleMessage(`[-] queing telemetry events failed ${exc}`));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ describe('rules_notification_alert_type', () => {

alert = signalRulesAlertType({
logger,
eventsTelemetry: undefined,
version,
ml: mlMock,
lists: listMock.createSetup(),
Expand Down Expand Up @@ -344,6 +345,7 @@ describe('rules_notification_alert_type', () => {
payload = getPayload(ruleAlert, alertServices) as jest.Mocked<RuleExecutorOptions>;
alert = signalRulesAlertType({
logger,
eventsTelemetry: undefined,
version,
ml: undefined,
lists: undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,18 +56,21 @@ 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 '../../telemetry/sender';
import { buildEqlSearchRequest } from '../../../../common/detection_engine/get_query_filter';
import { bulkInsertSignals } from './single_bulk_create';
import { buildSignalFromEvent, buildSignalGroupFromSequence } from './build_bulk_body';
import { createThreatSignals } from './threat_mapping/create_threat_signals';

export const signalRulesAlertType = ({
logger,
eventsTelemetry,
version,
ml,
lists,
}: {
logger: Logger;
eventsTelemetry: TelemetryEventsSender | undefined;
version: string;
ml: SetupPlugins['ml'];
lists: SetupPlugins['lists'] | undefined;
Expand Down Expand Up @@ -369,6 +372,7 @@ export const signalRulesAlertType = ({
previousStartedAt,
listClient,
logger,
eventsTelemetry,
alertId,
outputIndex,
params,
Expand Down Expand Up @@ -409,6 +413,7 @@ export const signalRulesAlertType = ({
ruleParams: params,
services,
logger,
eventsTelemetry,
id: alertId,
inputIndexPattern: inputIndex,
signalsIndex: outputIndex,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const createThreatSignal = async ({
previousStartedAt,
listClient,
logger,
eventsTelemetry,
alertId,
outputIndex,
params,
Expand Down Expand Up @@ -77,6 +78,7 @@ export const createThreatSignal = async ({
ruleParams: params,
services,
logger,
eventsTelemetry,
id: alertId,
inputIndexPattern: inputIndex,
signalsIndex: outputIndex,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const createThreatSignals = async ({
previousStartedAt,
listClient,
logger,
eventsTelemetry,
alertId,
outputIndex,
params,
Expand Down Expand Up @@ -79,6 +80,7 @@ export const createThreatSignals = async ({
previousStartedAt,
listClient,
logger,
eventsTelemetry,
alertId,
outputIndex,
params,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -38,6 +39,7 @@ export interface CreateThreatSignalsOptions {
previousStartedAt: Date | null;
listClient: ListClient;
logger: Logger;
eventsTelemetry: TelemetryEventsSender | undefined;
alertId: string;
outputIndex: string;
params: RuleTypeParams;
Expand Down Expand Up @@ -73,6 +75,7 @@ export interface CreateThreatSignalOptions {
previousStartedAt: Date | null;
listClient: ListClient;
logger: Logger;
eventsTelemetry: TelemetryEventsSender | undefined;
alertId: string;
outputIndex: string;
params: RuleTypeParams;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -211,6 +212,7 @@ export interface SearchAfterAndBulkCreateParams {
listClient: ListClient;
exceptionsList: ExceptionListItemSchema[];
logger: Logger;
eventsTelemetry: TelemetryEventsSender | undefined;
id: string;
inputIndexPattern: string[];
signalsIndex: string;
Expand Down
Loading

0 comments on commit 49c8ff3

Please sign in to comment.