[Security Solution] Adds telemetry for legacy notifications and regular notifications at a finer grained level (#123332)

## Summary

Related and previous PR:
https://github.com/elastic/kibana/pull/122472

This removes the above structure from the PR above and instead opts to use a more finer grained level of telemetry. The new structure adds to each rule these four counters to the telemetry:

* legacy_notifications_enabled - The number of legacy notifications on rules that are enabled/active
* legacy_notifications_disabled - The number of legacy notifications on rules that are disabled/in-active
* notifications_enabled - The number of notifications on rules that are enabled/active
* notifications_disabled - The number of notifications on rules that are disabled/in-active

For pre-built rules you have these booleans:
* has_legacy_notification - True if the pre-built rule has a legacy notification attached, otherwise false.
* has_notification - True if the pre-built rule has a notification attached, otherwise false.

Note, both those booleans are `false` if the pre-built rule has no notifications attached and both can never be `true` together.

These will show up within each rule type like for example on a query rule it will look like:

```json
"detection_rule_usage": {
  "query": {
    "enabled": 2,
    "disabled": 1,
    "cases": 0,
    "legacy_notifications_enabled": 1, <-- New
    "legacy_notifications_disabled": 0, <-- New
    "notifications_enabled": 1, <-- New
    "notifications_disabled": 1 <-- New
}
```

Within the counts/total sections it will show up on both the `elastic` rules and the `custom` rules like so:

```json
"elastic_total": {
  "enabled": 0,
  "disabled": 0,
  "alerts": 0,
  "cases": 0,
  "legacy_notifications_enabled": 0, <-- New
  "legacy_notifications_disabled": 0, <-- New
  "notifications_enabled": 0, <-- New
  "notifications_disabled": 0 <-- New
},
"custom_total": {
  "enabled": 2,
  "disabled": 1,
  "alerts": 7218,
  "cases": 0,
  "legacy_notifications_enabled": 1, <-- New
  "legacy_notifications_disabled": 0, <-- New
  "notifications_enabled": 1, <-- New
  "notifications_disabled": 1 <-- New
}
```

For pre-built it will be:

```json
"detection_rule_detail": [
  {
    "rule_name": "Potential Evasion via Filter Manager",
    "rule_id": "06dceabf-adca-48af-ac79-ffdf4c3b1e9a",
    "rule_type": "eql",
    "rule_version": 8,
    "enabled": false,
    "elastic_rule": true,
    "created_on": "2022-01-19T01:29:25.540Z",
    "updated_on": "2022-01-19T01:29:25.540Z",
    "alert_count_daily": 0,
    "cases_count_total": 0,
    "has_legacy_notification": false, <-- New
    "has_notification": false <-- New
  },
```

Screen shot of it if you go to "Advanced settings -> cluster data":
<img width="802" alt="Screen Shot 2022-01-18 at 6 27 14 PM" src="https://user-images.githubusercontent.com/1151048/150046445-b1850b1c-bca6-41e0-b101-1bac5f67dbb3.png">

<img width="798" alt="Screen Shot 2022-01-18 at 6 30 33 PM" src="https://user-images.githubusercontent.com/1151048/150046808-1109a4c9-8a54-4da8-8b42-5f957a9d3ed5.png">

Follow the manual test instructions on https://github.com/elastic/kibana/pull/122472 for how to test this. The same manual testing applies here for seeing how these work out. You should be able to see a higher granularity with these stats.

### Checklist

- [ ] [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
This commit is contained in:
Frank Hassanabad 2022-01-19 10:39:47 -07:00 committed by GitHub
parent d40c0abe5b
commit d2a8bb90be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 1797 additions and 196 deletions

View file

@ -3,7 +3,7 @@
"interval": "1m",
"actions": [
{
"id": "1fa31c30-3046-11ec-8971-1f3f7bae65af",
"id": "0cae9900-6e54-11ec-a124-bfe603780ab8",
"group": "default",
"params": {
"message": "Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts"

View file

@ -10,6 +10,8 @@ import { CollectorFetchContext } from '../../../../../src/plugins/usage_collecti
import { CollectorDependencies } from './types';
import { fetchDetectionsMetrics } from './detections';
import { SAVED_OBJECT_TYPES } from '../../../cases/common/constants';
// eslint-disable-next-line no-restricted-imports
import { legacyRuleActionsSavedObjectType } from '../lib/detection_engine/rule_actions/legacy_saved_object_mappings';
export type RegisterCollector = (deps: CollectorDependencies) => void;
export interface UsageData {
@ -19,7 +21,11 @@ export interface UsageData {
export async function getInternalSavedObjectsClient(core: CoreSetup) {
return core.getStartServices().then(async ([coreStart]) => {
// note: we include the "cases" and "alert" hidden types here otherwise we would not be able to query them. If at some point cases and alert is not considered a hidden type this can be removed
return coreStart.savedObjects.createInternalRepository(['alert', ...SAVED_OBJECT_TYPES]);
return coreStart.savedObjects.createInternalRepository([
'alert',
legacyRuleActionsSavedObjectType,
...SAVED_OBJECT_TYPES,
]);
});
}
@ -51,6 +57,22 @@ export const registerCollector: RegisterCollector = ({
type: 'long',
_meta: { description: 'Number of cases attached to query detection rule alerts' },
},
legacy_notifications_enabled: {
type: 'long',
_meta: { description: 'Number of legacy notifications enabled' },
},
legacy_notifications_disabled: {
type: 'long',
_meta: { description: 'Number of legacy notifications disabled' },
},
notifications_enabled: {
type: 'long',
_meta: { description: 'Number of notifications enabled' },
},
notifications_disabled: {
type: 'long',
_meta: { description: 'Number of notifications enabled' },
},
},
threshold: {
enabled: {
@ -71,6 +93,22 @@ export const registerCollector: RegisterCollector = ({
description: 'Number of cases attached to threshold detection rule alerts',
},
},
legacy_notifications_enabled: {
type: 'long',
_meta: { description: 'Number of legacy notifications enabled' },
},
legacy_notifications_disabled: {
type: 'long',
_meta: { description: 'Number of legacy notifications disabled' },
},
notifications_enabled: {
type: 'long',
_meta: { description: 'Number of notifications enabled' },
},
notifications_disabled: {
type: 'long',
_meta: { description: 'Number of notifications enabled' },
},
},
eql: {
enabled: { type: 'long', _meta: { description: 'Number of eql rules enabled' } },
@ -83,6 +121,22 @@ export const registerCollector: RegisterCollector = ({
type: 'long',
_meta: { description: 'Number of cases attached to eql detection rule alerts' },
},
legacy_notifications_enabled: {
type: 'long',
_meta: { description: 'Number of legacy notifications enabled' },
},
legacy_notifications_disabled: {
type: 'long',
_meta: { description: 'Number of legacy notifications disabled' },
},
notifications_enabled: {
type: 'long',
_meta: { description: 'Number of notifications enabled' },
},
notifications_disabled: {
type: 'long',
_meta: { description: 'Number of notifications enabled' },
},
},
machine_learning: {
enabled: {
@ -103,6 +157,22 @@ export const registerCollector: RegisterCollector = ({
description: 'Number of cases attached to machine_learning detection rule alerts',
},
},
legacy_notifications_enabled: {
type: 'long',
_meta: { description: 'Number of legacy notifications enabled' },
},
legacy_notifications_disabled: {
type: 'long',
_meta: { description: 'Number of legacy notifications disabled' },
},
notifications_enabled: {
type: 'long',
_meta: { description: 'Number of notifications enabled' },
},
notifications_disabled: {
type: 'long',
_meta: { description: 'Number of notifications enabled' },
},
},
threat_match: {
enabled: {
@ -123,11 +193,21 @@ export const registerCollector: RegisterCollector = ({
description: 'Number of cases attached to threat_match detection rule alerts',
},
},
},
legacy_notifications: {
total: {
legacy_notifications_enabled: {
type: 'long',
_meta: { description: 'Number of legacy notifications still in use' },
_meta: { description: 'Number of legacy notifications enabled' },
},
legacy_notifications_disabled: {
type: 'long',
_meta: { description: 'Number of legacy notifications disabled' },
},
notifications_enabled: {
type: 'long',
_meta: { description: 'Number of notifications enabled' },
},
notifications_disabled: {
type: 'long',
_meta: { description: 'Number of notifications enabled' },
},
},
elastic_total: {
@ -144,6 +224,22 @@ export const registerCollector: RegisterCollector = ({
type: 'long',
_meta: { description: 'Number of cases attached to elastic detection rule alerts' },
},
legacy_notifications_enabled: {
type: 'long',
_meta: { description: 'Number of legacy notifications enabled' },
},
legacy_notifications_disabled: {
type: 'long',
_meta: { description: 'Number of legacy notifications disabled' },
},
notifications_enabled: {
type: 'long',
_meta: { description: 'Number of notifications enabled' },
},
notifications_disabled: {
type: 'long',
_meta: { description: 'Number of notifications enabled' },
},
},
custom_total: {
enabled: { type: 'long', _meta: { description: 'Number of custom rules enabled' } },
@ -156,6 +252,22 @@ export const registerCollector: RegisterCollector = ({
type: 'long',
_meta: { description: 'Number of cases attached to custom detection rule alerts' },
},
legacy_notifications_enabled: {
type: 'long',
_meta: { description: 'Number of legacy notifications enabled' },
},
legacy_notifications_disabled: {
type: 'long',
_meta: { description: 'Number of legacy notifications disabled' },
},
notifications_enabled: {
type: 'long',
_meta: { description: 'Number of notifications enabled' },
},
notifications_disabled: {
type: 'long',
_meta: { description: 'Number of notifications enabled' },
},
},
},
detection_rule_detail: {
@ -198,6 +310,14 @@ export const registerCollector: RegisterCollector = ({
type: 'long',
_meta: { description: 'The number of total cases generated by a rule' },
},
has_legacy_notification: {
type: 'boolean',
_meta: { description: 'True if this rule has a legacy notification' },
},
has_notification: {
type: 'boolean',
_meta: { description: 'True if this rule has a notification' },
},
},
},
},

View file

@ -8,13 +8,25 @@
import { initialDetectionRulesUsage, updateDetectionRuleUsage } from './detection_rule_helpers';
import { DetectionRuleMetric, DetectionRulesTypeUsage } from './types';
const createStubRule = (
ruleType: string,
enabled: boolean,
elasticRule: boolean,
alertCount: number,
caseCount: number
): DetectionRuleMetric => ({
interface StubRuleOptions {
ruleType: string;
enabled: boolean;
elasticRule: boolean;
alertCount: number;
caseCount: number;
hasLegacyNotification: boolean;
hasNotification: boolean;
}
const createStubRule = ({
ruleType,
enabled,
elasticRule,
alertCount,
caseCount,
hasLegacyNotification,
hasNotification,
}: StubRuleOptions): DetectionRuleMetric => ({
rule_name: 'rule-name',
rule_id: 'id-123',
rule_type: ruleType,
@ -25,12 +37,22 @@ const createStubRule = (
updated_on: '2022-01-06T20:02:45.306Z',
alert_count_daily: alertCount,
cases_count_total: caseCount,
has_legacy_notification: hasLegacyNotification,
has_notification: hasNotification,
});
describe('Detections Usage and Metrics', () => {
describe('Update metrics with rule information', () => {
it('Should update elastic and eql rule metric total', async () => {
const stubRule = createStubRule('eql', true, true, 1, 1);
const stubRule = createStubRule({
ruleType: 'eql',
enabled: true,
elasticRule: true,
alertCount: 1,
caseCount: 1,
hasLegacyNotification: false,
hasNotification: false,
});
const usage = updateDetectionRuleUsage(stubRule, initialDetectionRulesUsage);
expect(usage).toEqual<DetectionRulesTypeUsage>({
@ -40,22 +62,70 @@ describe('Detections Usage and Metrics', () => {
cases: 1,
disabled: 0,
enabled: 1,
legacy_notifications_enabled: 0,
legacy_notifications_disabled: 0,
notifications_enabled: 0,
notifications_disabled: 0,
},
eql: {
alerts: 1,
cases: 1,
disabled: 0,
enabled: 1,
legacy_notifications_enabled: 0,
legacy_notifications_disabled: 0,
notifications_enabled: 0,
notifications_disabled: 0,
},
});
});
it('Should update based on multiple metrics', async () => {
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);
const stubEqlRule = createStubRule({
ruleType: 'eql',
enabled: true,
elasticRule: true,
alertCount: 1,
caseCount: 1,
hasLegacyNotification: false,
hasNotification: false,
});
const stubQueryRuleOne = createStubRule({
ruleType: 'query',
enabled: true,
elasticRule: true,
alertCount: 5,
caseCount: 2,
hasLegacyNotification: false,
hasNotification: false,
});
const stubQueryRuleTwo = createStubRule({
ruleType: 'query',
enabled: true,
elasticRule: false,
alertCount: 5,
caseCount: 2,
hasLegacyNotification: false,
hasNotification: false,
});
const stubMachineLearningOne = createStubRule({
ruleType: 'machine_learning',
enabled: false,
elasticRule: false,
alertCount: 0,
caseCount: 10,
hasLegacyNotification: false,
hasNotification: false,
});
const stubMachineLearningTwo = createStubRule({
ruleType: 'machine_learning',
enabled: true,
elasticRule: true,
alertCount: 22,
caseCount: 44,
hasLegacyNotification: false,
hasNotification: false,
});
let usage = updateDetectionRuleUsage(stubEqlRule, initialDetectionRulesUsage);
usage = updateDetectionRuleUsage(stubQueryRuleOne, usage);
@ -70,32 +140,152 @@ describe('Detections Usage and Metrics', () => {
cases: 12,
disabled: 1,
enabled: 1,
legacy_notifications_enabled: 0,
legacy_notifications_disabled: 0,
notifications_enabled: 0,
notifications_disabled: 0,
},
elastic_total: {
alerts: 28,
cases: 47,
disabled: 0,
enabled: 3,
legacy_notifications_enabled: 0,
legacy_notifications_disabled: 0,
notifications_enabled: 0,
notifications_disabled: 0,
},
eql: {
alerts: 1,
cases: 1,
disabled: 0,
enabled: 1,
legacy_notifications_enabled: 0,
legacy_notifications_disabled: 0,
notifications_enabled: 0,
notifications_disabled: 0,
},
machine_learning: {
alerts: 22,
cases: 54,
disabled: 1,
enabled: 1,
legacy_notifications_enabled: 0,
legacy_notifications_disabled: 0,
notifications_enabled: 0,
notifications_disabled: 0,
},
query: {
alerts: 10,
cases: 4,
disabled: 0,
enabled: 2,
legacy_notifications_enabled: 0,
legacy_notifications_disabled: 0,
notifications_enabled: 0,
notifications_disabled: 0,
},
});
});
describe('table tests of "ruleType", "enabled", "elasticRule", and "legacyNotification"', () => {
test.each`
ruleType | enabled | hasLegacyNotification | hasNotification | expectedLegacyNotificationsEnabled | expectedLegacyNotificationsDisabled | expectedNotificationsEnabled | expectedNotificationsDisabled
${'eql'} | ${true} | ${true} | ${false} | ${1} | ${0} | ${0} | ${0}
${'eql'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0}
${'eql'} | ${false} | ${false} | ${true} | ${0} | ${0} | ${0} | ${1}
${'eql'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0}
${'eql'} | ${false} | ${true} | ${false} | ${0} | ${1} | ${0} | ${0}
${'eql'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0}
${'query'} | ${true} | ${true} | ${false} | ${1} | ${0} | ${0} | ${0}
${'query'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0}
${'query'} | ${false} | ${false} | ${true} | ${0} | ${0} | ${0} | ${1}
${'query'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0}
${'query'} | ${false} | ${true} | ${false} | ${0} | ${1} | ${0} | ${0}
${'query'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0}
${'threshold'} | ${true} | ${true} | ${false} | ${1} | ${0} | ${0} | ${0}
${'threshold'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0}
${'threshold'} | ${false} | ${false} | ${true} | ${0} | ${0} | ${0} | ${1}
${'threshold'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0}
${'threshold'} | ${false} | ${true} | ${false} | ${0} | ${1} | ${0} | ${0}
${'threshold'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0}
${'machine_learning'} | ${true} | ${true} | ${false} | ${1} | ${0} | ${0} | ${0}
${'machine_learning'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0}
${'machine_learning'} | ${false} | ${false} | ${true} | ${0} | ${0} | ${0} | ${1}
${'machine_learning'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0}
${'machine_learning'} | ${false} | ${true} | ${false} | ${0} | ${1} | ${0} | ${0}
${'machine_learning'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0}
${'threat_match'} | ${true} | ${true} | ${false} | ${1} | ${0} | ${0} | ${0}
${'threat_match'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0}
${'threat_match'} | ${false} | ${false} | ${true} | ${0} | ${0} | ${0} | ${1}
${'threat_match'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0}
${'threat_match'} | ${false} | ${true} | ${false} | ${0} | ${1} | ${0} | ${0}
${'threat_match'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0}
`(
'expect { "ruleType": $ruleType, "enabled": $enabled, "hasLegacyNotification": $hasLegacyNotification, "hasNotification": $hasNotification } to equal { legacy_notifications_enabled: $expectedLegacyNotificationsEnabled, legacy_notifications_disabled: $expectedLegacyNotificationsDisabled, notifications_enabled: $expectedNotificationsEnabled, notifications_disabled, $expectedNotificationsDisabled }',
({
ruleType,
enabled,
hasLegacyNotification,
hasNotification,
expectedLegacyNotificationsEnabled,
expectedLegacyNotificationsDisabled,
expectedNotificationsEnabled,
expectedNotificationsDisabled,
}) => {
const rule1 = createStubRule({
ruleType,
enabled,
elasticRule: false,
hasLegacyNotification,
hasNotification,
alertCount: 0,
caseCount: 0,
});
const usage = updateDetectionRuleUsage(rule1, initialDetectionRulesUsage) as ReturnType<
typeof updateDetectionRuleUsage
> & { [key: string]: unknown };
expect(usage[ruleType]).toEqual(
expect.objectContaining({
legacy_notifications_enabled: expectedLegacyNotificationsEnabled,
legacy_notifications_disabled: expectedLegacyNotificationsDisabled,
notifications_enabled: expectedNotificationsEnabled,
notifications_disabled: expectedNotificationsDisabled,
})
);
// extra test where we add everything by 1 to ensure that the addition happens with the correct rule type
const rule2 = createStubRule({
ruleType,
enabled,
elasticRule: false,
hasLegacyNotification,
hasNotification,
alertCount: 0,
caseCount: 0,
});
const usageAddedByOne = updateDetectionRuleUsage(rule2, usage) as ReturnType<
typeof updateDetectionRuleUsage
> & { [key: string]: unknown };
expect(usageAddedByOne[ruleType]).toEqual(
expect.objectContaining({
legacy_notifications_enabled:
expectedLegacyNotificationsEnabled !== 0
? expectedLegacyNotificationsEnabled + 1
: 0,
legacy_notifications_disabled:
expectedLegacyNotificationsDisabled !== 0
? expectedLegacyNotificationsDisabled + 1
: 0,
notifications_enabled:
expectedNotificationsEnabled !== 0 ? expectedNotificationsEnabled + 1 : 0,
notifications_disabled:
expectedNotificationsDisabled !== 0 ? expectedNotificationsDisabled + 1 : 0,
})
);
}
);
});
});
});

View file

@ -15,7 +15,6 @@ import {
SAVED_QUERY_RULE_TYPE_ID,
} from '@kbn/securitysolution-rules';
import { ALERT_RULE_UUID } from '@kbn/rule-data-utils';
import { LEGACY_NOTIFICATIONS_ID } from '../../../common/constants';
import { CASE_COMMENT_SAVED_OBJECT } from '../../../../cases/common/constants';
import { ElasticsearchClient, SavedObjectsClientContract } from '../../../../../../src/core/server';
@ -30,6 +29,10 @@ import type {
RuleSearchResult,
DetectionMetrics,
} from './types';
// eslint-disable-next-line no-restricted-imports
import { legacyRuleActionsSavedObjectType } from '../../lib/detection_engine/rule_actions/legacy_saved_object_mappings';
// eslint-disable-next-line no-restricted-imports
import { LegacyIRuleActionsAttributesSavedObjectAttributes } from '../../lib/detection_engine/rule_actions/legacy_types';
/**
* Initial detection metrics initialized.
@ -63,45 +66,70 @@ export const initialDetectionRulesUsage: DetectionRulesTypeUsage = {
disabled: 0,
alerts: 0,
cases: 0,
legacy_notifications_enabled: 0,
legacy_notifications_disabled: 0,
notifications_enabled: 0,
notifications_disabled: 0,
},
threshold: {
enabled: 0,
disabled: 0,
alerts: 0,
cases: 0,
legacy_notifications_enabled: 0,
legacy_notifications_disabled: 0,
notifications_enabled: 0,
notifications_disabled: 0,
},
eql: {
enabled: 0,
disabled: 0,
alerts: 0,
cases: 0,
legacy_notifications_enabled: 0,
legacy_notifications_disabled: 0,
notifications_enabled: 0,
notifications_disabled: 0,
},
machine_learning: {
enabled: 0,
disabled: 0,
alerts: 0,
cases: 0,
legacy_notifications_enabled: 0,
legacy_notifications_disabled: 0,
notifications_enabled: 0,
notifications_disabled: 0,
},
threat_match: {
enabled: 0,
disabled: 0,
alerts: 0,
cases: 0,
legacy_notifications_enabled: 0,
legacy_notifications_disabled: 0,
notifications_enabled: 0,
notifications_disabled: 0,
},
elastic_total: {
enabled: 0,
disabled: 0,
alerts: 0,
cases: 0,
},
legacy_notifications: {
total: 0,
legacy_notifications_enabled: 0,
legacy_notifications_disabled: 0,
notifications_enabled: 0,
notifications_disabled: 0,
},
custom_total: {
enabled: 0,
disabled: 0,
alerts: 0,
cases: 0,
legacy_notifications_enabled: 0,
legacy_notifications_disabled: 0,
notifications_enabled: 0,
notifications_disabled: 0,
},
};
@ -112,6 +140,16 @@ export const updateDetectionRuleUsage = (
): DetectionRulesTypeUsage => {
let updatedUsage = usage;
const legacyNotificationEnabled =
detectionRuleMetric.has_legacy_notification && detectionRuleMetric.enabled;
const legacyNotificationDisabled =
detectionRuleMetric.has_legacy_notification && !detectionRuleMetric.enabled;
const notificationEnabled = detectionRuleMetric.has_notification && detectionRuleMetric.enabled;
const notificationDisabled = detectionRuleMetric.has_notification && !detectionRuleMetric.enabled;
if (detectionRuleMetric.rule_type === 'query') {
updatedUsage = {
...usage,
@ -121,6 +159,18 @@ export const updateDetectionRuleUsage = (
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_total,
legacy_notifications_enabled: legacyNotificationEnabled
? usage.query.legacy_notifications_enabled + 1
: usage.query.legacy_notifications_enabled,
legacy_notifications_disabled: legacyNotificationDisabled
? usage.query.legacy_notifications_disabled + 1
: usage.query.legacy_notifications_disabled,
notifications_enabled: notificationEnabled
? usage.query.notifications_enabled + 1
: usage.query.notifications_enabled,
notifications_disabled: notificationDisabled
? usage.query.notifications_disabled + 1
: usage.query.notifications_disabled,
},
};
} else if (detectionRuleMetric.rule_type === 'threshold') {
@ -136,6 +186,18 @@ export const updateDetectionRuleUsage = (
: usage.threshold.disabled,
alerts: usage.threshold.alerts + detectionRuleMetric.alert_count_daily,
cases: usage.threshold.cases + detectionRuleMetric.cases_count_total,
legacy_notifications_enabled: legacyNotificationEnabled
? usage.threshold.legacy_notifications_enabled + 1
: usage.threshold.legacy_notifications_enabled,
legacy_notifications_disabled: legacyNotificationDisabled
? usage.threshold.legacy_notifications_disabled + 1
: usage.threshold.legacy_notifications_disabled,
notifications_enabled: notificationEnabled
? usage.threshold.notifications_enabled + 1
: usage.threshold.notifications_enabled,
notifications_disabled: notificationDisabled
? usage.threshold.notifications_disabled + 1
: usage.threshold.notifications_disabled,
},
};
} else if (detectionRuleMetric.rule_type === 'eql') {
@ -147,6 +209,18 @@ export const updateDetectionRuleUsage = (
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_total,
legacy_notifications_enabled: legacyNotificationEnabled
? usage.eql.legacy_notifications_enabled + 1
: usage.eql.legacy_notifications_enabled,
legacy_notifications_disabled: legacyNotificationDisabled
? usage.eql.legacy_notifications_disabled + 1
: usage.eql.legacy_notifications_disabled,
notifications_enabled: notificationEnabled
? usage.eql.notifications_enabled + 1
: usage.eql.notifications_enabled,
notifications_disabled: notificationDisabled
? usage.eql.notifications_disabled + 1
: usage.eql.notifications_disabled,
},
};
} else if (detectionRuleMetric.rule_type === 'machine_learning') {
@ -162,6 +236,18 @@ export const updateDetectionRuleUsage = (
: usage.machine_learning.disabled,
alerts: usage.machine_learning.alerts + detectionRuleMetric.alert_count_daily,
cases: usage.machine_learning.cases + detectionRuleMetric.cases_count_total,
legacy_notifications_enabled: legacyNotificationEnabled
? usage.machine_learning.legacy_notifications_enabled + 1
: usage.machine_learning.legacy_notifications_enabled,
legacy_notifications_disabled: legacyNotificationDisabled
? usage.machine_learning.legacy_notifications_disabled + 1
: usage.machine_learning.legacy_notifications_disabled,
notifications_enabled: notificationEnabled
? usage.machine_learning.notifications_enabled + 1
: usage.machine_learning.notifications_enabled,
notifications_disabled: notificationDisabled
? usage.machine_learning.notifications_disabled + 1
: usage.machine_learning.notifications_disabled,
},
};
} else if (detectionRuleMetric.rule_type === 'threat_match') {
@ -177,6 +263,18 @@ export const updateDetectionRuleUsage = (
: usage.threat_match.disabled,
alerts: usage.threat_match.alerts + detectionRuleMetric.alert_count_daily,
cases: usage.threat_match.cases + detectionRuleMetric.cases_count_total,
legacy_notifications_enabled: legacyNotificationEnabled
? usage.threat_match.legacy_notifications_enabled + 1
: usage.threat_match.legacy_notifications_enabled,
legacy_notifications_disabled: legacyNotificationDisabled
? usage.threat_match.legacy_notifications_disabled + 1
: usage.threat_match.legacy_notifications_disabled,
notifications_enabled: notificationEnabled
? usage.threat_match.notifications_enabled + 1
: usage.threat_match.notifications_enabled,
notifications_disabled: notificationDisabled
? usage.threat_match.notifications_disabled + 1
: usage.threat_match.notifications_disabled,
},
};
}
@ -194,6 +292,18 @@ export const updateDetectionRuleUsage = (
: updatedUsage.elastic_total.disabled,
alerts: updatedUsage.elastic_total.alerts + detectionRuleMetric.alert_count_daily,
cases: updatedUsage.elastic_total.cases + detectionRuleMetric.cases_count_total,
legacy_notifications_enabled: legacyNotificationEnabled
? updatedUsage.elastic_total.legacy_notifications_enabled + 1
: updatedUsage.elastic_total.legacy_notifications_enabled,
legacy_notifications_disabled: legacyNotificationDisabled
? updatedUsage.elastic_total.legacy_notifications_disabled + 1
: updatedUsage.elastic_total.legacy_notifications_disabled,
notifications_enabled: notificationEnabled
? updatedUsage.elastic_total.notifications_enabled + 1
: updatedUsage.elastic_total.notifications_enabled,
notifications_disabled: notificationDisabled
? updatedUsage.elastic_total.notifications_disabled + 1
: updatedUsage.elastic_total.notifications_disabled,
},
};
} else {
@ -209,6 +319,18 @@ export const updateDetectionRuleUsage = (
: updatedUsage.custom_total.disabled,
alerts: updatedUsage.custom_total.alerts + detectionRuleMetric.alert_count_daily,
cases: updatedUsage.custom_total.cases + detectionRuleMetric.cases_count_total,
legacy_notifications_enabled: legacyNotificationEnabled
? updatedUsage.custom_total.legacy_notifications_enabled + 1
: updatedUsage.custom_total.legacy_notifications_enabled,
legacy_notifications_disabled: legacyNotificationDisabled
? updatedUsage.custom_total.legacy_notifications_disabled + 1
: updatedUsage.custom_total.legacy_notifications_disabled,
notifications_enabled: notificationEnabled
? updatedUsage.custom_total.notifications_enabled + 1
: updatedUsage.custom_total.notifications_enabled,
notifications_disabled: notificationDisabled
? updatedUsage.custom_total.notifications_disabled + 1
: updatedUsage.custom_total.notifications_disabled,
},
};
}
@ -287,18 +409,28 @@ export const getDetectionRuleMetrics = async (
filter: `${CASE_COMMENT_SAVED_OBJECT}.attributes.type: alert`,
});
// We get just 1 per a single page so we can get the total count to add to the rulesUsage.
// Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function.
const legacyNotificationsCount = (
await savedObjectClient.find({
type: 'alert',
const legacyRuleActions =
await savedObjectClient.find<LegacyIRuleActionsAttributesSavedObjectAttributes>({
type: legacyRuleActionsSavedObjectType,
page: 1,
perPage: MAX_RESULTS_WINDOW,
namespaces: ['*'],
perPage: 1,
filter: `alert.attributes.alertTypeId: ${LEGACY_NOTIFICATIONS_ID}`,
})
).total;
rulesUsage = { ...rulesUsage, legacy_notifications: { total: legacyNotificationsCount } };
});
const legacyNotificationRuleIds = legacyRuleActions.saved_objects.reduce(
(cache, legacyNotificationsObject) => {
const ruleRef = legacyNotificationsObject.references.find(
(reference) => reference.name === 'alert_0' && reference.type === 'alert'
);
if (ruleRef != null) {
const enabled = legacyNotificationsObject.attributes.ruleThrottle !== 'no_actions';
cache.set(ruleRef.id, { enabled });
}
return cache;
},
new Map<string, { enabled: boolean }>()
);
const casesCache = cases.saved_objects.reduce((cache, { attributes: casesObject }) => {
const ruleId = casesObject.rule.id;
@ -320,6 +452,17 @@ export const getDetectionRuleMetrics = async (
const ruleObjects = ruleResults.hits.hits.map((hit) => {
const ruleId = hit._id.split(':')[1];
const isElastic = isElasticRule(hit._source?.alert.tags);
// Even if the legacy notification is set to "no_actions" we still count the rule as having a legacy notification that is not migrated yet.
const hasLegacyNotification = legacyNotificationRuleIds.get(ruleId) != null;
// We only count a rule as having a notification and being "enabled" if it is _not_ set to "no_actions"/"muteAll" and it has at least one action within its array.
const hasNotification =
!hasLegacyNotification &&
hit._source?.alert.actions != null &&
hit._source?.alert.actions.length > 0 &&
hit._source?.alert.muteAll !== true;
return {
rule_name: hit._source?.alert.name,
rule_id: hit._source?.alert.params.ruleId,
@ -331,6 +474,8 @@ export const getDetectionRuleMetrics = async (
updated_on: hit._source?.alert.updatedAt,
alert_count_daily: alertsCache.get(ruleId) || 0,
cases_count_total: casesCache.get(ruleId) || 0,
has_legacy_notification: hasLegacyNotification,
has_notification: hasNotification,
} as DetectionRuleMetric;
});

View file

@ -70,6 +70,8 @@ describe('Detections Usage and Metrics', () => {
rule_type: 'query',
rule_version: 4,
updated_on: '2021-03-23T17:15:59.634Z',
has_legacy_notification: false,
has_notification: false,
},
],
detection_rule_usage: {
@ -79,15 +81,20 @@ describe('Detections Usage and Metrics', () => {
disabled: 1,
alerts: 3400,
cases: 1,
legacy_notifications_enabled: 0,
legacy_notifications_disabled: 0,
notifications_enabled: 0,
notifications_disabled: 0,
},
elastic_total: {
alerts: 3400,
cases: 1,
disabled: 1,
enabled: 0,
},
legacy_notifications: {
total: 4,
legacy_notifications_enabled: 0,
legacy_notifications_disabled: 0,
notifications_enabled: 0,
notifications_disabled: 0,
},
},
},
@ -118,15 +125,20 @@ describe('Detections Usage and Metrics', () => {
cases: 1,
disabled: 1,
enabled: 0,
legacy_notifications_enabled: 0,
legacy_notifications_disabled: 0,
notifications_enabled: 0,
notifications_disabled: 0,
},
query: {
alerts: 800,
cases: 1,
disabled: 1,
enabled: 0,
},
legacy_notifications: {
total: 4,
legacy_notifications_enabled: 0,
legacy_notifications_disabled: 0,
notifications_enabled: 0,
notifications_disabled: 0,
},
},
},
@ -144,7 +156,7 @@ describe('Detections Usage and Metrics', () => {
savedObjectsClient.find.mockResolvedValue(getMockAlertCasesResponse());
const result = await fetchDetectionsMetrics('', '', esClientMock, savedObjectsClient, mlMock);
expect(result).toEqual({
expect(result).toEqual<DetectionMetrics>({
...getInitialDetectionMetrics(),
detection_rules: {
detection_rule_detail: [
@ -159,6 +171,8 @@ describe('Detections Usage and Metrics', () => {
rule_type: 'query',
rule_version: 4,
updated_on: '2021-03-23T17:15:59.634Z',
has_legacy_notification: false,
has_notification: false,
},
],
detection_rule_usage: {
@ -168,15 +182,20 @@ describe('Detections Usage and Metrics', () => {
cases: 1,
disabled: 1,
enabled: 0,
legacy_notifications_enabled: 0,
legacy_notifications_disabled: 0,
notifications_enabled: 0,
notifications_disabled: 0,
},
query: {
alerts: 0,
cases: 1,
disabled: 1,
enabled: 0,
},
legacy_notifications: {
total: 4,
legacy_notifications_enabled: 0,
legacy_notifications_disabled: 0,
notifications_enabled: 0,
notifications_disabled: 0,
},
},
},

View file

@ -30,7 +30,9 @@ export interface RuleSearchResult {
tags: string[];
createdAt: string;
updatedAt: string;
muteAll: boolean | undefined | null;
params: DetectionRuleParms;
actions: unknown[];
};
}
@ -55,8 +57,11 @@ interface FeatureTypeUsage {
disabled: number;
alerts: number;
cases: number;
legacy_notifications_enabled: number;
legacy_notifications_disabled: number;
notifications_enabled: number;
notifications_disabled: number;
}
export interface DetectionRulesTypeUsage {
query: FeatureTypeUsage;
threshold: FeatureTypeUsage;
@ -65,7 +70,6 @@ export interface DetectionRulesTypeUsage {
threat_match: FeatureTypeUsage;
elastic_total: FeatureTypeUsage;
custom_total: FeatureTypeUsage;
legacy_notifications: LegacyNotifications;
}
export interface MlJobsUsage {
@ -129,6 +133,8 @@ export interface DetectionRuleMetric {
updated_on: string;
alert_count_daily: number;
cases_count_total: number;
has_legacy_notification: boolean;
has_notification: boolean;
}
export interface AlertsAggregationResponse {
@ -162,11 +168,3 @@ export interface DetectionRuleAdoption {
detection_rule_detail: DetectionRuleMetric[];
detection_rule_usage: DetectionRulesTypeUsage;
}
/**
* The legacy notifications that are still in use.
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
*/
export interface LegacyNotifications {
total: number;
}

View file

@ -6986,6 +6986,30 @@
"_meta": {
"description": "Number of cases attached to query detection rule alerts"
}
},
"legacy_notifications_enabled": {
"type": "long",
"_meta": {
"description": "Number of legacy notifications enabled"
}
},
"legacy_notifications_disabled": {
"type": "long",
"_meta": {
"description": "Number of legacy notifications disabled"
}
},
"notifications_enabled": {
"type": "long",
"_meta": {
"description": "Number of notifications enabled"
}
},
"notifications_disabled": {
"type": "long",
"_meta": {
"description": "Number of notifications enabled"
}
}
}
},
@ -7014,6 +7038,30 @@
"_meta": {
"description": "Number of cases attached to threshold detection rule alerts"
}
},
"legacy_notifications_enabled": {
"type": "long",
"_meta": {
"description": "Number of legacy notifications enabled"
}
},
"legacy_notifications_disabled": {
"type": "long",
"_meta": {
"description": "Number of legacy notifications disabled"
}
},
"notifications_enabled": {
"type": "long",
"_meta": {
"description": "Number of notifications enabled"
}
},
"notifications_disabled": {
"type": "long",
"_meta": {
"description": "Number of notifications enabled"
}
}
}
},
@ -7042,6 +7090,30 @@
"_meta": {
"description": "Number of cases attached to eql detection rule alerts"
}
},
"legacy_notifications_enabled": {
"type": "long",
"_meta": {
"description": "Number of legacy notifications enabled"
}
},
"legacy_notifications_disabled": {
"type": "long",
"_meta": {
"description": "Number of legacy notifications disabled"
}
},
"notifications_enabled": {
"type": "long",
"_meta": {
"description": "Number of notifications enabled"
}
},
"notifications_disabled": {
"type": "long",
"_meta": {
"description": "Number of notifications enabled"
}
}
}
},
@ -7070,6 +7142,30 @@
"_meta": {
"description": "Number of cases attached to machine_learning detection rule alerts"
}
},
"legacy_notifications_enabled": {
"type": "long",
"_meta": {
"description": "Number of legacy notifications enabled"
}
},
"legacy_notifications_disabled": {
"type": "long",
"_meta": {
"description": "Number of legacy notifications disabled"
}
},
"notifications_enabled": {
"type": "long",
"_meta": {
"description": "Number of notifications enabled"
}
},
"notifications_disabled": {
"type": "long",
"_meta": {
"description": "Number of notifications enabled"
}
}
}
},
@ -7098,15 +7194,29 @@
"_meta": {
"description": "Number of cases attached to threat_match detection rule alerts"
}
}
}
},
"legacy_notifications": {
"properties": {
"total": {
},
"legacy_notifications_enabled": {
"type": "long",
"_meta": {
"description": "Number of legacy notifications still in use"
"description": "Number of legacy notifications enabled"
}
},
"legacy_notifications_disabled": {
"type": "long",
"_meta": {
"description": "Number of legacy notifications disabled"
}
},
"notifications_enabled": {
"type": "long",
"_meta": {
"description": "Number of notifications enabled"
}
},
"notifications_disabled": {
"type": "long",
"_meta": {
"description": "Number of notifications enabled"
}
}
}
@ -7136,6 +7246,30 @@
"_meta": {
"description": "Number of cases attached to elastic detection rule alerts"
}
},
"legacy_notifications_enabled": {
"type": "long",
"_meta": {
"description": "Number of legacy notifications enabled"
}
},
"legacy_notifications_disabled": {
"type": "long",
"_meta": {
"description": "Number of legacy notifications disabled"
}
},
"notifications_enabled": {
"type": "long",
"_meta": {
"description": "Number of notifications enabled"
}
},
"notifications_disabled": {
"type": "long",
"_meta": {
"description": "Number of notifications enabled"
}
}
}
},
@ -7164,6 +7298,30 @@
"_meta": {
"description": "Number of cases attached to custom detection rule alerts"
}
},
"legacy_notifications_enabled": {
"type": "long",
"_meta": {
"description": "Number of legacy notifications enabled"
}
},
"legacy_notifications_disabled": {
"type": "long",
"_meta": {
"description": "Number of legacy notifications disabled"
}
},
"notifications_enabled": {
"type": "long",
"_meta": {
"description": "Number of notifications enabled"
}
},
"notifications_disabled": {
"type": "long",
"_meta": {
"description": "Number of notifications enabled"
}
}
}
}
@ -7232,6 +7390,18 @@
"_meta": {
"description": "The number of total cases generated by a rule"
}
},
"has_legacy_notification": {
"type": "boolean",
"_meta": {
"description": "True if this rule has a legacy notification"
}
},
"has_notification": {
"type": "boolean",
"_meta": {
"description": "True if this rule has a notification"
}
}
}
}

View file

@ -1,8 +1,6 @@
These are tests for the telemetry rules within "security_solution/server/usage"
* detection_rules
* legacy_notifications
Detection rules are tests around each of the rule types to affirm they work such as query, eql, etc...
Legacy notifications are tests around the legacy notification telemetry. Once legacy notifications are removed,
these tests can be removed too.
Detection rules are tests around each of the rule types to affirm they work such as query, eql, etc... This includes
legacy notifications. Once legacy notifications are moved, those tests can be removed too.

View file

@ -13,7 +13,6 @@ export default ({ loadTestFile }: FtrProviderContext): void => {
describe('', function () {
this.tags('ciGroup11');
loadTestFile(require.resolve('./detection_rules'));
loadTestFile(require.resolve('./legacy_notifications'));
});
});
};

View file

@ -1,75 +0,0 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
import {
createRule,
createSignalsIndex,
deleteAllAlerts,
deleteSignalsIndex,
getSimpleRule,
getStats,
getWebHookAction,
} from '../../../utils';
// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest');
const log = getService('log');
const retry = getService('retry');
describe('legacy notification telemetry', async () => {
beforeEach(async () => {
await createSignalsIndex(supertest, log);
});
afterEach(async () => {
await deleteSignalsIndex(supertest, log);
await deleteAllAlerts(supertest, log);
});
it('should have 1 legacy notification when there is a rule on the default', async () => {
// create an connector/action
const { body: hookAction } = await supertest
.post('/api/actions/action')
.set('kbn-xsrf', 'true')
.send(getWebHookAction())
.expect(200);
// create a rule without actions
const createRuleBody = await createRule(supertest, log, getSimpleRule('rule-1'));
// attach the legacy notification
await supertest
.post(`/internal/api/detection/legacy/notifications?alert_id=${createRuleBody.id}`)
.set('kbn-xsrf', 'true')
.send({
name: 'Legacy notification with one action',
interval: '1h',
actions: [
{
id: hookAction.id,
group: 'default',
params: {
message:
'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts',
},
actionTypeId: hookAction.actionTypeId,
},
],
})
.expect(200);
await retry.try(async () => {
const stats = await getStats(supertest, log);
// NOTE: We have to do "above 0" until this bug is fixed: https://github.com/elastic/kibana/issues/122456 because other tests are accumulating non-cleaned up legacy actions/notifications and this number isn't reliable at the moment
expect(stats.detection_rules.detection_rule_usage.legacy_notifications.total).to.above(0);
});
});
});
};