[7.13][Telemetry] Detection Rule Adoption (#95659)

* 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>
This commit is contained in:
Pete Hampton 2021-04-20 21:58:36 +01:00 committed by GitHub
parent e5754d772f
commit eee204ff26
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 1553 additions and 130 deletions

View file

@ -174,6 +174,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,
});

View file

@ -33,6 +33,7 @@ export const registerCollector: RegisterCollector = ({
core,
endpointAppContext,
kibanaIndex,
signalsIndex,
ml,
usageCollection,
}) => {
@ -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: {
@ -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),
]);

View file

@ -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,
},
})
);
});
});
});

View file

@ -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;
}

View file

@ -302,3 +302,179 @@ export const getMockMlDatafeedStatsResponse = () => ({
},
],
});
export const getMockRuleSearchResponse = (immutableTag: string = '__internal_immutable:true') => ({
took: 2,
timed_out: false,
_shards: {
total: 1,
successful: 1,
skipped: 0,
failed: 0,
},
hits: {
total: {
value: 1093,
relation: 'eq',
},
max_score: 0,
hits: [
{
_index: '.kibanaindex',
_id: 'alert:6eecd8c2-8bfb-11eb-afbe-1b7a66309c6d',
_score: 0,
_source: {
alert: {
name: 'Azure Diagnostic Settings Deletion',
tags: [
'Elastic',
'Cloud',
'Azure',
'Continuous Monitoring',
'SecOps',
'Monitoring',
'__internal_rule_id:5370d4cd-2bb3-4d71-abf5-1e1d0ff5a2de',
`${immutableTag}`,
],
alertTypeId: 'siem.signals',
consumer: 'siem',
params: {
author: ['Elastic'],
description:
'Identifies the deletion of diagnostic settings in Azure, which send platform logs and metrics to different destinations. An adversary may delete diagnostic settings in an attempt to evade defenses.',
ruleId: '5370d4cd-2bb3-4d71-abf5-1e1d0ff5a2de',
index: ['filebeat-*', 'logs-azure*'],
falsePositives: [
'Deletion of diagnostic settings may be done by a system or network administrator. Verify whether the username, hostname, and/or resource name should be making changes in your environment. Diagnostic settings deletion from unfamiliar users or hosts should be investigated. If known behavior is causing false positives, it can be exempted from the rule.',
],
from: 'now-25m',
immutable: true,
query:
'event.dataset:azure.activitylogs and azure.activitylogs.operation_name:"MICROSOFT.INSIGHTS/DIAGNOSTICSETTINGS/DELETE" and event.outcome:(Success or success)',
language: 'kuery',
license: 'Elastic License v2',
outputIndex: '.siem-signals',
maxSignals: 100,
riskScore: 47,
timestampOverride: 'event.ingested',
to: 'now',
type: 'query',
references: [
'https://docs.microsoft.com/en-us/azure/azure-monitor/platform/diagnostic-settings',
],
note: 'The Azure Filebeat module must be enabled to use this rule.',
version: 4,
exceptionsList: [],
},
schedule: {
interval: '5m',
},
enabled: false,
actions: [],
throttle: null,
notifyWhen: 'onActiveAlert',
apiKeyOwner: null,
apiKey: null,
createdBy: 'user',
updatedBy: 'user',
createdAt: '2021-03-23T17:15:59.634Z',
updatedAt: '2021-03-23T17:15:59.634Z',
muteAll: false,
mutedInstanceIds: [],
executionStatus: {
status: 'pending',
lastExecutionDate: '2021-03-23T17:15:59.634Z',
error: null,
},
meta: {
versionApiKeyLastmodified: '8.0.0',
},
},
type: 'alert',
references: [],
migrationVersion: {
alert: '7.13.0',
},
coreMigrationVersion: '8.0.0',
updated_at: '2021-03-23T17:15:59.634Z',
},
},
],
},
});
export const getMockRuleAlertsResponse = (docCount: number) => ({
took: 7,
timed_out: false,
_shards: {
total: 1,
successful: 1,
skipped: 0,
failed: 0,
},
hits: {
total: {
value: 7322,
relation: 'eq',
},
max_score: null,
hits: [],
},
aggregations: {
detectionAlerts: {
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [
{
key: '6eecd8c2-8bfb-11eb-afbe-1b7a66309c6d',
doc_count: docCount,
},
],
},
},
});
export const getMockAlertCasesResponse = () => ({
page: 1,
per_page: 10000,
total: 4,
saved_objects: [
{
type: 'cases-comments',
id: '3bb5cc10-9249-11eb-85b7-254c8af1a983',
attributes: {
associationType: 'case',
type: 'alert',
alertId: '54802763917f521249c9f68d0d4be0c26cc538404c26dfed1ae7dcfa94ea2226',
index: '.siem-signals-default-000001',
rule: {
id: '6eecd8c2-8bfb-11eb-afbe-1b7a66309c6d',
name: 'Azure Diagnostic Settings Deletion',
},
created_at: '2021-03-31T17:47:59.449Z',
created_by: {
email: '',
full_name: '',
username: '',
},
pushed_at: null,
pushed_by: null,
updated_at: null,
updated_by: null,
},
references: [
{
type: 'cases',
name: 'associated-cases',
id: '3a3a4fa0-9249-11eb-85b7-254c8af1a983',
},
],
migrationVersion: {},
coreMigrationVersion: '8.0.0',
updated_at: '2021-03-31T17:47:59.818Z',
version: 'WzI3MDIyODMsNF0=',
namespaces: ['default'],
score: 0,
},
],
});

View file

@ -5,8 +5,11 @@
* 2.0.
*/
import { ElasticsearchClient, SavedObjectsClientContract } from '../../../../../../src/core/server';
import { elasticsearchServiceMock } from '../../../../../../src/core/server/mocks';
import { ElasticsearchClient } from '../../../../../../src/core/server';
import {
elasticsearchServiceMock,
savedObjectsClientMock,
} from '../../../../../../src/core/server/mocks';
import { mlServicesMock } from '../../lib/machine_learning/mocks';
import {
getMockJobSummaryResponse,
@ -15,12 +18,16 @@ import {
getMockMlJobDetailsResponse,
getMockMlJobStatsResponse,
getMockMlDatafeedStatsResponse,
getMockRuleSearchResponse,
getMockRuleAlertsResponse,
getMockAlertCasesResponse,
} from './detections.mocks';
import { fetchDetectionsUsage, fetchDetectionsMetrics } from './index';
const savedObjectsClient = savedObjectsClientMock.create();
describe('Detections Usage and Metrics', () => {
let esClientMock: jest.Mocked<ElasticsearchClient>;
let savedObjectsClientMock: jest.Mocked<SavedObjectsClientContract>;
let mlMock: ReturnType<typeof mlServicesMock.createSetupContract>;
describe('fetchDetectionsUsage()', () => {
@ -30,7 +37,7 @@ describe('Detections Usage and Metrics', () => {
});
it('returns zeroed counts if both calls are empty', async () => {
const result = await fetchDetectionsUsage('', esClientMock, mlMock, savedObjectsClientMock);
const result = await fetchDetectionsUsage('', esClientMock, mlMock, savedObjectsClient);
expect(result).toEqual({
detection_rules: {
@ -59,7 +66,7 @@ describe('Detections Usage and Metrics', () => {
it('tallies rules data given rules results', async () => {
(esClientMock.search as jest.Mock).mockResolvedValue({ body: getMockRulesResponse() });
const result = await fetchDetectionsUsage('', esClientMock, mlMock, savedObjectsClientMock);
const result = await fetchDetectionsUsage('', esClientMock, mlMock, savedObjectsClient);
expect(result).toEqual(
expect.objectContaining({
@ -87,7 +94,7 @@ describe('Detections Usage and Metrics', () => {
jobsSummary: mockJobSummary,
});
const result = await fetchDetectionsUsage('', esClientMock, mlMock, savedObjectsClientMock);
const result = await fetchDetectionsUsage('', esClientMock, mlMock, savedObjectsClient);
expect(result).toEqual(
expect.objectContaining({
@ -106,8 +113,285 @@ describe('Detections Usage and Metrics', () => {
});
});
describe('getDetectionRuleMetrics()', () => {
beforeEach(() => {
esClientMock = elasticsearchServiceMock.createClusterClient().asInternalUser;
mlMock = mlServicesMock.createSetupContract();
});
it('returns zeroed counts if calls are empty', async () => {
const result = await fetchDetectionsMetrics('', '', esClientMock, mlMock, savedObjectsClient);
expect(result).toEqual(
expect.objectContaining({
detection_rules: {
detection_rule_detail: [],
detection_rule_usage: {
query: {
enabled: 0,
disabled: 0,
alerts: 0,
cases: 0,
},
threshold: {
enabled: 0,
disabled: 0,
alerts: 0,
cases: 0,
},
eql: {
enabled: 0,
disabled: 0,
alerts: 0,
cases: 0,
},
machine_learning: {
enabled: 0,
disabled: 0,
alerts: 0,
cases: 0,
},
threat_match: {
enabled: 0,
disabled: 0,
alerts: 0,
cases: 0,
},
elastic_total: {
enabled: 0,
disabled: 0,
alerts: 0,
cases: 0,
},
custom_total: {
enabled: 0,
disabled: 0,
alerts: 0,
cases: 0,
},
},
},
ml_jobs: [],
})
);
});
it('returns information with rule, alerts and cases', async () => {
(esClientMock.search as jest.Mock)
.mockReturnValueOnce({ body: getMockRuleSearchResponse() })
.mockReturnValue({ body: getMockRuleAlertsResponse(3400) });
(savedObjectsClient.find as jest.Mock).mockReturnValue(getMockAlertCasesResponse());
const result = await fetchDetectionsMetrics('', '', esClientMock, mlMock, savedObjectsClient);
expect(result).toEqual(
expect.objectContaining({
detection_rules: {
detection_rule_detail: [
{
alert_count_daily: 3400,
cases_count_daily: 1,
created_on: '2021-03-23T17:15:59.634Z',
elastic_rule: true,
enabled: false,
rule_id: '6eecd8c2-8bfb-11eb-afbe-1b7a66309c6d',
rule_name: 'Azure Diagnostic Settings Deletion',
rule_type: 'query',
rule_version: 4,
updated_on: '2021-03-23T17:15:59.634Z',
},
],
detection_rule_usage: {
custom_total: {
alerts: 0,
cases: 0,
disabled: 0,
enabled: 0,
},
elastic_total: {
alerts: 3400,
cases: 1,
disabled: 1,
enabled: 0,
},
eql: {
alerts: 0,
cases: 0,
disabled: 0,
enabled: 0,
},
machine_learning: {
alerts: 0,
cases: 0,
disabled: 0,
enabled: 0,
},
query: {
alerts: 3400,
cases: 1,
disabled: 1,
enabled: 0,
},
threat_match: {
alerts: 0,
cases: 0,
disabled: 0,
enabled: 0,
},
threshold: {
alerts: 0,
cases: 0,
disabled: 0,
enabled: 0,
},
},
},
ml_jobs: [],
})
);
});
it('returns information with on non elastic prebuilt rule', async () => {
(esClientMock.search as jest.Mock)
.mockReturnValueOnce({ body: getMockRuleSearchResponse('not_immutable') })
.mockReturnValue({ body: getMockRuleAlertsResponse(800) });
(savedObjectsClient.find as jest.Mock).mockReturnValue(getMockAlertCasesResponse());
const result = await fetchDetectionsMetrics('', '', esClientMock, mlMock, savedObjectsClient);
expect(result).toEqual(
expect.objectContaining({
detection_rules: {
detection_rule_detail: [], // *should not* contain custom detection rule details
detection_rule_usage: {
custom_total: {
alerts: 800,
cases: 1,
disabled: 1,
enabled: 0,
},
elastic_total: {
alerts: 0,
cases: 0,
disabled: 0,
enabled: 0,
},
eql: {
alerts: 0,
cases: 0,
disabled: 0,
enabled: 0,
},
machine_learning: {
alerts: 0,
cases: 0,
disabled: 0,
enabled: 0,
},
query: {
alerts: 800,
cases: 1,
disabled: 1,
enabled: 0,
},
threat_match: {
alerts: 0,
cases: 0,
disabled: 0,
enabled: 0,
},
threshold: {
alerts: 0,
cases: 0,
disabled: 0,
enabled: 0,
},
},
},
ml_jobs: [],
})
);
});
it('returns information with rule, no alerts and no cases', async () => {
(esClientMock.search as jest.Mock)
.mockReturnValueOnce({ body: getMockRuleSearchResponse() })
.mockReturnValue({ body: getMockRuleAlertsResponse(0) });
(savedObjectsClient.find as jest.Mock).mockReturnValue(getMockAlertCasesResponse());
const result = await fetchDetectionsMetrics('', '', esClientMock, mlMock, savedObjectsClient);
expect(result).toEqual(
expect.objectContaining({
detection_rules: {
detection_rule_detail: [
{
alert_count_daily: 0,
cases_count_daily: 1,
created_on: '2021-03-23T17:15:59.634Z',
elastic_rule: true,
enabled: false,
rule_id: '6eecd8c2-8bfb-11eb-afbe-1b7a66309c6d',
rule_name: 'Azure Diagnostic Settings Deletion',
rule_type: 'query',
rule_version: 4,
updated_on: '2021-03-23T17:15:59.634Z',
},
],
detection_rule_usage: {
custom_total: {
alerts: 0,
cases: 0,
disabled: 0,
enabled: 0,
},
elastic_total: {
alerts: 0,
cases: 1,
disabled: 1,
enabled: 0,
},
eql: {
alerts: 0,
cases: 0,
disabled: 0,
enabled: 0,
},
machine_learning: {
alerts: 0,
cases: 0,
disabled: 0,
enabled: 0,
},
query: {
alerts: 0,
cases: 1,
disabled: 1,
enabled: 0,
},
threat_match: {
alerts: 0,
cases: 0,
disabled: 0,
enabled: 0,
},
threshold: {
alerts: 0,
cases: 0,
disabled: 0,
enabled: 0,
},
},
},
ml_jobs: [],
})
);
});
});
describe('fetchDetectionsMetrics()', () => {
beforeEach(() => {
esClientMock = elasticsearchServiceMock.createClusterClient().asInternalUser;
mlMock = mlServicesMock.createSetupContract();
});
@ -116,7 +400,7 @@ describe('Detections Usage and Metrics', () => {
jobs: null,
jobStats: null,
} as unknown) as ReturnType<typeof mlMock.anomalyDetectorsProvider>);
const result = await fetchDetectionsMetrics(mlMock, savedObjectsClientMock);
const result = await fetchDetectionsMetrics('', '', esClientMock, mlMock, savedObjectsClient);
expect(result).toEqual(
expect.objectContaining({
@ -138,7 +422,7 @@ describe('Detections Usage and Metrics', () => {
datafeedStats: mockDatafeedStatsResponse,
} as unknown) as ReturnType<typeof mlMock.anomalyDetectorsProvider>);
const result = await fetchDetectionsMetrics(mlMock, savedObjectsClientMock);
const result = await fetchDetectionsMetrics('', '', esClientMock, mlMock, savedObjectsClient);
expect(result).toEqual(
expect.objectContaining({

View file

@ -0,0 +1,380 @@
/*
* 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 {
ElasticsearchClient,
KibanaRequest,
SavedObjectsClientContract,
} from '../../../../../../src/core/server';
import {
AlertsAggregationResponse,
CasesSavedObject,
DetectionRulesTypeUsage,
DetectionRuleMetric,
DetectionRuleAdoption,
MlJobMetric,
} from './index';
import { SIGNALS_ID } from '../../../common/constants';
import { DatafeedStats, Job, MlPluginSetup } from '../../../../ml/server';
import { isElasticRule, RuleSearchParams, RuleSearchResult } from './detection_telemetry_helpers';
/**
* Default detection rule usage count, split by type + elastic/custom
*/
export const initialDetectionRulesUsage: DetectionRulesTypeUsage = {
query: {
enabled: 0,
disabled: 0,
alerts: 0,
cases: 0,
},
threshold: {
enabled: 0,
disabled: 0,
alerts: 0,
cases: 0,
},
eql: {
enabled: 0,
disabled: 0,
alerts: 0,
cases: 0,
},
machine_learning: {
enabled: 0,
disabled: 0,
alerts: 0,
cases: 0,
},
threat_match: {
enabled: 0,
disabled: 0,
alerts: 0,
cases: 0,
},
elastic_total: {
enabled: 0,
disabled: 0,
alerts: 0,
cases: 0,
},
custom_total: {
enabled: 0,
disabled: 0,
alerts: 0,
cases: 0,
},
};
/* eslint-disable complexity */
export const updateDetectionRuleUsage = (
detectionRuleMetric: DetectionRuleMetric,
usage: DetectionRulesTypeUsage
): DetectionRulesTypeUsage => {
let updatedUsage = usage;
if (detectionRuleMetric.rule_type === 'query') {
updatedUsage = {
...usage,
query: {
...usage.query,
enabled: detectionRuleMetric.enabled ? usage.query.enabled + 1 : usage.query.enabled,
disabled: !detectionRuleMetric.enabled ? usage.query.disabled + 1 : usage.query.disabled,
alerts: usage.query.alerts + detectionRuleMetric.alert_count_daily,
cases: usage.query.cases + detectionRuleMetric.cases_count_daily,
},
};
} else if (detectionRuleMetric.rule_type === 'threshold') {
updatedUsage = {
...usage,
threshold: {
...usage.threshold,
enabled: detectionRuleMetric.enabled
? usage.threshold.enabled + 1
: usage.threshold.enabled,
disabled: !detectionRuleMetric.enabled
? usage.threshold.disabled + 1
: usage.threshold.disabled,
alerts: usage.threshold.alerts + detectionRuleMetric.alert_count_daily,
cases: usage.threshold.cases + detectionRuleMetric.cases_count_daily,
},
};
} else if (detectionRuleMetric.rule_type === 'eql') {
updatedUsage = {
...usage,
eql: {
...usage.eql,
enabled: detectionRuleMetric.enabled ? usage.eql.enabled + 1 : usage.eql.enabled,
disabled: !detectionRuleMetric.enabled ? usage.eql.disabled + 1 : usage.eql.disabled,
alerts: usage.eql.alerts + detectionRuleMetric.alert_count_daily,
cases: usage.eql.cases + detectionRuleMetric.cases_count_daily,
},
};
} else if (detectionRuleMetric.rule_type === 'machine_learning') {
updatedUsage = {
...usage,
machine_learning: {
...usage.machine_learning,
enabled: detectionRuleMetric.enabled
? usage.machine_learning.enabled + 1
: usage.machine_learning.enabled,
disabled: !detectionRuleMetric.enabled
? usage.machine_learning.disabled + 1
: usage.machine_learning.disabled,
alerts: usage.machine_learning.alerts + detectionRuleMetric.alert_count_daily,
cases: usage.machine_learning.cases + detectionRuleMetric.cases_count_daily,
},
};
} else if (detectionRuleMetric.rule_type === 'threat_match') {
updatedUsage = {
...usage,
threat_match: {
...usage.threat_match,
enabled: detectionRuleMetric.enabled
? usage.threat_match.enabled + 1
: usage.threat_match.enabled,
disabled: !detectionRuleMetric.enabled
? usage.threat_match.disabled + 1
: usage.threat_match.disabled,
alerts: usage.threat_match.alerts + detectionRuleMetric.alert_count_daily,
cases: usage.threat_match.cases + detectionRuleMetric.cases_count_daily,
},
};
}
if (detectionRuleMetric.elastic_rule) {
updatedUsage = {
...updatedUsage,
elastic_total: {
...updatedUsage.elastic_total,
enabled: detectionRuleMetric.enabled
? updatedUsage.elastic_total.enabled + 1
: updatedUsage.elastic_total.enabled,
disabled: !detectionRuleMetric.enabled
? updatedUsage.elastic_total.disabled + 1
: updatedUsage.elastic_total.disabled,
alerts: updatedUsage.elastic_total.alerts + detectionRuleMetric.alert_count_daily,
cases: updatedUsage.elastic_total.cases + detectionRuleMetric.cases_count_daily,
},
};
} else {
updatedUsage = {
...updatedUsage,
custom_total: {
...updatedUsage.custom_total,
enabled: detectionRuleMetric.enabled
? updatedUsage.custom_total.enabled + 1
: updatedUsage.custom_total.enabled,
disabled: !detectionRuleMetric.enabled
? updatedUsage.custom_total.disabled + 1
: updatedUsage.custom_total.disabled,
alerts: updatedUsage.custom_total.alerts + detectionRuleMetric.alert_count_daily,
cases: updatedUsage.custom_total.cases + detectionRuleMetric.cases_count_daily,
},
};
}
return updatedUsage;
};
export const getDetectionRuleMetrics = async (
kibanaIndex: string,
signalsIndex: string,
esClient: ElasticsearchClient,
savedObjectClient: SavedObjectsClientContract
): Promise<DetectionRuleAdoption> => {
let rulesUsage: DetectionRulesTypeUsage = initialDetectionRulesUsage;
const ruleSearchOptions: RuleSearchParams = {
body: { query: { bool: { filter: { term: { 'alert.alertTypeId': SIGNALS_ID } } } } },
filterPath: [],
ignoreUnavailable: true,
index: kibanaIndex,
size: 1,
};
try {
const { body: ruleResults } = await esClient.search<RuleSearchResult>(ruleSearchOptions);
const { body: detectionAlertsResp } = (await esClient.search({
index: `${signalsIndex}*`,
size: 0,
body: {
aggs: {
detectionAlerts: {
terms: { field: 'signal.rule.id.keyword' },
},
},
query: {
bool: {
filter: [
{
range: {
'@timestamp': {
gte: 'now-24h',
lte: 'now',
},
},
},
],
},
},
},
})) as { body: AlertsAggregationResponse };
const cases = await savedObjectClient.find<CasesSavedObject>({
type: 'cases-comments',
fields: [],
page: 1,
perPage: 10_000,
filter: 'cases-comments.attributes.type: alert',
});
const casesCache = cases.saved_objects.reduce((cache, { attributes: casesObject }) => {
const ruleId = casesObject.rule.id;
const cacheCount = cache.get(ruleId);
if (cacheCount === undefined) {
cache.set(ruleId, 1);
} else {
cache.set(ruleId, cacheCount + 1);
}
return cache;
}, new Map<string, number>());
const alertBuckets = detectionAlertsResp.aggregations?.detectionAlerts?.buckets ?? [];
const alertsCache = new Map<string, number>();
alertBuckets.map((bucket) => alertsCache.set(bucket.key, bucket.doc_count));
if (ruleResults.hits?.hits?.length > 0) {
const ruleObjects = ruleResults.hits.hits.map((hit) => {
const ruleId = hit._id.split(':')[1];
const isElastic = isElasticRule(hit._source?.alert.tags);
return {
rule_name: hit._source?.alert.name,
rule_id: ruleId,
rule_type: hit._source?.alert.params.type,
rule_version: hit._source?.alert.params.version,
enabled: hit._source?.alert.enabled,
elastic_rule: isElastic,
created_on: hit._source?.alert.createdAt,
updated_on: hit._source?.alert.updatedAt,
alert_count_daily: alertsCache.get(ruleId) || 0,
cases_count_daily: casesCache.get(ruleId) || 0,
} as DetectionRuleMetric;
});
// Only bring back rule detail on elastic prepackaged detection rules
const elasticRuleObjects = ruleObjects.filter((hit) => hit.elastic_rule === true);
rulesUsage = ruleObjects.reduce((usage, rule) => {
return updateDetectionRuleUsage(rule, usage);
}, rulesUsage);
return {
detection_rule_detail: elasticRuleObjects,
detection_rule_usage: rulesUsage,
};
}
} catch (e) {
// ignore failure, usage will be zeroed
}
return {
detection_rule_detail: [],
detection_rule_usage: rulesUsage,
};
};
export const getMlJobMetrics = async (
ml: MlPluginSetup | undefined,
savedObjectClient: SavedObjectsClientContract
): Promise<MlJobMetric[]> => {
if (ml) {
try {
const fakeRequest = { headers: {} } as KibanaRequest;
const jobsType = 'security';
const securityJobStats = await ml
.anomalyDetectorsProvider(fakeRequest, savedObjectClient)
.jobStats(jobsType);
const jobDetails = await ml
.anomalyDetectorsProvider(fakeRequest, savedObjectClient)
.jobs(jobsType);
const jobDetailsCache = new Map<string, Job>();
jobDetails.jobs.forEach((detail) => jobDetailsCache.set(detail.job_id, detail));
const datafeedStats = await ml
.anomalyDetectorsProvider(fakeRequest, savedObjectClient)
.datafeedStats();
const datafeedStatsCache = new Map<string, DatafeedStats>();
datafeedStats.datafeeds.forEach((datafeedStat) =>
datafeedStatsCache.set(`${datafeedStat.datafeed_id}`, datafeedStat)
);
return securityJobStats.jobs.map((stat) => {
const jobId = stat.job_id;
const jobDetail = jobDetailsCache.get(stat.job_id);
const datafeed = datafeedStatsCache.get(`datafeed-${jobId}`);
return {
job_id: jobId,
open_time: stat.open_time,
create_time: jobDetail?.create_time,
finished_time: jobDetail?.finished_time,
state: stat.state,
data_counts: {
bucket_count: stat.data_counts.bucket_count,
empty_bucket_count: stat.data_counts.empty_bucket_count,
input_bytes: stat.data_counts.input_bytes,
input_record_count: stat.data_counts.input_record_count,
last_data_time: stat.data_counts.last_data_time,
processed_record_count: stat.data_counts.processed_record_count,
},
model_size_stats: {
bucket_allocation_failures_count:
stat.model_size_stats.bucket_allocation_failures_count,
memory_status: stat.model_size_stats.memory_status,
model_bytes: stat.model_size_stats.model_bytes,
model_bytes_exceeded: stat.model_size_stats.model_bytes_exceeded,
model_bytes_memory_limit: stat.model_size_stats.model_bytes_memory_limit,
peak_model_bytes: stat.model_size_stats.peak_model_bytes,
},
timing_stats: {
average_bucket_processing_time_ms: stat.timing_stats.average_bucket_processing_time_ms,
bucket_count: stat.timing_stats.bucket_count,
exponential_average_bucket_processing_time_ms:
stat.timing_stats.exponential_average_bucket_processing_time_ms,
exponential_average_bucket_processing_time_per_hour_ms:
stat.timing_stats.exponential_average_bucket_processing_time_per_hour_ms,
maximum_bucket_processing_time_ms: stat.timing_stats.maximum_bucket_processing_time_ms,
minimum_bucket_processing_time_ms: stat.timing_stats.minimum_bucket_processing_time_ms,
total_bucket_processing_time_ms: stat.timing_stats.total_bucket_processing_time_ms,
},
datafeed: {
datafeed_id: datafeed?.datafeed_id,
state: datafeed?.state,
timing_stats: {
average_search_time_per_bucket_ms:
datafeed?.timing_stats.average_search_time_per_bucket_ms,
bucket_count: datafeed?.timing_stats.bucket_count,
exponential_average_search_time_per_hour_ms:
datafeed?.timing_stats.exponential_average_search_time_per_hour_ms,
search_count: datafeed?.timing_stats.search_count,
total_search_time_ms: datafeed?.timing_stats.total_search_time_ms,
},
},
} as MlJobMetric;
});
} catch (e) {
// ignore failure, usage will be zeroed
}
}
return [];
};

View file

@ -7,42 +7,21 @@
import {
ElasticsearchClient,
SavedObjectsClientContract,
KibanaRequest,
SavedObjectsClientContract,
} from '../../../../../../src/core/server';
import { MlPluginSetup } from '../../../../ml/server';
import { SIGNALS_ID, INTERNAL_IMMUTABLE_KEY } from '../../../common/constants';
import { DetectionRulesUsage, MlJobsUsage, MlJobMetric } from './index';
import { SIGNALS_ID } from '../../../common/constants';
import { isJobStarted } from '../../../common/machine_learning/helpers';
import { isSecurityJob } from '../../../common/machine_learning/is_security_job';
import { MlPluginSetup } from '../../../../ml/server';
import { DetectionRulesUsage, MlJobsUsage } from './index';
import { isElasticRule, RuleSearchParams, RuleSearchResult } from './detection_telemetry_helpers';
interface DetectionsMetric {
isElastic: boolean;
isEnabled: boolean;
}
interface RuleSearchBody {
query: {
bool: {
filter: {
term: { [key: string]: string };
};
};
};
}
interface RuleSearchParams {
body: RuleSearchBody;
filterPath: string[];
ignoreUnavailable: boolean;
index: string;
size: number;
}
interface RuleSearchResult {
alert: { enabled: boolean; tags: string[] };
}
const isElasticRule = (tags: string[]) => tags.includes(`${INTERNAL_IMMUTABLE_KEY}:true`);
/**
* Default detection rule usage count
*/
@ -170,7 +149,6 @@ export const getRulesUsage = async (
if (ruleResults.hits?.hits?.length > 0) {
rulesUsage = ruleResults.hits.hits.reduce((usage, hit) => {
// @ts-expect-error _source is optional
const isElastic = isElasticRule(hit._source?.alert.tags);
const isEnabled = Boolean(hit._source?.alert.enabled);
@ -211,93 +189,3 @@ export const getMlJobsUsage = async (
return jobsUsage;
};
export const getMlJobMetrics = async (
ml: MlPluginSetup | undefined,
savedObjectClient: SavedObjectsClientContract
): Promise<MlJobMetric[]> => {
if (ml) {
try {
const fakeRequest = { headers: {} } as KibanaRequest;
const jobsType = 'security';
const securityJobStats = await ml
.anomalyDetectorsProvider(fakeRequest, savedObjectClient)
.jobStats(jobsType);
const jobDetails = await ml
.anomalyDetectorsProvider(fakeRequest, savedObjectClient)
.jobs(jobsType);
const jobDetailsCache = new Map();
jobDetails.jobs.forEach((detail) => jobDetailsCache.set(detail.job_id, detail));
const datafeedStats = await ml
.anomalyDetectorsProvider(fakeRequest, savedObjectClient)
.datafeedStats();
const datafeedStatsCache = new Map();
datafeedStats.datafeeds.forEach((datafeedStat) =>
datafeedStatsCache.set(`${datafeedStat.datafeed_id}`, datafeedStat)
);
return securityJobStats.jobs.map((stat) => {
const jobId = stat.job_id;
const jobDetail = jobDetailsCache.get(stat.job_id);
const datafeed = datafeedStatsCache.get(`datafeed-${jobId}`);
return {
job_id: jobId,
open_time: stat.open_time,
create_time: jobDetail?.create_time,
finished_time: jobDetail?.finished_time,
state: stat.state,
data_counts: {
bucket_count: stat.data_counts.bucket_count,
empty_bucket_count: stat.data_counts.empty_bucket_count,
input_bytes: stat.data_counts.input_bytes,
input_record_count: stat.data_counts.input_record_count,
last_data_time: stat.data_counts.last_data_time,
processed_record_count: stat.data_counts.processed_record_count,
},
model_size_stats: {
bucket_allocation_failures_count:
stat.model_size_stats.bucket_allocation_failures_count,
memory_status: stat.model_size_stats.memory_status,
model_bytes: stat.model_size_stats.model_bytes,
model_bytes_exceeded: stat.model_size_stats.model_bytes_exceeded,
model_bytes_memory_limit: stat.model_size_stats.model_bytes_memory_limit,
peak_model_bytes: stat.model_size_stats.peak_model_bytes,
},
timing_stats: {
average_bucket_processing_time_ms: stat.timing_stats.average_bucket_processing_time_ms,
bucket_count: stat.timing_stats.bucket_count,
exponential_average_bucket_processing_time_ms:
stat.timing_stats.exponential_average_bucket_processing_time_ms,
exponential_average_bucket_processing_time_per_hour_ms:
stat.timing_stats.exponential_average_bucket_processing_time_per_hour_ms,
maximum_bucket_processing_time_ms: stat.timing_stats.maximum_bucket_processing_time_ms,
minimum_bucket_processing_time_ms: stat.timing_stats.minimum_bucket_processing_time_ms,
total_bucket_processing_time_ms: stat.timing_stats.total_bucket_processing_time_ms,
},
datafeed: {
datafeed_id: datafeed?.datafeed_id,
state: datafeed?.state,
timing_stats: {
average_search_time_per_bucket_ms:
datafeed?.timing_stats.average_search_time_per_bucket_ms,
bucket_count: datafeed?.timing_stats.bucket_count,
exponential_average_search_time_per_hour_ms:
datafeed?.timing_stats.exponential_average_search_time_per_hour_ms,
search_count: datafeed?.timing_stats.search_count,
total_search_time_ms: datafeed?.timing_stats.total_search_time_ms,
},
},
} as MlJobMetric;
});
} catch (e) {
// ignore failure, usage will be zeroed
}
}
return [];
};

View file

@ -8,11 +8,15 @@
import { ElasticsearchClient, SavedObjectsClientContract } from '../../../../../../src/core/server';
import {
getMlJobsUsage,
getMlJobMetrics,
getRulesUsage,
initialRulesUsage,
initialMlJobsUsage,
} from './detections_helpers';
} from './detections_usage_helpers';
import {
getMlJobMetrics,
getDetectionRuleMetrics,
initialDetectionRulesUsage,
} from './detections_metrics_helpers';
import { MlPluginSetup } from '../../../../ml/server';
interface FeatureUsage {
@ -20,6 +24,23 @@ interface FeatureUsage {
disabled: number;
}
interface FeatureTypeUsage {
enabled: number;
disabled: number;
alerts: number;
cases: number;
}
export interface DetectionRulesTypeUsage {
query: FeatureTypeUsage;
threshold: FeatureTypeUsage;
eql: FeatureTypeUsage;
machine_learning: FeatureTypeUsage;
threat_match: FeatureTypeUsage;
elastic_total: FeatureTypeUsage;
custom_total: FeatureTypeUsage;
}
export interface DetectionRulesUsage {
custom: FeatureUsage;
elastic: FeatureUsage;
@ -37,6 +58,7 @@ export interface DetectionsUsage {
export interface DetectionMetrics {
ml_jobs: MlJobMetric[];
detection_rules: DetectionRuleAdoption;
}
export interface MlJobDataCount {
@ -76,6 +98,45 @@ export interface MlJobMetric {
timing_stats: MlTimingStats;
}
export interface DetectionRuleMetric {
rule_name: string;
rule_id: string;
rule_type: string;
enabled: boolean;
elastic_rule: boolean;
created_on: string;
updated_on: string;
alert_count_daily: number;
cases_count_daily: number;
}
export interface DetectionRuleAdoption {
detection_rule_detail: DetectionRuleMetric[];
detection_rule_usage: DetectionRulesTypeUsage;
}
export interface AlertsAggregationResponse {
hits: {
total: { value: number };
};
aggregations: {
[aggName: string]: {
buckets: Array<{ key: string; doc_count: number }>;
};
};
}
export interface CasesSavedObject {
associationType: string;
type: string;
alertId: string;
index: string;
rule: {
id: string;
name: string;
};
}
export const defaultDetectionsUsage = {
detection_rules: initialRulesUsage,
ml_jobs: initialMlJobsUsage,
@ -99,12 +160,22 @@ export const fetchDetectionsUsage = async (
};
export const fetchDetectionsMetrics = async (
kibanaIndex: string,
signalsIndex: string,
esClient: ElasticsearchClient,
ml: MlPluginSetup | undefined,
savedObjectClient: SavedObjectsClientContract
): Promise<DetectionMetrics> => {
const [mlJobMetrics] = await Promise.allSettled([getMlJobMetrics(ml, savedObjectClient)]);
const [mlJobMetrics, detectionRuleMetrics] = await Promise.allSettled([
getMlJobMetrics(ml, savedObjectClient),
getDetectionRuleMetrics(kibanaIndex, signalsIndex, esClient, savedObjectClient),
]);
return {
ml_jobs: mlJobMetrics.status === 'fulfilled' ? mlJobMetrics.value : [],
detection_rules:
detectionRuleMetrics.status === 'fulfilled'
? detectionRuleMetrics.value
: { detection_rule_detail: [], detection_rule_usage: initialDetectionRulesUsage },
};
};

View file

@ -11,6 +11,7 @@ import { SetupPlugins } from '../plugin';
export type CollectorDependencies = {
kibanaIndex: string;
signalsIndex: string;
core: CoreSetup;
endpointAppContext: EndpointAppContext;
} & Pick<SetupPlugins, 'ml' | 'usageCollection'>;

View file

@ -4544,6 +4544,277 @@
},
"detectionMetrics": {
"properties": {
"detection_rules": {
"properties": {
"detection_rule_usage": {
"properties": {
"query": {
"properties": {
"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": {
"properties": {
"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": {
"properties": {
"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": {
"properties": {
"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": {
"properties": {
"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": {
"properties": {
"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": {
"properties": {
"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": {
"properties": {
"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": {