Skip to content

Commit 4d3ca34

Browse files
rylndelasticmachine
andcommitted
[Security Solution][Detections] Adoption telemetry (#71102)
* style: sort plugin interface * WIP: UsageCollector for Security Adoption This uses ML and raw ES calls to query our ML Jobs and Rules, and parse them into a format to be consumed by telemetry. Still to come: * initialization * tests * Initialize usage collectors during plugin setup * Rename usage key The service seems to convert colons to underscores, so let's just use an underscure. * Collector is ready if we have a kibana index * Refactor collector to generate options in a function This allows us to test our adherence to the collector API, focusing particularly on the fetch function. * Refactor usage collector in anticipation of endpoint data We're going to have our usage data under one key corresponding to the app, so this nests the existing data under a 'detections' key while allowing another fetching function to be plugged into the main collector under a separate key. * Update our collector to satisfy telemetry tooling * inlines collector options * inlines schema object * makes DetectionsUsage an interface instead of a type alias * Extracts telemetry mappings via scripts/telemetry_extract * Refactor detections usage logic to perform one loop instead of two We were previously performing two loops over each set of data: one to format it down to just the data we need, and another to convert that into usage data. We now perform both steps within a single loop. * Refactor detections telemetry to be nested * Extract new nested detections telemetry mappings Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
1 parent a46732a commit 4d3ca34

File tree

9 files changed

+643
-2
lines changed

9 files changed

+643
-2
lines changed

x-pack/plugins/security_solution/server/plugin.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
PluginInitializerContext,
1717
SavedObjectsClient,
1818
} from '../../../../src/core/server';
19+
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server';
1920
import { PluginSetupContract as AlertingSetup } from '../../alerts/server';
2021
import { SecurityPluginSetup as SecuritySetup } from '../../security/server';
2122
import { PluginSetupContract as FeaturesSetup } from '../../features/server';
@@ -46,17 +47,19 @@ import { ArtifactClient, ManifestManager } from './endpoint/services';
4647
import { EndpointAppContextService } from './endpoint/endpoint_app_context_services';
4748
import { EndpointAppContext } from './endpoint/types';
4849
import { registerDownloadExceptionListRoute } from './endpoint/routes/artifacts';
50+
import { initUsageCollectors } from './usage';
4951

5052
export interface SetupPlugins {
5153
alerts: AlertingSetup;
5254
encryptedSavedObjects?: EncryptedSavedObjectsSetup;
5355
features: FeaturesSetup;
5456
licensing: LicensingPluginSetup;
57+
lists?: ListPluginSetup;
58+
ml?: MlSetup;
5559
security?: SecuritySetup;
5660
spaces?: SpacesSetup;
5761
taskManager?: TaskManagerSetupContract;
58-
ml?: MlSetup;
59-
lists?: ListPluginSetup;
62+
usageCollection?: UsageCollectionSetup;
6063
}
6164

6265
export interface StartPlugins {
@@ -106,9 +109,15 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
106109
this.logger.debug('plugin setup');
107110

108111
const config = await this.config$.pipe(first()).toPromise();
112+
const globalConfig = await this.context.config.legacy.globalConfig$.pipe(first()).toPromise();
109113

110114
initSavedObjects(core.savedObjects);
111115
initUiSettings(core.uiSettings);
116+
initUsageCollectors({
117+
kibanaIndex: globalConfig.kibana.index,
118+
ml: plugins.ml,
119+
usageCollection: plugins.usageCollection,
120+
});
112121

113122
const endpointContext: EndpointAppContext = {
114123
logFactory: this.context.logger,
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { LegacyAPICaller } from '../../../../../src/core/server';
8+
import { CollectorDependencies } from './types';
9+
import { DetectionsUsage, fetchDetectionsUsage } from './detections';
10+
11+
export type RegisterCollector = (deps: CollectorDependencies) => void;
12+
export interface UsageData {
13+
detections: DetectionsUsage;
14+
}
15+
16+
export const registerCollector: RegisterCollector = ({ kibanaIndex, ml, usageCollection }) => {
17+
if (!usageCollection) {
18+
return;
19+
}
20+
21+
const collector = usageCollection.makeUsageCollector<UsageData>({
22+
type: 'security_solution',
23+
schema: {
24+
detections: {
25+
detection_rules: {
26+
custom: {
27+
enabled: { type: 'long' },
28+
disabled: { type: 'long' },
29+
},
30+
elastic: {
31+
enabled: { type: 'long' },
32+
disabled: { type: 'long' },
33+
},
34+
},
35+
ml_jobs: {
36+
custom: {
37+
enabled: { type: 'long' },
38+
disabled: { type: 'long' },
39+
},
40+
elastic: {
41+
enabled: { type: 'long' },
42+
disabled: { type: 'long' },
43+
},
44+
},
45+
},
46+
},
47+
isReady: () => kibanaIndex.length > 0,
48+
fetch: async (callCluster: LegacyAPICaller): Promise<UsageData> => ({
49+
detections: await fetchDetectionsUsage(kibanaIndex, callCluster, ml),
50+
}),
51+
});
52+
53+
usageCollection.registerCollector(collector);
54+
};
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
import { INTERNAL_IMMUTABLE_KEY } from '../../common/constants';
7+
8+
export const getMockJobSummaryResponse = () => [
9+
{
10+
id: 'linux_anomalous_network_activity_ecs',
11+
description:
12+
'SIEM Auditbeat: Looks for unusual processes using the network which could indicate command-and-control, lateral movement, persistence, or data exfiltration activity (beta)',
13+
groups: ['auditbeat', 'process', 'siem'],
14+
processed_record_count: 141889,
15+
memory_status: 'ok',
16+
jobState: 'opened',
17+
hasDatafeed: true,
18+
datafeedId: 'datafeed-linux_anomalous_network_activity_ecs',
19+
datafeedIndices: ['auditbeat-*'],
20+
datafeedState: 'started',
21+
latestTimestampMs: 1594085401911,
22+
earliestTimestampMs: 1593054845656,
23+
latestResultsTimestampMs: 1594085401911,
24+
isSingleMetricViewerJob: true,
25+
nodeName: 'node',
26+
},
27+
{
28+
id: 'linux_anomalous_network_port_activity_ecs',
29+
description:
30+
'SIEM Auditbeat: Looks for unusual destination port activity that could indicate command-and-control, persistence mechanism, or data exfiltration activity (beta)',
31+
groups: ['auditbeat', 'process', 'siem'],
32+
processed_record_count: 0,
33+
memory_status: 'ok',
34+
jobState: 'closed',
35+
hasDatafeed: true,
36+
datafeedId: 'datafeed-linux_anomalous_network_port_activity_ecs',
37+
datafeedIndices: ['auditbeat-*'],
38+
datafeedState: 'stopped',
39+
isSingleMetricViewerJob: true,
40+
},
41+
{
42+
id: 'other_job',
43+
description: 'a job that is custom',
44+
groups: ['auditbeat', 'process'],
45+
processed_record_count: 0,
46+
memory_status: 'ok',
47+
jobState: 'closed',
48+
hasDatafeed: true,
49+
datafeedId: 'datafeed-other',
50+
datafeedIndices: ['auditbeat-*'],
51+
datafeedState: 'stopped',
52+
isSingleMetricViewerJob: true,
53+
},
54+
{
55+
id: 'another_job',
56+
description: 'another job that is custom',
57+
groups: ['auditbeat', 'process'],
58+
processed_record_count: 0,
59+
memory_status: 'ok',
60+
jobState: 'opened',
61+
hasDatafeed: true,
62+
datafeedId: 'datafeed-another',
63+
datafeedIndices: ['auditbeat-*'],
64+
datafeedState: 'started',
65+
isSingleMetricViewerJob: true,
66+
},
67+
];
68+
69+
export const getMockListModulesResponse = () => [
70+
{
71+
id: 'siem_auditbeat',
72+
title: 'SIEM Auditbeat',
73+
description:
74+
'Detect suspicious network activity and unusual processes in Auditbeat data (beta).',
75+
type: 'Auditbeat data',
76+
logoFile: 'logo.json',
77+
defaultIndexPattern: 'auditbeat-*',
78+
query: {
79+
bool: {
80+
filter: [
81+
{
82+
term: {
83+
'agent.type': 'auditbeat',
84+
},
85+
},
86+
],
87+
},
88+
},
89+
jobs: [
90+
{
91+
id: 'linux_anomalous_network_activity_ecs',
92+
config: {
93+
job_type: 'anomaly_detector',
94+
description:
95+
'SIEM Auditbeat: Looks for unusual processes using the network which could indicate command-and-control, lateral movement, persistence, or data exfiltration activity (beta)',
96+
groups: ['siem', 'auditbeat', 'process'],
97+
analysis_config: {
98+
bucket_span: '15m',
99+
detectors: [
100+
{
101+
detector_description: 'rare by "process.name"',
102+
function: 'rare',
103+
by_field_name: 'process.name',
104+
},
105+
],
106+
influencers: ['host.name', 'process.name', 'user.name', 'destination.ip'],
107+
},
108+
allow_lazy_open: true,
109+
analysis_limits: {
110+
model_memory_limit: '64mb',
111+
},
112+
data_description: {
113+
time_field: '@timestamp',
114+
},
115+
},
116+
},
117+
{
118+
id: 'linux_anomalous_network_port_activity_ecs',
119+
config: {
120+
job_type: 'anomaly_detector',
121+
description:
122+
'SIEM Auditbeat: Looks for unusual destination port activity that could indicate command-and-control, persistence mechanism, or data exfiltration activity (beta)',
123+
groups: ['siem', 'auditbeat', 'network'],
124+
analysis_config: {
125+
bucket_span: '15m',
126+
detectors: [
127+
{
128+
detector_description: 'rare by "destination.port"',
129+
function: 'rare',
130+
by_field_name: 'destination.port',
131+
},
132+
],
133+
influencers: ['host.name', 'process.name', 'user.name', 'destination.ip'],
134+
},
135+
allow_lazy_open: true,
136+
analysis_limits: {
137+
model_memory_limit: '32mb',
138+
},
139+
data_description: {
140+
time_field: '@timestamp',
141+
},
142+
},
143+
},
144+
],
145+
datafeeds: [],
146+
kibana: {},
147+
},
148+
];
149+
150+
export const getMockRulesResponse = () => ({
151+
hits: {
152+
hits: [
153+
{ _source: { alert: { enabled: true, tags: [`${INTERNAL_IMMUTABLE_KEY}:true`] } } },
154+
{ _source: { alert: { enabled: true, tags: [`${INTERNAL_IMMUTABLE_KEY}:false`] } } },
155+
{ _source: { alert: { enabled: false, tags: [`${INTERNAL_IMMUTABLE_KEY}:true`] } } },
156+
{ _source: { alert: { enabled: true, tags: [`${INTERNAL_IMMUTABLE_KEY}:true`] } } },
157+
{ _source: { alert: { enabled: false, tags: [`${INTERNAL_IMMUTABLE_KEY}:false`] } } },
158+
{ _source: { alert: { enabled: false, tags: [`${INTERNAL_IMMUTABLE_KEY}:true`] } } },
159+
{ _source: { alert: { enabled: false, tags: [`${INTERNAL_IMMUTABLE_KEY}:true`] } } },
160+
],
161+
},
162+
});
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { LegacyAPICaller } from '../../../../../src/core/server';
8+
import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks';
9+
import { jobServiceProvider } from '../../../ml/server/models/job_service';
10+
import { DataRecognizer } from '../../../ml/server/models/data_recognizer';
11+
import { mlServicesMock } from '../lib/machine_learning/mocks';
12+
import {
13+
getMockJobSummaryResponse,
14+
getMockListModulesResponse,
15+
getMockRulesResponse,
16+
} from './detections.mocks';
17+
import { fetchDetectionsUsage } from './detections';
18+
19+
jest.mock('../../../ml/server/models/job_service');
20+
jest.mock('../../../ml/server/models/data_recognizer');
21+
22+
describe('Detections Usage', () => {
23+
describe('fetchDetectionsUsage()', () => {
24+
let callClusterMock: jest.Mocked<LegacyAPICaller>;
25+
let mlMock: ReturnType<typeof mlServicesMock.create>;
26+
27+
beforeEach(() => {
28+
callClusterMock = elasticsearchServiceMock.createLegacyClusterClient().callAsInternalUser;
29+
mlMock = mlServicesMock.create();
30+
});
31+
32+
it('returns zeroed counts if both calls are empty', async () => {
33+
const result = await fetchDetectionsUsage('', callClusterMock, mlMock);
34+
35+
expect(result).toEqual({
36+
detection_rules: {
37+
custom: {
38+
enabled: 0,
39+
disabled: 0,
40+
},
41+
elastic: {
42+
enabled: 0,
43+
disabled: 0,
44+
},
45+
},
46+
ml_jobs: {
47+
custom: {
48+
enabled: 0,
49+
disabled: 0,
50+
},
51+
elastic: {
52+
enabled: 0,
53+
disabled: 0,
54+
},
55+
},
56+
});
57+
});
58+
59+
it('tallies rules data given rules results', async () => {
60+
(callClusterMock as jest.Mock).mockResolvedValue(getMockRulesResponse());
61+
const result = await fetchDetectionsUsage('', callClusterMock, mlMock);
62+
63+
expect(result).toEqual(
64+
expect.objectContaining({
65+
detection_rules: {
66+
custom: {
67+
enabled: 1,
68+
disabled: 1,
69+
},
70+
elastic: {
71+
enabled: 2,
72+
disabled: 3,
73+
},
74+
},
75+
})
76+
);
77+
});
78+
79+
it('tallies jobs data given jobs results', async () => {
80+
const mockJobSummary = jest.fn().mockResolvedValue(getMockJobSummaryResponse());
81+
const mockListModules = jest.fn().mockResolvedValue(getMockListModulesResponse());
82+
(jobServiceProvider as jest.Mock).mockImplementation(() => ({
83+
jobsSummary: mockJobSummary,
84+
}));
85+
(DataRecognizer as jest.Mock).mockImplementation(() => ({
86+
listModules: mockListModules,
87+
}));
88+
89+
const result = await fetchDetectionsUsage('', callClusterMock, mlMock);
90+
91+
expect(result).toEqual(
92+
expect.objectContaining({
93+
ml_jobs: {
94+
custom: {
95+
enabled: 1,
96+
disabled: 1,
97+
},
98+
elastic: {
99+
enabled: 1,
100+
disabled: 1,
101+
},
102+
},
103+
})
104+
);
105+
});
106+
});
107+
});

0 commit comments

Comments
 (0)