Skip to content

Commit

Permalink
[7.13][Telemetry] Detection Rule Adoption (elastic#95659)
Browse files Browse the repository at this point in the history
* pushing initial experiments.

* Add name, version tags.

* Get alert count.

* Include rule type.

* Fetch cases count.

* Get all data sources working together.

* Stage work.

* Add detection adoption metrics.

* Add usage collector schema.

* Add usage collector schema.

* Update telemetry schema.

* Use let instead of const

* Fix spelling on array key.

* Update telemetry schema.

* Add unit tests.

* Fix type.

* Move types to index.

* Bug fix

* Update telemetry schema.

* Pass in signals index.

* Opps. Broke tests.

* Update.

* Fix types.

* Reflect @FrankHassanabad feedback in PR.

* Separate metric / usage telemetry code for complexity reduction.

* Add first e2e jest test.

* Add some additional tests for custom cases.

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
2 people authored and madirey committed May 11, 2021
1 parent 9f2bed6 commit 4154d68
Show file tree
Hide file tree
Showing 11 changed files with 1,553 additions and 130 deletions.
1 change: 1 addition & 0 deletions x-pack/plugins/security_solution/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
core,
endpointAppContext: endpointContext,
kibanaIndex: globalConfig.kibana.index,
signalsIndex: config.signalsIndex,
ml: plugins.ml,
usageCollection: plugins.usageCollection,
});
Expand Down
162 changes: 160 additions & 2 deletions x-pack/plugins/security_solution/server/usage/collector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const registerCollector: RegisterCollector = ({
core,
endpointAppContext,
kibanaIndex,
signalsIndex,
ml,
usageCollection,
}) => {
Expand Down Expand Up @@ -65,6 +66,163 @@ export const registerCollector: RegisterCollector = ({
},
},
detectionMetrics: {
detection_rules: {
detection_rule_usage: {
query: {
enabled: { type: 'long', _meta: { description: 'Number of query rules enabled' } },
disabled: { type: 'long', _meta: { description: 'Number of query rules disabled' } },
alerts: {
type: 'long',
_meta: { description: 'Number of alerts generated by query rules' },
},
cases: {
type: 'long',
_meta: { description: 'Number of cases attached to query detection rule alerts' },
},
},
threshold: {
enabled: {
type: 'long',
_meta: { description: 'Number of threshold rules enabled' },
},
disabled: {
type: 'long',
_meta: { description: 'Number of threshold rules disabled' },
},
alerts: {
type: 'long',
_meta: { description: 'Number of alerts generated by threshold rules' },
},
cases: {
type: 'long',
_meta: {
description: 'Number of cases attached to threshold detection rule alerts',
},
},
},
eql: {
enabled: { type: 'long', _meta: { description: 'Number of eql rules enabled' } },
disabled: { type: 'long', _meta: { description: 'Number of eql rules disabled' } },
alerts: {
type: 'long',
_meta: { description: 'Number of alerts generated by eql rules' },
},
cases: {
type: 'long',
_meta: { description: 'Number of cases attached to eql detection rule alerts' },
},
},
machine_learning: {
enabled: {
type: 'long',
_meta: { description: 'Number of machine_learning rules enabled' },
},
disabled: {
type: 'long',
_meta: { description: 'Number of machine_learning rules disabled' },
},
alerts: {
type: 'long',
_meta: { description: 'Number of alerts generated by machine_learning rules' },
},
cases: {
type: 'long',
_meta: {
description: 'Number of cases attached to machine_learning detection rule alerts',
},
},
},
threat_match: {
enabled: {
type: 'long',
_meta: { description: 'Number of threat_match rules enabled' },
},
disabled: {
type: 'long',
_meta: { description: 'Number of threat_match rules disabled' },
},
alerts: {
type: 'long',
_meta: { description: 'Number of alerts generated by threat_match rules' },
},
cases: {
type: 'long',
_meta: {
description: 'Number of cases attached to threat_match detection rule alerts',
},
},
},
elastic_total: {
enabled: { type: 'long', _meta: { description: 'Number of elastic rules enabled' } },
disabled: {
type: 'long',
_meta: { description: 'Number of elastic rules disabled' },
},
alerts: {
type: 'long',
_meta: { description: 'Number of alerts generated by elastic rules' },
},
cases: {
type: 'long',
_meta: { description: 'Number of cases attached to elastic detection rule alerts' },
},
},
custom_total: {
enabled: { type: 'long', _meta: { description: 'Number of custom rules enabled' } },
disabled: { type: 'long', _meta: { description: 'Number of custom rules disabled' } },
alerts: {
type: 'long',
_meta: { description: 'Number of alerts generated by custom rules' },
},
cases: {
type: 'long',
_meta: { description: 'Number of cases attached to custom detection rule alerts' },
},
},
},
detection_rule_detail: {
type: 'array',
items: {
rule_name: {
type: 'keyword',
_meta: { description: 'The name of the detection rule' },
},
rule_id: {
type: 'keyword',
_meta: { description: 'The UUID id of the detection rule' },
},
rule_type: {
type: 'keyword',
_meta: { description: 'The type of detection rule. ie eql, query...' },
},
rule_version: { type: 'long', _meta: { description: 'The version of the rule' } },
enabled: {
type: 'boolean',
_meta: { description: 'If the detection rule has been enabled by the user' },
},
elastic_rule: {
type: 'boolean',
_meta: { description: 'If the detection rule has been authored by Elastic' },
},
created_on: {
type: 'keyword',
_meta: { description: 'When the detection rule was created on the cluster' },
},
updated_on: {
type: 'keyword',
_meta: { description: 'When the detection rule was updated on the cluster' },
},
alert_count_daily: {
type: 'long',
_meta: { description: 'The number of daily alerts generated by a rule' },
},
cases_count_daily: {
type: 'long',
_meta: { description: 'The number of daily cases generated by a rule' },
},
},
},
},
ml_jobs: {
type: 'array',
items: {
Expand Down Expand Up @@ -132,13 +290,13 @@ export const registerCollector: RegisterCollector = ({
},
},
},
isReady: () => kibanaIndex.length > 0,
isReady: () => true,
fetch: async ({ esClient }: CollectorFetchContext): Promise<UsageData> => {
const internalSavedObjectsClient = await getInternalSavedObjectsClient(core);
const savedObjectsClient = (internalSavedObjectsClient as unknown) as SavedObjectsClientContract;
const [detections, detectionMetrics, endpoints] = await Promise.allSettled([
fetchDetectionsUsage(kibanaIndex, esClient, ml, savedObjectsClient),
fetchDetectionsMetrics(ml, savedObjectsClient),
fetchDetectionsMetrics(kibanaIndex, signalsIndex, esClient, ml, savedObjectsClient),
getEndpointTelemetryFromFleet(savedObjectsClient, endpointAppContext, esClient),
]);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* 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 { initialDetectionRulesUsage, updateDetectionRuleUsage } from './detections_metrics_helpers';
import { DetectionRuleMetric, DetectionRulesTypeUsage } from './index';
import { v4 as uuid } from 'uuid';

const createStubRule = (
ruleType: string,
enabled: boolean,
elasticRule: boolean,
alertCount: number,
caseCount: number
): DetectionRuleMetric => ({
rule_name: uuid(),
rule_id: uuid(),
rule_type: ruleType,
enabled,
elastic_rule: elasticRule,
created_on: uuid(),
updated_on: uuid(),
alert_count_daily: alertCount,
cases_count_daily: caseCount,
});

describe('Detections Usage and Metrics', () => {
describe('Update metrics with rule information', () => {
it('Should update elastic and eql rule metric total', async () => {
const initialUsage: DetectionRulesTypeUsage = initialDetectionRulesUsage;
const stubRule = createStubRule('eql', true, true, 1, 1);
const usage = updateDetectionRuleUsage(stubRule, initialUsage);

expect(usage).toEqual(
expect.objectContaining({
custom_total: {
alerts: 0,
cases: 0,
disabled: 0,
enabled: 0,
},
elastic_total: {
alerts: 1,
cases: 1,
disabled: 0,
enabled: 1,
},
eql: {
alerts: 1,
cases: 1,
disabled: 0,
enabled: 1,
},
machine_learning: {
alerts: 0,
cases: 0,
disabled: 0,
enabled: 0,
},
query: {
alerts: 0,
cases: 0,
disabled: 0,
enabled: 0,
},
threat_match: {
alerts: 0,
cases: 0,
disabled: 0,
enabled: 0,
},
threshold: {
alerts: 0,
cases: 0,
disabled: 0,
enabled: 0,
},
})
);
});

it('Should update based on multiple metrics', async () => {
const initialUsage: DetectionRulesTypeUsage = initialDetectionRulesUsage;
const stubEqlRule = createStubRule('eql', true, true, 1, 1);
const stubQueryRuleOne = createStubRule('query', true, true, 5, 2);
const stubQueryRuleTwo = createStubRule('query', true, false, 5, 2);
const stubMachineLearningOne = createStubRule('machine_learning', false, false, 0, 10);
const stubMachineLearningTwo = createStubRule('machine_learning', true, true, 22, 44);

let usage = updateDetectionRuleUsage(stubEqlRule, initialUsage);
usage = updateDetectionRuleUsage(stubQueryRuleOne, usage);
usage = updateDetectionRuleUsage(stubQueryRuleTwo, usage);
usage = updateDetectionRuleUsage(stubMachineLearningOne, usage);
usage = updateDetectionRuleUsage(stubMachineLearningTwo, usage);

expect(usage).toEqual(
expect.objectContaining({
custom_total: {
alerts: 5,
cases: 12,
disabled: 1,
enabled: 1,
},
elastic_total: {
alerts: 28,
cases: 47,
disabled: 0,
enabled: 3,
},
eql: {
alerts: 1,
cases: 1,
disabled: 0,
enabled: 1,
},
machine_learning: {
alerts: 22,
cases: 54,
disabled: 1,
enabled: 1,
},
query: {
alerts: 10,
cases: 4,
disabled: 0,
enabled: 2,
},
threat_match: {
alerts: 0,
cases: 0,
disabled: 0,
enabled: 0,
},
threshold: {
alerts: 0,
cases: 0,
disabled: 0,
enabled: 0,
},
})
);
});
});
});
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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { INTERNAL_IMMUTABLE_KEY } from '../../../common/constants';

export const isElasticRule = (tags: string[] = []) =>
tags.includes(`${INTERNAL_IMMUTABLE_KEY}:true`);

interface RuleSearchBody {
query: {
bool: {
filter: {
term: { [key: string]: string };
};
};
};
}

export interface RuleSearchParams {
body: RuleSearchBody;
filterPath: string[];
ignoreUnavailable: boolean;
index: string;
size: number;
}

export interface RuleSearchResult {
alert: {
name: string;
enabled: boolean;
tags: string[];
createdAt: string;
updatedAt: string;
params: DetectionRuleParms;
};
}

interface DetectionRuleParms {
ruleId: string;
version: string;
type: string;
}
Loading

0 comments on commit 4154d68

Please sign in to comment.