[Security Solution] Migrates siem-detection-engine-rule-status alertId to saved object references array (#114585)

## Summary

Resolves (a portion of) https://github.com/elastic/kibana/issues/107068 for the `siem-detection-engine-rule-status` type by migrating the `alertId` to be within the `SO references[]`. Based on: https://github.com/elastic/kibana/pull/113577

* Migrates the legacy `siem-detection-engine-rule-status` `alertId` to saved object references array
* Adds an e2e test for `siem-detection-engine-rule-status` 
* Breaks out `siem-detection-engine-rule-status` & `security-rule` SO's to their own dedicated files/directories, and cleaned up typings/imports


Before migration you can observe the existing data structure of `siem-detection-engine-rule-status` via Dev tools as follows:

```
GET .kibana/_search
{
  "size": 10000, 
  "query": {
    "term": {
      "type": {
        "value": "siem-detection-engine-rule-status"
      }
    }
  }
}
```

``` JSON
{
  "_index" : ".kibana-spong_8.0.0_001",
  "_id" : "siem-detection-engine-rule-status:d580f1a0-2afe-11ec-8621-8d6bfcdfd75e",
  "_score" : 2.150102,
  "_source" : {
    "siem-detection-engine-rule-status" : {
      "alertId" : "d62d2980-27c4-11ec-92b0-f7b47106bb35", <-- alertId which we want in the references array and removed
      "statusDate" : "2021-10-12T01:50:52.898Z",
      "status" : "failed",
      "lastFailureAt" : "2021-10-12T01:50:52.898Z",
      "lastSuccessAt" : "2021-10-12T01:18:29.195Z",
      "lastFailureMessage" : "6 minutes (385585ms) were not queried between this rule execution and the last execution, so signals may have been missed. Consider increasing your look behind time or adding more Kibana instances. name: \"I am the Host who Names!\" id: \"d62d2980-27c4-11ec-92b0-f7b47106bb35\" rule id: \"214ccef6-e98e-493a-98c5-5bcc2d497b79\" signals index: \".siem-signals-spong-default\"",
      "lastSuccessMessage" : "succeeded",
      "gap" : "6 minutes",
      "lastLookBackDate" : "2021-10-07T23:43:27.961Z"
    },
    "type" : "siem-detection-engine-rule-status",
    "references" : [ ],
    "coreMigrationVersion" : "7.14.0",
    "updated_at" : "2021-10-12T01:50:53.404Z"
  }
}
```

Post migration the data structure should be updated as follows:

``` JSON
{
  "_index": ".kibana-spong_8.0.0_001",
  "_id": "siem-detection-engine-rule-status:d580f1a0-2afe-11ec-8621-8d6bfcdfd75e",
  "_score": 2.1865466,
  "_source": {
    "siem-detection-engine-rule-status": {
      "statusDate": "2021-10-12T01:50:52.898Z", <-- alertId is no more!
      "status": "failed",
      "lastFailureAt": "2021-10-12T01:50:52.898Z",
      "lastSuccessAt": "2021-10-12T01:18:29.195Z",
      "lastFailureMessage": "6 minutes (385585ms) were not queried between this rule execution and the last execution, so signals may have been missed. Consider increasing your look behind time or adding more Kibana instances. name: \"I am the Host who Names!\" id: \"d62d2980-27c4-11ec-92b0-f7b47106bb35\" rule id: \"214ccef6-e98e-493a-98c5-5bcc2d497b79\" signals index: \".siem-signals-spong-default\"",
      "lastSuccessMessage": "succeeded",
      "gap": "6 minutes",
      "lastLookBackDate": "2021-10-07T23:43:27.961Z"
    },
    "type": "siem-detection-engine-rule-status",
    "references": [
      {
        "id": "d62d2980-27c4-11ec-92b0-f7b47106bb35", <-- previous alertId has been converted to references[]
        "type": "alert",
        "name": "alert_0"
      }
    ],
    "migrationVersion": {
      "siem-detection-engine-rule-status": "7.16.0"
    },
    "coreMigrationVersion": "8.0.0",
    "updated_at": "2021-10-12T01:50:53.406Z"
  }
},
```

#### Manual testing
---
There are e2e tests but for any manual testing or verification you can do the following:

##### Manual upgrade test

If you have a 7.15.0 system and can migrate it forward that is the most straight forward way to ensure this does migrate correctly. You should see that the `Rule Monitoring` table and Rule Details `Failure History` table continue to function without error.

##### Downgrade via script and test migration on kibana reboot
If you have a migrated `Rule Status SO` and want to test the migration, you can run the below script to downgrade the status SO then restart Kibana and observe the migration on startup. 

Note: Since this PR removes the mapping, you would need to [update the SO mapping](https://github.com/elastic/kibana/pull/114585/files#r729386126) to include `alertId` again else you will receive a strict/dynamic mapping error.

```json
# Replace id w/ correct Rule Status SO id of existing migrated object
POST .kibana/_update/siem-detection-engine-rule-status:d580ca91-2afe-11ec-8621-8d6bfcdfd75e
{
  "script" : {
    "source": """
    ctx._source.migrationVersion['siem-detection-engine-rule-status'] = "7.15.0";
    ctx._source['siem-detection-engine-rule-status'].alertId = ctx._source.references[0].id;
    ctx._source.references.remove(0);
    """,
    "lang": "painless"
  }
}
```

Restart Kibana and now it should be migrated correctly and you shouldn't see any errors in your console.  You should also see that the `Rule Monitoring` table and Rule Details `Failure History` table continue to function without error.




### Checklist

Delete any items that are not applicable to this PR.

- [ ] ~[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials~
- [X] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios

### For maintainers

- [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)


Co-authored-by: Georgii Gorbachev <georgii.gorbachev@elastic.co>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Garrett Spong 2021-10-18 06:20:40 -06:00 committed by GitHub
parent 596b2e3460
commit fe979e4932
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 441 additions and 190 deletions

View file

@ -16,6 +16,7 @@ import { sortOrderSchema } from './common_schemas';
* - filter
* - histogram
* - nested
* - reverse_nested
* - terms
*
* Not implemented:
@ -37,7 +38,6 @@ import { sortOrderSchema } from './common_schemas';
* - parent
* - range
* - rare_terms
* - reverse_nested
* - sampler
* - significant_terms
* - significant_text
@ -76,6 +76,9 @@ export const bucketAggsSchemas: Record<string, ObjectType> = {
nested: s.object({
path: s.string(),
}),
reverse_nested: s.object({
path: s.maybe(s.string()),
}),
terms: s.object({
field: s.maybe(s.string()),
collect_mode: s.maybe(s.string()),

View file

@ -479,7 +479,6 @@ export const getRuleExecutionStatuses = (): Array<
type: 'my-type',
id: 'e0b86950-4e9f-11ea-bdbd-07b56aa159b3',
attributes: {
alertId: '04128c15-0d1b-4716-a4c5-46997ac7f3bc',
statusDate: '2020-02-18T15:26:49.783Z',
status: RuleExecutionStatus.succeeded,
lastFailureAt: undefined,
@ -492,7 +491,13 @@ export const getRuleExecutionStatuses = (): Array<
bulkCreateTimeDurations: ['800.43'],
},
score: 1,
references: [],
references: [
{
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bc',
type: 'alert',
name: 'alert_0',
},
],
updated_at: '2020-02-18T15:26:51.333Z',
version: 'WzQ2LDFd',
},
@ -500,7 +505,6 @@ export const getRuleExecutionStatuses = (): Array<
type: 'my-type',
id: '91246bd0-5261-11ea-9650-33b954270f67',
attributes: {
alertId: '1ea5a820-4da1-4e82-92a1-2b43a7bece08',
statusDate: '2020-02-18T15:15:58.806Z',
status: RuleExecutionStatus.failed,
lastFailureAt: '2020-02-18T15:15:58.806Z',
@ -514,7 +518,13 @@ export const getRuleExecutionStatuses = (): Array<
bulkCreateTimeDurations: ['800.43'],
},
score: 1,
references: [],
references: [
{
id: '1ea5a820-4da1-4e82-92a1-2b43a7bece08',
type: 'alert',
name: 'alert_0',
},
],
updated_at: '2020-02-18T15:15:58.860Z',
version: 'WzMyLDFd',
},
@ -523,7 +533,6 @@ export const getRuleExecutionStatuses = (): Array<
export const getFindBulkResultStatus = (): FindBulkExecutionLogResponse => ({
'04128c15-0d1b-4716-a4c5-46997ac7f3bd': [
{
alertId: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
statusDate: '2020-02-18T15:26:49.783Z',
status: RuleExecutionStatus.succeeded,
lastFailureAt: undefined,
@ -538,7 +547,6 @@ export const getFindBulkResultStatus = (): FindBulkExecutionLogResponse => ({
],
'1ea5a820-4da1-4e82-92a1-2b43a7bece08': [
{
alertId: '1ea5a820-4da1-4e82-92a1-2b43a7bece08',
statusDate: '2020-02-18T15:15:58.806Z',
status: RuleExecutionStatus.failed,
lastFailureAt: '2020-02-18T15:15:58.806Z',

View file

@ -31,7 +31,7 @@ import { updatePrepackagedRules } from '../../rules/update_prepacked_rules';
import { getRulesToInstall } from '../../rules/get_rules_to_install';
import { getRulesToUpdate } from '../../rules/get_rules_to_update';
import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackaged_rules';
import { ruleAssetSavedObjectsClientFactory } from '../../rules/rule_asset_saved_objects_client';
import { ruleAssetSavedObjectsClientFactory } from '../../rules/rule_asset/rule_asset_saved_objects_client';
import { buildSiemResponse } from '../utils';
import { RulesClient } from '../../../../../../alerting/server';

View file

@ -20,7 +20,7 @@ import { getRulesToUpdate } from '../../rules/get_rules_to_update';
import { findRules } from '../../rules/find_rules';
import { getLatestPrepackagedRules } from '../../rules/get_prepackaged_rules';
import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackaged_rules';
import { ruleAssetSavedObjectsClientFactory } from '../../rules/rule_asset_saved_objects_client';
import { ruleAssetSavedObjectsClientFactory } from '../../rules/rule_asset/rule_asset_saved_objects_client';
import { buildFrameworkRequest } from '../../../timeline/utils/common';
import { ConfigType } from '../../../../config';
import { SetupPlugins } from '../../../../plugin';

View file

@ -136,16 +136,16 @@ describe.each([
describe('mergeStatuses', () => {
it('merges statuses and converts from camelCase saved object to snake_case HTTP response', () => {
//
const statusOne = exampleRuleStatus();
statusOne.attributes.status = RuleExecutionStatus.failed;
const statusTwo = exampleRuleStatus();
statusTwo.attributes.status = RuleExecutionStatus.failed;
const currentStatus = exampleRuleStatus();
const foundRules = [currentStatus.attributes, statusOne.attributes, statusTwo.attributes];
const res = mergeStatuses(currentStatus.attributes.alertId, foundRules, {
const res = mergeStatuses(currentStatus.references[0].id, foundRules, {
'myfakealertid-8cfac': {
current_status: {
alert_id: 'myfakealertid-8cfac',
status_date: '2020-03-27T22:55:59.517Z',
status: RuleExecutionStatus.succeeded,
last_failure_at: null,
@ -163,7 +163,6 @@ describe.each([
expect(res).toEqual({
'myfakealertid-8cfac': {
current_status: {
alert_id: 'myfakealertid-8cfac',
status_date: '2020-03-27T22:55:59.517Z',
status: 'succeeded',
last_failure_at: null,
@ -179,7 +178,6 @@ describe.each([
},
'f4b8e31d-cf93-4bde-a265-298bde885cd7': {
current_status: {
alert_id: 'f4b8e31d-cf93-4bde-a265-298bde885cd7',
status_date: '2020-03-27T22:55:59.517Z',
status: 'succeeded',
last_failure_at: null,
@ -193,7 +191,6 @@ describe.each([
},
failures: [
{
alert_id: 'f4b8e31d-cf93-4bde-a265-298bde885cd7',
status_date: '2020-03-27T22:55:59.517Z',
status: 'failed',
last_failure_at: null,
@ -206,7 +203,6 @@ describe.each([
last_look_back_date: null, // NOTE: This is no longer used on the UI, but left here in case users are using it within the API
},
{
alert_id: 'f4b8e31d-cf93-4bde-a265-298bde885cd7',
status_date: '2020-03-27T22:55:59.517Z',
status: 'failed',
last_failure_at: null,

View file

@ -42,7 +42,7 @@ export class EventLogAdapter implements IRuleExecutionLogClient {
}
public async update(args: UpdateExecutionLogArgs) {
const { attributes, spaceId, ruleName, ruleType } = args;
const { attributes, spaceId, ruleId, ruleName, ruleType } = args;
await this.savedObjectsAdapter.update(args);
@ -51,7 +51,7 @@ export class EventLogAdapter implements IRuleExecutionLogClient {
this.eventLogClient.logStatusChange({
ruleName,
ruleType,
ruleId: attributes.alertId,
ruleId,
newStatus: attributes.status,
spaceId,
});

View file

@ -5,27 +5,33 @@
* 2.0.
*/
import { get } from 'lodash';
import {
SavedObjectsClientContract,
SavedObject,
SavedObjectsUpdateResponse,
SavedObjectsClientContract,
SavedObjectsCreateOptions,
SavedObjectsFindOptions,
SavedObjectsFindOptionsReference,
SavedObjectsFindResult,
} from '../../../../../../../../src/core/server';
import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
SavedObjectsUpdateResponse,
} from 'kibana/server';
import { get } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import { legacyRuleStatusSavedObjectType } from '../../rules/legacy_rule_status/legacy_rule_status_saved_object_mappings';
import { IRuleStatusSOAttributes } from '../../rules/types';
import { buildChunkedOrFilter } from '../../signals/utils';
export interface RuleStatusSavedObjectsClient {
find: (
options?: Omit<SavedObjectsFindOptions, 'type'>
) => Promise<Array<SavedObjectsFindResult<IRuleStatusSOAttributes>>>;
findBulk: (ids: string[], statusesPerId: number) => Promise<FindBulkResponse>;
create: (attributes: IRuleStatusSOAttributes) => Promise<SavedObject<IRuleStatusSOAttributes>>;
create: (
attributes: IRuleStatusSOAttributes,
options: SavedObjectsCreateOptions
) => Promise<SavedObject<IRuleStatusSOAttributes>>;
update: (
id: string,
attributes: Partial<IRuleStatusSOAttributes>
attributes: Partial<IRuleStatusSOAttributes>,
options: SavedObjectsCreateOptions
) => Promise<SavedObjectsUpdateResponse<IRuleStatusSOAttributes>>;
delete: (id: string) => Promise<{}>;
}
@ -35,7 +41,7 @@ export interface FindBulkResponse {
}
/**
* @pdeprecated Use RuleExecutionLogClient instead
* @deprecated Use RuleExecutionLogClient instead
*/
export const ruleStatusSavedObjectsClientFactory = (
savedObjectsClient: SavedObjectsClientContract
@ -43,7 +49,7 @@ export const ruleStatusSavedObjectsClientFactory = (
find: async (options) => {
const result = await savedObjectsClient.find<IRuleStatusSOAttributes>({
...options,
type: ruleStatusSavedObjectType,
type: legacyRuleStatusSavedObjectType,
});
return result.saved_objects;
},
@ -51,47 +57,64 @@ export const ruleStatusSavedObjectsClientFactory = (
if (ids.length === 0) {
return {};
}
const filter = buildChunkedOrFilter(`${ruleStatusSavedObjectType}.attributes.alertId`, ids);
const references = ids.map<SavedObjectsFindOptionsReference>((alertId) => ({
id: alertId,
type: 'alert',
}));
const order: 'desc' = 'desc';
const aggs = {
alertIds: {
terms: {
field: `${ruleStatusSavedObjectType}.attributes.alertId`,
size: ids.length,
references: {
nested: {
path: `${legacyRuleStatusSavedObjectType}.references`,
},
aggs: {
most_recent_statuses: {
top_hits: {
sort: [
{
[`${ruleStatusSavedObjectType}.statusDate`]: {
order,
alertIds: {
terms: {
field: `${legacyRuleStatusSavedObjectType}.references.id`,
size: ids.length,
},
aggs: {
rule_status: {
reverse_nested: {},
aggs: {
most_recent_statuses: {
top_hits: {
sort: [
{
[`${legacyRuleStatusSavedObjectType}.statusDate`]: {
order,
},
},
],
size: statusesPerId,
},
},
},
],
size: statusesPerId,
},
},
},
},
},
};
const results = await savedObjectsClient.find({
filter,
hasReference: references,
aggs,
type: ruleStatusSavedObjectType,
type: legacyRuleStatusSavedObjectType,
perPage: 0,
});
const buckets = get(results, 'aggregations.alertIds.buckets');
const buckets = get(results, 'aggregations.references.alertIds.buckets');
return buckets.reduce((acc: Record<string, unknown>, bucket: unknown) => {
const key = get(bucket, 'key');
const hits = get(bucket, 'most_recent_statuses.hits.hits');
const hits = get(bucket, 'rule_status.most_recent_statuses.hits.hits');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const statuses = hits.map((hit: any) => hit._source['siem-detection-engine-rule-status']);
acc[key] = statuses;
acc[key] = hits.map((hit: any) => hit._source[legacyRuleStatusSavedObjectType]);
return acc;
}, {});
},
create: (attributes) => savedObjectsClient.create(ruleStatusSavedObjectType, attributes),
update: (id, attributes) => savedObjectsClient.update(ruleStatusSavedObjectType, id, attributes),
delete: (id) => savedObjectsClient.delete(ruleStatusSavedObjectType, id),
create: (attributes, options) => {
return savedObjectsClient.create(legacyRuleStatusSavedObjectType, attributes, options);
},
update: (id, attributes, options) =>
savedObjectsClient.update(legacyRuleStatusSavedObjectType, id, attributes, options),
delete: (id) => savedObjectsClient.delete(legacyRuleStatusSavedObjectType, id),
});

View file

@ -5,9 +5,12 @@
* 2.0.
*/
import { SavedObject } from 'src/core/server';
import { SavedObject, SavedObjectReference } from 'src/core/server';
import { SavedObjectsClientContract } from '../../../../../../../../src/core/server';
import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas';
// eslint-disable-next-line no-restricted-imports
import { legacyGetRuleReference } from '../../rules/legacy_rule_status/legacy_utils';
import { IRuleStatusSOAttributes } from '../../rules/types';
import {
RuleStatusSavedObjectsClient,
@ -51,7 +54,7 @@ export class SavedObjectsAdapter implements IRuleExecutionLogClient {
sortField: 'statusDate',
sortOrder: 'desc',
search: ruleId,
searchFields: ['alertId'],
searchFields: ['references.id'],
});
}
@ -59,8 +62,9 @@ export class SavedObjectsAdapter implements IRuleExecutionLogClient {
return this.ruleStatusClient.findBulk(ruleIds, logsCount);
}
public async update({ id, attributes }: UpdateExecutionLogArgs) {
await this.ruleStatusClient.update(id, attributes);
public async update({ id, attributes, ruleId }: UpdateExecutionLogArgs) {
const references: SavedObjectReference[] = [legacyGetRuleReference(ruleId)];
await this.ruleStatusClient.update(id, attributes, { references });
}
public async delete(id: string) {
@ -68,31 +72,39 @@ export class SavedObjectsAdapter implements IRuleExecutionLogClient {
}
public async logExecutionMetrics({ ruleId, metrics }: LogExecutionMetricsArgs) {
const references: SavedObjectReference[] = [legacyGetRuleReference(ruleId)];
const [currentStatus] = await this.getOrCreateRuleStatuses(ruleId);
await this.ruleStatusClient.update(currentStatus.id, {
...currentStatus.attributes,
...convertMetricFields(metrics),
});
await this.ruleStatusClient.update(
currentStatus.id,
{
...currentStatus.attributes,
...convertMetricFields(metrics),
},
{ references }
);
}
private createNewRuleStatus = async (
ruleId: string
): Promise<SavedObject<IRuleStatusSOAttributes>> => {
const references: SavedObjectReference[] = [legacyGetRuleReference(ruleId)];
const now = new Date().toISOString();
return this.ruleStatusClient.create({
alertId: ruleId,
statusDate: now,
status: RuleExecutionStatus['going to run'],
lastFailureAt: null,
lastSuccessAt: null,
lastFailureMessage: null,
lastSuccessMessage: null,
gap: null,
bulkCreateTimeDurations: [],
searchAfterTimeDurations: [],
lastLookBackDate: null,
});
return this.ruleStatusClient.create(
{
statusDate: now,
status: RuleExecutionStatus['going to run'],
lastFailureAt: null,
lastSuccessAt: null,
lastFailureMessage: null,
lastSuccessMessage: null,
gap: null,
bulkCreateTimeDurations: [],
searchAfterTimeDurations: [],
lastLookBackDate: null,
},
{ references }
);
};
private getOrCreateRuleStatuses = async (
@ -112,6 +124,8 @@ export class SavedObjectsAdapter implements IRuleExecutionLogClient {
};
public async logStatusChange({ newStatus, ruleId, message, metrics }: LogStatusChangeArgs) {
const references: SavedObjectReference[] = [legacyGetRuleReference(ruleId)];
switch (newStatus) {
case RuleExecutionStatus['going to run']:
case RuleExecutionStatus.succeeded:
@ -119,10 +133,14 @@ export class SavedObjectsAdapter implements IRuleExecutionLogClient {
case RuleExecutionStatus['partial failure']: {
const [currentStatus] = await this.getOrCreateRuleStatuses(ruleId);
await this.ruleStatusClient.update(currentStatus.id, {
...currentStatus.attributes,
...buildRuleStatusAttributes(newStatus, message, metrics),
});
await this.ruleStatusClient.update(
currentStatus.id,
{
...currentStatus.attributes,
...buildRuleStatusAttributes(newStatus, message, metrics),
},
{ references }
);
return;
}
@ -137,8 +155,8 @@ export class SavedObjectsAdapter implements IRuleExecutionLogClient {
};
// We always update the newest status, so to 'persist' a failure we push a copy to the head of the list
await this.ruleStatusClient.update(currentStatus.id, failureAttributes);
const lastStatus = await this.ruleStatusClient.create(failureAttributes);
await this.ruleStatusClient.update(currentStatus.id, failureAttributes, { references });
const lastStatus = await this.ruleStatusClient.create(failureAttributes, { references });
// drop oldest failures
const oldStatuses = [lastStatus, ...ruleStatuses].slice(MAX_RULE_STATUSES);

View file

@ -53,6 +53,7 @@ export interface LogStatusChangeArgs {
export interface UpdateExecutionLogArgs {
id: string;
attributes: IRuleStatusSOAttributes;
ruleId: string;
ruleName: string;
ruleType: string;
spaceId: string;

View file

@ -26,7 +26,6 @@ describe('deleteRules', () => {
type: '',
references: [],
attributes: {
alertId: 'alertId',
statusDate: '',
lastFailureAt: null,
lastFailureMessage: null,

View file

@ -44,6 +44,7 @@ export const enableRule = async ({
const currentStatusToDisable = ruleCurrentStatus[0];
await ruleStatusClient.update({
id: currentStatusToDisable.id,
ruleId: rule.id,
ruleName: rule.name,
ruleType: rule.alertTypeId,
attributes: {

View file

@ -18,7 +18,7 @@ import {
// TODO: convert rules files to TS and add explicit type definitions
import { rawRules } from './prepackaged_rules';
import { RuleAssetSavedObjectsClient } from './rule_asset_saved_objects_client';
import { RuleAssetSavedObjectsClient } from './rule_asset/rule_asset_saved_objects_client';
import { IRuleAssetSOAttributes } from './types';
import { SavedObjectAttributes } from '../../../../../../../src/core/types';
import { ConfigType } from '../../../config';

View file

@ -0,0 +1,115 @@
/*
* 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 {
SavedObjectMigrationFn,
SavedObjectReference,
SavedObjectSanitizedDoc,
SavedObjectUnsanitizedDoc,
} from 'kibana/server';
import { isString } from 'lodash/fp';
import { truncateMessage } from '../../rule_execution_log';
import { IRuleSavedAttributesSavedObjectAttributes } from '../types';
// eslint-disable-next-line no-restricted-imports
import { legacyGetRuleReference } from './legacy_utils';
export const truncateMessageFields: SavedObjectMigrationFn<Record<string, unknown>> = (doc) => {
const { lastFailureMessage, lastSuccessMessage, ...restAttributes } = doc.attributes;
return {
...doc,
attributes: {
lastFailureMessage: truncateMessage(lastFailureMessage),
lastSuccessMessage: truncateMessage(lastSuccessMessage),
...restAttributes,
},
references: doc.references ?? [],
};
};
/**
* This side-car rule status SO is deprecated and is to be replaced by the RuleExecutionLog on Event-Log and
* additional fields on the Alerting Framework Rule SO.
*
* @deprecated Remove this once we've fully migrated to event-log and no longer require addition status SO (8.x)
*/
export const legacyRuleStatusSavedObjectMigration = {
'7.15.2': truncateMessageFields,
'7.16.0': (
doc: SavedObjectUnsanitizedDoc<IRuleSavedAttributesSavedObjectAttributes>
): SavedObjectSanitizedDoc<IRuleSavedAttributesSavedObjectAttributes> => {
return legacyMigrateRuleAlertIdSOReferences(doc);
},
};
/**
* This migrates alertId within legacy `siem-detection-engine-rule-status` to saved object references on an upgrade.
* We only migrate alertId if we find these conditions:
* - alertId is a string and not null, undefined, or malformed data.
* - The existing references do not already have a alertId found within it.
*
* Some of these issues could crop up during either user manual errors of modifying things, earlier migration
* issues, etc... so we are safer to check them as possibilities
*
* @deprecated Remove this once we've fully migrated to event-log and no longer require addition status SO (8.x)
* @param doc The document having an alertId to migrate into references
* @returns The document migrated with saved object references
*/
export const legacyMigrateRuleAlertIdSOReferences = (
doc: SavedObjectUnsanitizedDoc<IRuleSavedAttributesSavedObjectAttributes>
): SavedObjectSanitizedDoc<IRuleSavedAttributesSavedObjectAttributes> => {
const { references } = doc;
// Isolate alertId from the doc
const { alertId, ...attributesWithoutAlertId } = doc.attributes;
const existingReferences = references ?? [];
if (!isString(alertId)) {
// early return if alertId is not a string as expected
return { ...doc, references: existingReferences };
} else {
const alertReferences = legacyMigrateAlertId({
alertId,
existingReferences,
});
return {
...doc,
attributes: {
...attributesWithoutAlertId.attributes,
},
references: [...existingReferences, ...alertReferences],
};
}
};
/**
* This is a helper to migrate "alertId"
*
* @deprecated Remove this once we've fully migrated to event-log and no longer require addition status SO (8.x)
*
* @param existingReferences The existing saved object references
* @param alertId The alertId to migrate
*
* @returns The savedObjectReferences migrated
*/
export const legacyMigrateAlertId = ({
existingReferences,
alertId,
}: {
existingReferences: SavedObjectReference[];
alertId: string;
}): SavedObjectReference[] => {
const existingReferenceFound = existingReferences.find((reference) => {
return reference.id === alertId && reference.type === 'alert';
});
if (existingReferenceFound) {
return [];
} else {
return [legacyGetRuleReference(alertId)];
}
};

View file

@ -0,0 +1,73 @@
/*
* 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 { SavedObjectsType } from 'kibana/server';
// eslint-disable-next-line no-restricted-imports
import { legacyRuleStatusSavedObjectMigration } from './legacy_migrations';
/**
* This side-car rule status SO is deprecated and is to be replaced by the RuleExecutionLog on Event-Log and
* additional fields on the Alerting Framework Rule SO.
*
* @deprecated Remove this once we've fully migrated to event-log and no longer require addition status SO (8.x)
*/
export const legacyRuleStatusSavedObjectType = 'siem-detection-engine-rule-status';
/**
* This side-car rule status SO is deprecated and is to be replaced by the RuleExecutionLog on Event-Log and
* additional fields on the Alerting Framework Rule SO.
*
* @deprecated Remove this once we've fully migrated to event-log and no longer require addition status SO (8.x)
*/
export const ruleStatusSavedObjectMappings: SavedObjectsType['mappings'] = {
properties: {
status: {
type: 'keyword',
},
statusDate: {
type: 'date',
},
lastFailureAt: {
type: 'date',
},
lastSuccessAt: {
type: 'date',
},
lastFailureMessage: {
type: 'text',
},
lastSuccessMessage: {
type: 'text',
},
lastLookBackDate: {
type: 'date',
},
gap: {
type: 'text',
},
bulkCreateTimeDurations: {
type: 'float',
},
searchAfterTimeDurations: {
type: 'float',
},
},
};
/**
* This side-car rule status SO is deprecated and is to be replaced by the RuleExecutionLog on Event-Log and
* additional fields on the Alerting Framework Rule SO.
*
* @deprecated Remove this once we've fully migrated to event-log and no longer require addition status SO (8.x)
*/
export const legacyRuleStatusType: SavedObjectsType = {
name: legacyRuleStatusSavedObjectType,
hidden: false,
namespaceType: 'single',
mappings: ruleStatusSavedObjectMappings,
migrations: legacyRuleStatusSavedObjectMigration,
};

View file

@ -0,0 +1,17 @@
/*
* 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.
*/
/**
* Given an id this returns a legacy rule reference.
* @param id The id of the alert
* @deprecated Remove this once we've fully migrated to event-log and no longer require addition status SO (8.x)
*/
export const legacyGetRuleReference = (id: string) => ({
id,
type: 'alert',
name: 'alert_0',
});

View file

@ -0,0 +1,32 @@
/*
* 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 { SavedObjectsType } from '../../../../../../../../src/core/server';
export const ruleAssetSavedObjectType = 'security-rule';
export const ruleAssetSavedObjectMappings: SavedObjectsType['mappings'] = {
dynamic: false,
properties: {
name: {
type: 'keyword',
},
rule_id: {
type: 'keyword',
},
version: {
type: 'long',
},
},
};
export const ruleAssetType: SavedObjectsType = {
name: ruleAssetSavedObjectType,
hidden: false,
namespaceType: 'agnostic',
mappings: ruleAssetSavedObjectMappings,
};

View file

@ -9,9 +9,9 @@ import {
SavedObjectsClientContract,
SavedObjectsFindOptions,
SavedObjectsFindResponse,
} from '../../../../../../../src/core/server';
import { ruleAssetSavedObjectType } from '../rules/saved_object_mappings';
import { IRuleAssetSavedObject } from '../rules/types';
} from 'kibana/server';
import { ruleAssetSavedObjectType } from './rule_asset_saved_object_mappings';
import { IRuleAssetSavedObject } from '../types';
const DEFAULT_PAGE_SIZE = 100;

View file

@ -1,97 +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 { SavedObjectsType, SavedObjectMigrationFn } from 'kibana/server';
import { truncateMessage } from '../rule_execution_log';
export const ruleStatusSavedObjectType = 'siem-detection-engine-rule-status';
export const ruleStatusSavedObjectMappings: SavedObjectsType['mappings'] = {
properties: {
alertId: {
type: 'keyword',
},
status: {
type: 'keyword',
},
statusDate: {
type: 'date',
},
lastFailureAt: {
type: 'date',
},
lastSuccessAt: {
type: 'date',
},
lastFailureMessage: {
type: 'text',
},
lastSuccessMessage: {
type: 'text',
},
lastLookBackDate: {
type: 'date',
},
gap: {
type: 'text',
},
bulkCreateTimeDurations: {
type: 'float',
},
searchAfterTimeDurations: {
type: 'float',
},
},
};
const truncateMessageFields: SavedObjectMigrationFn<Record<string, unknown>> = (doc) => {
const { lastFailureMessage, lastSuccessMessage, ...restAttributes } = doc.attributes;
return {
...doc,
attributes: {
lastFailureMessage: truncateMessage(lastFailureMessage),
lastSuccessMessage: truncateMessage(lastSuccessMessage),
...restAttributes,
},
references: doc.references ?? [],
};
};
export const type: SavedObjectsType = {
name: ruleStatusSavedObjectType,
hidden: false,
namespaceType: 'single',
mappings: ruleStatusSavedObjectMappings,
migrations: {
'7.15.2': truncateMessageFields,
},
};
export const ruleAssetSavedObjectType = 'security-rule';
export const ruleAssetSavedObjectMappings: SavedObjectsType['mappings'] = {
dynamic: false,
properties: {
name: {
type: 'keyword',
},
rule_id: {
type: 'keyword',
},
version: {
type: 'long',
},
},
};
export const ruleAssetType: SavedObjectsType = {
name: ruleAssetSavedObjectType,
hidden: false,
namespaceType: 'agnostic',
mappings: ruleAssetSavedObjectMappings,
};

View file

@ -111,7 +111,6 @@ export type RuleAlertType = SanitizedAlert<RuleParams>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface IRuleStatusSOAttributes extends Record<string, any> {
alertId: string; // created alert id.
statusDate: StatusDate;
lastFailureAt: LastFailureAt | null | undefined;
lastFailureMessage: LastFailureMessage | null | undefined;
@ -125,7 +124,6 @@ export interface IRuleStatusSOAttributes extends Record<string, any> {
}
export interface IRuleStatusResponseAttributes {
alert_id: string; // created alert id.
status_date: StatusDate;
last_failure_at: LastFailureAt | null | undefined;
last_failure_message: LastFailureMessage | null | undefined;

View file

@ -18,7 +18,8 @@ import type {
import { SavedObject } from '../../../../../../../../src/core/server';
import { loggingSystemMock } from '../../../../../../../../src/core/server/mocks';
import { IRuleStatusSOAttributes } from '../../rules/types';
import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
// eslint-disable-next-line no-restricted-imports
import { legacyRuleStatusSavedObjectType } from '../../rules/legacy_rule_status/legacy_rule_status_saved_object_mappings';
import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock';
import { RulesSchema } from '../../../../../common/detection_engine/schemas/response';
import { RuleParams } from '../../schemas/rule_schemas';
@ -725,10 +726,9 @@ export const sampleRuleGuid = '04128c15-0d1b-4716-a4c5-46997ac7f3bd';
export const sampleIdGuid = 'e1e08ddc-5e37-49ff-a258-5393aa44435a';
export const exampleRuleStatus: () => SavedObject<IRuleStatusSOAttributes> = () => ({
type: ruleStatusSavedObjectType,
type: legacyRuleStatusSavedObjectType,
id: '042e6d90-7069-11ea-af8b-0f8ae4fa817e',
attributes: {
alertId: 'f4b8e31d-cf93-4bde-a265-298bde885cd7',
statusDate: '2020-03-27T22:55:59.517Z',
status: RuleExecutionStatus.succeeded,
lastFailureAt: null,
@ -740,7 +740,13 @@ export const exampleRuleStatus: () => SavedObject<IRuleStatusSOAttributes> = ()
searchAfterTimeDurations: [],
lastLookBackDate: null,
},
references: [],
references: [
{
id: 'f4b8e31d-cf93-4bde-a265-298bde885cd7',
type: 'alert',
name: 'alert_0',
},
],
updated_at: '2020-03-27T22:55:59.577Z',
version: 'WzgyMiwxXQ==',
});

View file

@ -8,10 +8,9 @@
import { CoreSetup } from '../../../../src/core/server';
import { noteType, pinnedEventType, timelineType } from './lib/timeline/saved_object_mappings';
import {
type as ruleStatusType,
ruleAssetType,
} from './lib/detection_engine/rules/saved_object_mappings';
// eslint-disable-next-line no-restricted-imports
import { legacyRuleStatusType } from './lib/detection_engine/rules/legacy_rule_status/legacy_rule_status_saved_object_mappings';
import { ruleAssetType } from './lib/detection_engine/rules/rule_asset/rule_asset_saved_object_mappings';
// eslint-disable-next-line no-restricted-imports
import { legacyType as legacyRuleActionsType } from './lib/detection_engine/rule_actions/legacy_saved_object_mappings';
import { type as signalsMigrationType } from './lib/detection_engine/migrations/saved_objects';
@ -24,7 +23,7 @@ const types = [
noteType,
pinnedEventType,
legacyRuleActionsType,
ruleStatusType,
legacyRuleStatusType,
ruleAssetType,
timelineType,
exceptionsArtifactType,

View file

@ -86,6 +86,33 @@ export default ({ getService }: FtrProviderContext): void => {
'7d'
);
});
it('migrates legacy siem-detection-engine-rule-status to use saved object references', async () => {
const response = await es.get<{
'siem-detection-engine-rule-status': {
alertId: string;
};
references: [{}];
}>({
index: '.kibana',
id: 'siem-detection-engine-rule-status:d62d2980-27c4-11ec-92b0-f7b47106bb35',
});
expect(response.statusCode).to.eql(200);
// references exist and are expected values
expect(response.body._source?.references).to.eql([
{
name: 'alert_0',
id: 'fb1046a0-0452-11ec-9b15-d13d79d162f3',
type: 'alert',
},
]);
// alertId no longer exist
expect(response.body._source?.['siem-detection-engine-rule-status'].alertId).to.eql(
undefined
);
});
});
});
};

View file

@ -1,4 +1,4 @@
{
{
"type": "doc",
"value": {
"id": "siem-detection-engine-rule-actions:fce024a0-0452-11ec-9b15-d13d79d162f3",
@ -29,3 +29,35 @@
}
}
}
{
"type": "doc",
"value": {
"id": "siem-detection-engine-rule-status:d62d2980-27c4-11ec-92b0-f7b47106bb35",
"index": ".kibana_1",
"source": {
"siem-detection-engine-rule-status": {
"alertId": "fb1046a0-0452-11ec-9b15-d13d79d162f3",
"statusDate": "2021-10-11T20:51:26.622Z",
"status": "succeeded",
"lastFailureAt": "2021-10-11T18:10:08.982Z",
"lastSuccessAt": "2021-10-11T20:51:26.622Z",
"lastFailureMessage": "4 days (323690920ms) were not queried between this rule execution and the last execution, so signals may have been missed. Consider increasing your look behind time or adding more Kibana instances. name: \"Threshy\" id: \"fb1046a0-0452-11ec-9b15-d13d79d162f3\" rule id: \"b789c80f-f6d8-41f1-8b4f-b4a23342cde2\" signals index: \".siem-signals-spong-default\"",
"lastSuccessMessage": "succeeded",
"gap": "4 days",
"bulkCreateTimeDurations": [
"34.49"
],
"searchAfterTimeDurations": [
"62.58"
],
"lastLookBackDate": null
},
"type": "siem-detection-engine-rule-status",
"references": [],
"coreMigrationVersion": "7.14.0",
"updated_at": "2021-10-11T20:51:26.657Z"
}
}
}