Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
3 changes: 3 additions & 0 deletions x-pack/platform/test/plugin_api_integration/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
'--xpack.eventLog.logEntries=true',
'--xpack.eventLog.indexEntries=true',
'--xpack.task_manager.monitored_aggregated_stats_refresh_rate=5000',
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
'responseActionsTelemetryEnabled',
])}`,
`--xpack.stack_connectors.enableExperimental=${JSON.stringify([
'crowdstrikeConnectorOn',
'microsoftDefenderEndpointOn',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ export default function ({ getService }: FtrProviderContext) {
'security:telemetry-filterlist-artifact',
'security:telemetry-lists',
'security:telemetry-prebuilt-rule-alerts',
'security:telemetry-response-actions-rules',
'security:telemetry-timelines',
'session_cleanup',
'slo:bulk-delete-task',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ export const createMockTelemetryReceiver = (
fetchTrustedApplications: jest.fn(),
fetchEndpointList: jest.fn(),
fetchDetectionRules: jest.fn().mockReturnValue({ body: null }),
fetchResponseActionsRules: jest
.fn()
.mockReturnValue({ body: { aggregations: { actionTypes: {} } } }),
fetchEndpointMetadata: jest.fn().mockReturnValue(Promise.resolve(new Map())),
fetchTimelineAlerts: jest.fn().mockReturnValue(Promise.resolve(stubEndpointAlertResponse())),
buildProcessTree: jest.fn().mockReturnValue(processTreeResponse),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
createMockUsageCounter,
} from './__mocks__';
import { TelemetryEventsSender } from './sender';
import type { ExperimentalFeatures } from '../../../common';

jest.mock('axios');
jest.mock('./receiver');
Expand Down Expand Up @@ -1006,7 +1007,10 @@ describe('AsyncTelemetryEventsSender', () => {

describe('ITelemetryEventsSender integration', () => {
it('should send events using the async service', async () => {
const serviceV1 = new TelemetryEventsSender(loggingSystemMock.createLogger());
const serviceV1 = new TelemetryEventsSender(
loggingSystemMock.createLogger(),
{} as ExperimentalFeatures
Comment thread
szaffarano marked this conversation as resolved.
);

service.setup(DEFAULT_RETRY_CONFIG, DEFAULT_QUEUE_CONFIG, receiver, telemetryPluginSetup);
service.start(telemetryPluginStart);
Expand Down Expand Up @@ -1038,7 +1042,10 @@ describe('AsyncTelemetryEventsSender', () => {
const bufferTimeSpanMillis = initialTimeSpan * 10;
const events = ['e1', 'e2', 'e3'];
const expectedBody = events.map((e) => JSON.stringify(e)).join('\n');
const serviceV1 = new TelemetryEventsSender(loggingSystemMock.createLogger());
const serviceV1 = new TelemetryEventsSender(
loggingSystemMock.createLogger(),
{} as ExperimentalFeatures
);

serviceV1.setup(receiver, telemetryPluginSetup, undefined, telemetryUsageCounter, service);

Expand Down Expand Up @@ -1077,7 +1084,10 @@ describe('AsyncTelemetryEventsSender', () => {
...detectionAlertsBefore,
bufferTimeSpanMillis: 5001,
};
const serviceV1 = new TelemetryEventsSender(loggingSystemMock.createLogger());
const serviceV1 = new TelemetryEventsSender(
loggingSystemMock.createLogger(),
{} as ExperimentalFeatures
);

serviceV1.setup(receiver, telemetryPluginSetup, undefined, telemetryUsageCounter, service);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import type {
ExtraInfo,
ListTemplate,
Nullable,
ResponseActionsRuleTelemetryTemplate,
ResponseActionRules,
TelemetryEvent,
TimeFrame,
TimelineResult,
Expand Down Expand Up @@ -236,6 +238,34 @@ export const templateExceptionList = (
});
};

/**
* Constructs the response actions custom rule telemetry schema from a list of rule params
* */
export const responseActionsCustomRuleTelemetryData = (
responseActionsRules: ResponseActionRules,
clusterInfo: ESClusterInfo,
licenseInfo: Nullable<ESLicense>
): ResponseActionsRuleTelemetryTemplate => {
const baseTelemetryData: ResponseActionsRuleTelemetryTemplate = {
'@timestamp': moment().toISOString(),
cluster_uuid: clusterInfo.cluster_uuid,
cluster_name: clusterInfo.cluster_name,
license_id: licenseInfo?.uid,
response_actions_rules: {
endpoint: 0,
osquery: 0,
},
};

return {
...baseTelemetryData,
response_actions_rules: {
endpoint: responseActionsRules.endpoint,
osquery: responseActionsRules.osquery,
},
};
};

/**
* Convert counter label list to kebab case
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ import type {
} from '@kbn/fleet-plugin/server';
import type { ExceptionListClient } from '@kbn/lists-plugin/server';
import moment from 'moment';

import { RULE_SAVED_OBJECT_TYPE } from '@kbn/alerting-plugin/server';
import { DEFAULT_DIAGNOSTIC_INDEX_PATTERN } from '../../../common/endpoint/constants';
import type { ExperimentalFeatures } from '../../../common';
import type { EndpointAppContextService } from '../../endpoint/endpoint_app_context_services';
Expand Down Expand Up @@ -219,6 +221,13 @@ export interface ITelemetryReceiver {
>
>;

fetchResponseActionsRules(
executeFrom: string,
executeTo: string
): Promise<
TransportResult<SearchResponse<unknown, Record<string, AggregationsAggregate>>, unknown>
>;

fetchDetectionExceptionList(
listId: string,
ruleVersion: number
Expand Down Expand Up @@ -749,6 +758,78 @@ export class TelemetryReceiver implements ITelemetryReceiver {
return this.esClient().search<RuleSearchResult>(query, { meta: true });
}

/**
* Find elastic rules SOs which are the rules that have immutable set to true and are of a particular rule type
* @returns custom elastic rules SOs with response actions enabled
*/
public async fetchResponseActionsRules(executeFrom: string, executeTo: string) {
const query: SearchRequest = {
index: `${this.getIndexForType?.(RULE_SAVED_OBJECT_TYPE)}`,
ignore_unavailable: true,
size: 0, // no query results required - only aggregation quantity
from: 0,
query: {
bool: {
must: [
{
term: {
type: 'alert',
},
},
{
term: {
'alert.params.immutable': {
value: false,
},
},
},
{
term: {
'alert.enabled': {
value: true,
},
},
},
{
terms: {
'alert.consumer': ['siem', 'securitySolution'],
},
},
{
terms: {
'alert.params.responseActions.actionTypeId': ['.endpoint', '.osquery'],
},
},
{
range: {
'alert.updatedAt': {
gte: executeFrom,
lte: executeTo,
},
},
},
],
},
},
sort: [
{
'alert.updatedAt': {
order: 'desc',
},
},
],
aggs: {
actionTypes: {
terms: {
field: 'alert.params.responseActions.actionTypeId',
},
},
},
};

return this.esClient().search<unknown>(query, { meta: true });
}

public async fetchDetectionExceptionList(listId: string, ruleVersion: number) {
if (this?.exceptionListClient === undefined || this?.exceptionListClient === null) {
throw Error('exception list client is unavailable: could not retrieve trusted applications');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

/* eslint-disable dot-notation */
import type { ExperimentalFeatures } from '../../../common';
import { TelemetryEventsSender } from './sender';
import { loggingSystemMock } from '@kbn/core/server/mocks';
import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock';
Expand All @@ -25,13 +26,13 @@ describe('TelemetryEventsSender', () => {

describe('processEvents', () => {
it('returns empty array when empty array is passed', () => {
const sender = new TelemetryEventsSender(logger);
const sender = new TelemetryEventsSender(logger, {} as ExperimentalFeatures);
const result = sender.processEvents([]);
expect(result).toStrictEqual([]);
});

it('applies the allowlist', () => {
const sender = new TelemetryEventsSender(logger);
const sender = new TelemetryEventsSender(logger, {} as ExperimentalFeatures);
const input = [
{
credential_access: {
Expand Down Expand Up @@ -465,13 +466,13 @@ describe('TelemetryEventsSender', () => {

describe('queueTelemetryEvents', () => {
it('queues two events', () => {
const sender = new TelemetryEventsSender(logger);
const sender = new TelemetryEventsSender(logger, {} as ExperimentalFeatures);
sender.queueTelemetryEvents([{ 'event.kind': '1' }, { 'event.kind': '2' }]);
expect(sender['queue'].length).toBe(2);
});

it('queues more than maxQueueSize events', () => {
const sender = new TelemetryEventsSender(logger);
const sender = new TelemetryEventsSender(logger, {} as ExperimentalFeatures);
sender['maxQueueSize'] = 5;
sender.queueTelemetryEvents([{ 'event.kind': '1' }, { 'event.kind': '2' }]);
sender.queueTelemetryEvents([{ 'event.kind': '3' }, { 'event.kind': '4' }]);
Expand All @@ -481,7 +482,7 @@ describe('TelemetryEventsSender', () => {
});

it('empties the queue when sending', async () => {
const sender = new TelemetryEventsSender(logger);
const sender = new TelemetryEventsSender(logger, {} as ExperimentalFeatures);
sender['telemetryStart'] = {
getIsOptedIn: jest.fn(async () => true),
isOptedIn$: new Observable<boolean>(),
Expand Down Expand Up @@ -514,7 +515,7 @@ describe('TelemetryEventsSender', () => {
});

it("shouldn't send when telemetry is disabled", async () => {
const sender = new TelemetryEventsSender(logger);
const sender = new TelemetryEventsSender(logger, {} as ExperimentalFeatures);
sender['sendEvents'] = jest.fn();
const telemetryStart = {
getIsOptedIn: jest.fn(async () => false),
Expand All @@ -531,7 +532,7 @@ describe('TelemetryEventsSender', () => {
});

it("shouldn't send when telemetry when opted in but cannot connect to elastic telemetry services", async () => {
const sender = new TelemetryEventsSender(logger);
const sender = new TelemetryEventsSender(logger, {} as ExperimentalFeatures);
sender['sendEvents'] = jest.fn();
const telemetryStart = {
getIsOptedIn: jest.fn(async () => true),
Expand All @@ -558,28 +559,28 @@ describe('getV3UrlFromV2', () => {
});

it('should return prod url', () => {
const sender = new TelemetryEventsSender(logger);
const sender = new TelemetryEventsSender(logger, {} as ExperimentalFeatures);
expect(
sender.getV3UrlFromV2('https://telemetry.elastic.co/xpack/v2/send', 'alerts-endpoint')
).toBe('https://telemetry.elastic.co/v3/send/alerts-endpoint');
});

it('should work when receiving a V3 URL', () => {
const sender = new TelemetryEventsSender(logger);
const sender = new TelemetryEventsSender(logger, {} as ExperimentalFeatures);
expect(
sender.getV3UrlFromV2('https://telemetry.elastic.co/v3/send/channel', 'alerts-endpoint')
).toBe('https://telemetry.elastic.co/v3/send/alerts-endpoint');
});

it('should return staging url', () => {
const sender = new TelemetryEventsSender(logger);
const sender = new TelemetryEventsSender(logger, {} as ExperimentalFeatures);
expect(
sender.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', () => {
const sender = new TelemetryEventsSender(logger);
const sender = new TelemetryEventsSender(logger, {} as ExperimentalFeatures);
expect(
sender.getV3UrlFromV2('http://user:pass@myproxy.local:1337/xpack/v2/send', 'alerts-endpoint')
).toBe('http://user:pass@myproxy.local:1337/v3/send/alerts-endpoint');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
TaskManagerStartContract,
} from '@kbn/task-manager-plugin/server';
import { exhaustMap, Subject, takeUntil, timer } from 'rxjs';
import type { ExperimentalFeatures } from '../../../common';
import type { ITelemetryReceiver } from './receiver';
import { copyAllowlistedFields, filterList } from './filterlists';
import { createTelemetryTaskConfigs } from './tasks';
Expand Down Expand Up @@ -99,6 +100,7 @@ export class TelemetryEventsSender implements ITelemetryEventsSender {
private readonly initialCheckDelayMs = 10 * 1000;
private readonly checkIntervalMs = 60 * 1000;
private readonly logger: TelemetryLogger;
private readonly experimentalFeatures: ExperimentalFeatures;
private readonly stop$ = new Subject<void>();
private maxQueueSize = telemetryConfiguration.telemetry_max_buffer_size;
private telemetryStart?: TelemetryPluginStart;
Expand All @@ -116,7 +118,8 @@ export class TelemetryEventsSender implements ITelemetryEventsSender {

private asyncTelemetrySender?: IAsyncTelemetryEventsSender;

constructor(logger: Logger) {
constructor(logger: Logger, experimentalFeatures: ExperimentalFeatures) {
this.experimentalFeatures = experimentalFeatures;
this.logger = newTelemetryLogger(logger.get('telemetry_events.sender'));
}

Expand All @@ -131,7 +134,7 @@ export class TelemetryEventsSender implements ITelemetryEventsSender {
this.telemetryUsageCounter = telemetryUsageCounter;
if (taskManager) {
const taskMetricsService = new TaskMetricsService(this.logger, this);
this.telemetryTasks = createTelemetryTaskConfigs().map(
this.telemetryTasks = createTelemetryTaskConfigs(this.experimentalFeatures).map(
(config: SecurityTelemetryTaskConfig) => {
const task = new SecurityTelemetryTask(
config,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { loggingSystemMock } from '@kbn/core/server/mocks';
import { createTelemetryCustomResponseActionRulesTaskConfig } from './custom_response_actions_rule';
import {
createMockTelemetryEventsSender,
createMockTelemetryReceiver,
createMockTaskMetrics,
} from '../__mocks__';

describe('security response actions rule task test', () => {
let logger: ReturnType<typeof loggingSystemMock.createLogger>;

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

test('security response actions rule task should fetch response actions rules data', async () => {
const testTaskExecutionPeriod = {
last: undefined,
current: new Date().toISOString(),
};
const mockTelemetryEventsSender = createMockTelemetryEventsSender();
const mockTelemetryReceiver = createMockTelemetryReceiver();
const telemetryCustomResponseActionsRulesTaskConfig =
createTelemetryCustomResponseActionRulesTaskConfig(1);
const mockTaskMetrics = createMockTaskMetrics();

await telemetryCustomResponseActionsRulesTaskConfig.runTask(
'test-id',
logger,
mockTelemetryReceiver,
mockTelemetryEventsSender,
mockTaskMetrics,
testTaskExecutionPeriod
);

expect(mockTelemetryReceiver.fetchResponseActionsRules).toHaveBeenCalled();
});
});
Loading
Loading