mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[SecruitySolution][Endpoint][ResponseActions] Add usage telemtery for response actions rules (#221518)
## Summary Adds usage telemetry to collect usage info on rules that have response actions configured. > [!Note] > The changes in the PR do not collect sensitive user info. ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) ### Identify risks Does this PR introduce any risks? For example, consider risks like hard to test bugs, performance regression, potential of data loss. Describe the risk, its severity, and mitigation for each identified risk. Invite stakeholders and evaluate how to proceed before merging. - [ ] [See some risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) - [ ] ...
This commit is contained in:
parent
7081eea795
commit
92df41a533
13 changed files with 389 additions and 19 deletions
|
@ -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',
|
||||
|
|
|
@ -191,6 +191,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',
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
createMockUsageCounter,
|
||||
} from './__mocks__';
|
||||
import { TelemetryEventsSender } from './sender';
|
||||
import type { ExperimentalFeatures } from '../../../common';
|
||||
|
||||
jest.mock('axios');
|
||||
jest.mock('./receiver');
|
||||
|
@ -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
|
||||
);
|
||||
|
||||
service.setup(DEFAULT_RETRY_CONFIG, DEFAULT_QUEUE_CONFIG, receiver, telemetryPluginSetup);
|
||||
service.start(telemetryPluginStart);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
|
|
@ -24,6 +24,8 @@ import type {
|
|||
ExtraInfo,
|
||||
ListTemplate,
|
||||
Nullable,
|
||||
ResponseActionsRuleTelemetryTemplate,
|
||||
ResponseActionRules,
|
||||
TelemetryEvent,
|
||||
TimeFrame,
|
||||
TimelineResult,
|
||||
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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';
|
||||
|
@ -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
|
||||
|
@ -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');
|
||||
|
|
|
@ -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';
|
||||
|
@ -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: {
|
||||
|
@ -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' }]);
|
||||
|
@ -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>(),
|
||||
|
@ -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),
|
||||
|
@ -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),
|
||||
|
@ -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');
|
||||
|
|
|
@ -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';
|
||||
|
@ -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;
|
||||
|
@ -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'));
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* 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 { cloneDeep } from 'lodash';
|
||||
import type { Logger } from '@kbn/core/server';
|
||||
import {
|
||||
batchTelemetryRecords,
|
||||
responseActionsCustomRuleTelemetryData,
|
||||
newTelemetryLogger,
|
||||
createUsageCounterLabel,
|
||||
safeValue,
|
||||
} from '../helpers';
|
||||
import type { ITelemetryEventsSender } from '../sender';
|
||||
import type { ITelemetryReceiver } from '../receiver';
|
||||
import {
|
||||
TelemetryChannel,
|
||||
type ResponseActionRules,
|
||||
type ResponseActionsRuleResponseAggregations,
|
||||
} from '../types';
|
||||
import type { TaskExecutionPeriod } from '../task';
|
||||
import type { ITaskMetricsService } from '../task_metrics.types';
|
||||
import { telemetryConfiguration } from '../configuration';
|
||||
|
||||
export function createTelemetryCustomResponseActionRulesTaskConfig(maxTelemetryBatch: number) {
|
||||
const taskName = 'Security Solution Response Actions Rules Telemetry';
|
||||
const taskType = 'security:telemetry-response-actions-rules';
|
||||
return {
|
||||
type: taskType,
|
||||
title: taskName,
|
||||
interval: '24h',
|
||||
timeout: '10m',
|
||||
version: '1.0.0',
|
||||
runTask: async (
|
||||
taskId: string,
|
||||
logger: Logger,
|
||||
receiver: ITelemetryReceiver,
|
||||
sender: ITelemetryEventsSender,
|
||||
taskMetricsService: ITaskMetricsService,
|
||||
taskExecutionPeriod: TaskExecutionPeriod
|
||||
) => {
|
||||
const mdc = { task_id: taskId, task_execution_period: taskExecutionPeriod };
|
||||
const log = newTelemetryLogger(logger.get('response_actions_rules'), mdc);
|
||||
const usageCollector = sender.getTelemetryUsageCluster();
|
||||
const usageLabelEndpointPrefix: string[] = [
|
||||
'security_telemetry',
|
||||
'endpoint-response-actions-rules',
|
||||
];
|
||||
const usageLabelOsqueryPrefix: string[] = [
|
||||
'security_telemetry',
|
||||
'osquery-response-actions-rules',
|
||||
];
|
||||
const trace = taskMetricsService.start(taskType);
|
||||
|
||||
log.l('Running response actions rules telemetry task');
|
||||
|
||||
try {
|
||||
const [clusterInfoPromise, licenseInfoPromise] = await Promise.allSettled([
|
||||
receiver.fetchClusterInfo(),
|
||||
receiver.fetchLicenseInfo(),
|
||||
]);
|
||||
|
||||
const clusterInfo = safeValue(clusterInfoPromise);
|
||||
const licenseInfo = safeValue(licenseInfoPromise);
|
||||
|
||||
const {
|
||||
body: { aggregations },
|
||||
} = await receiver.fetchResponseActionsRules(
|
||||
taskExecutionPeriod.last ?? 'now-24h',
|
||||
taskExecutionPeriod.current
|
||||
);
|
||||
|
||||
if (!aggregations || !aggregations.actionTypes) {
|
||||
log.debug('no custom response action rules found');
|
||||
await taskMetricsService.end(trace);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const responseActionRules = (
|
||||
aggregations as unknown as ResponseActionsRuleResponseAggregations
|
||||
).actionTypes.buckets.reduce<ResponseActionRules>(
|
||||
(acc, agg) => {
|
||||
if (agg.key === '.endpoint') {
|
||||
acc.endpoint = agg.doc_count;
|
||||
} else if (agg.key === '.osquery') {
|
||||
acc.osquery = agg.doc_count;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{ endpoint: 0, osquery: 0 }
|
||||
);
|
||||
|
||||
const shouldNotProcessTelemetry =
|
||||
responseActionRules.endpoint === 0 || responseActionRules.osquery === 0;
|
||||
|
||||
if (shouldNotProcessTelemetry) {
|
||||
log.debug('no new custom response action rules found');
|
||||
await taskMetricsService.end(trace);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const responseActionsRulesTelemetryData = responseActionsCustomRuleTelemetryData(
|
||||
responseActionRules,
|
||||
clusterInfo,
|
||||
licenseInfo
|
||||
);
|
||||
|
||||
log.l('Custom response actions rules data', {
|
||||
data: JSON.stringify(responseActionsRulesTelemetryData),
|
||||
});
|
||||
|
||||
usageCollector?.incrementCounter({
|
||||
counterName: createUsageCounterLabel(usageLabelEndpointPrefix),
|
||||
counterType: 'response_actions_endpoint_rules_count',
|
||||
incrementBy: responseActionsRulesTelemetryData.response_actions_rules.endpoint,
|
||||
});
|
||||
|
||||
usageCollector?.incrementCounter({
|
||||
counterName: createUsageCounterLabel(usageLabelOsqueryPrefix),
|
||||
counterType: 'response_actions_osquery_rules_count',
|
||||
incrementBy: responseActionsRulesTelemetryData.response_actions_rules.osquery,
|
||||
});
|
||||
|
||||
const documents = cloneDeep(Object.values(responseActionsRulesTelemetryData));
|
||||
|
||||
if (telemetryConfiguration.use_async_sender) {
|
||||
await sender.sendAsync(TelemetryChannel.LISTS, documents);
|
||||
} else {
|
||||
const batches = batchTelemetryRecords(documents, maxTelemetryBatch);
|
||||
for (const batch of batches) {
|
||||
await sender.sendOnDemand(TelemetryChannel.LISTS, batch);
|
||||
}
|
||||
}
|
||||
|
||||
await taskMetricsService.end(trace);
|
||||
|
||||
const totalCount = Object.values(
|
||||
responseActionsRulesTelemetryData.response_actions_rules
|
||||
).reduce((acc, count) => acc + count, 0);
|
||||
|
||||
log.l('Response actions rules telemetry task executed', {
|
||||
totalCount,
|
||||
});
|
||||
|
||||
return totalCount;
|
||||
} catch (err) {
|
||||
await taskMetricsService.end(trace, err);
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ExperimentalFeatures } from '../../../../common';
|
||||
import type { SecurityTelemetryTaskConfig } from '../task';
|
||||
import { createTelemetryDiagnosticsTaskConfig } from './diagnostic';
|
||||
import { createTelemetryEndpointTaskConfig } from './endpoint';
|
||||
|
@ -18,9 +19,12 @@ import { telemetryConfiguration } from '../configuration';
|
|||
import { createTelemetryFilterListArtifactTaskConfig } from './filterlists';
|
||||
import { createTelemetryIndicesMetadataTaskConfig } from './indices.metadata';
|
||||
import { createIngestStatsTaskConfig } from './ingest_pipelines_stats';
|
||||
import { createTelemetryCustomResponseActionRulesTaskConfig } from './custom_response_actions_rule';
|
||||
|
||||
export function createTelemetryTaskConfigs(): SecurityTelemetryTaskConfig[] {
|
||||
return [
|
||||
export function createTelemetryTaskConfigs(
|
||||
experimentalFeatures: ExperimentalFeatures
|
||||
): SecurityTelemetryTaskConfig[] {
|
||||
const tasks = [
|
||||
createTelemetryDiagnosticsTaskConfig(),
|
||||
createTelemetryEndpointTaskConfig(telemetryConfiguration.max_security_list_telemetry_batch),
|
||||
createTelemetrySecurityListTaskConfig(telemetryConfiguration.max_endpoint_telemetry_batch),
|
||||
|
@ -35,4 +39,14 @@ export function createTelemetryTaskConfigs(): SecurityTelemetryTaskConfig[] {
|
|||
createTelemetryIndicesMetadataTaskConfig(),
|
||||
createIngestStatsTaskConfig(),
|
||||
];
|
||||
|
||||
if (experimentalFeatures.responseActionsTelemetryEnabled) {
|
||||
tasks.push(
|
||||
createTelemetryCustomResponseActionRulesTaskConfig(
|
||||
telemetryConfiguration.max_detection_rule_telemetry_batch
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return tasks;
|
||||
}
|
||||
|
|
|
@ -386,6 +386,27 @@ interface ExceptionListEntry {
|
|||
namespace_type: string;
|
||||
}
|
||||
|
||||
export interface ResponseActionsRuleResponseAggregations {
|
||||
actionTypes: {
|
||||
buckets: Array<{
|
||||
key: '.endpoint' | '.osquery';
|
||||
doc_count: number;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ResponseActionsRuleTelemetryTemplate {
|
||||
'@timestamp': string;
|
||||
cluster_uuid: string;
|
||||
cluster_name: string;
|
||||
license_id: string | undefined;
|
||||
response_actions_rules: ResponseActionRules;
|
||||
}
|
||||
|
||||
export interface ResponseActionRules {
|
||||
endpoint: number;
|
||||
osquery: number;
|
||||
}
|
||||
interface DetectionRuleParms {
|
||||
ruleId: string;
|
||||
version: number;
|
||||
|
|
|
@ -187,7 +187,10 @@ export class Plugin implements ISecuritySolutionPlugin {
|
|||
);
|
||||
|
||||
this.ruleMonitoringService = createRuleMonitoringService(this.config, this.logger);
|
||||
this.telemetryEventsSender = new TelemetryEventsSender(this.logger);
|
||||
this.telemetryEventsSender = new TelemetryEventsSender(
|
||||
this.logger,
|
||||
this.config.experimentalFeatures
|
||||
);
|
||||
this.asyncTelemetryEventsSender = new AsyncTelemetryEventsSender(this.logger);
|
||||
this.telemetryReceiver = new TelemetryReceiver(this.logger);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue