Skip to content

Commit

Permalink
[ResponseOps][Cases] Set case alert attachment rule info to null (#12…
Browse files Browse the repository at this point in the history
…3094)

* Setting rule info to null

* Renaming variables

* Addressing PR feedback

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
jonathan-buttner and kibanamachine authored Jan 19, 2022
1 parent 3280400 commit d1eb0df
Show file tree
Hide file tree
Showing 12 changed files with 882 additions and 23 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* 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 { CommentResponseAlertsType } from '../../../../common/api';
import { SnakeToCamelCase } from '../../../../common/types';
import { getRuleId, getRuleName } from './alert';
import { Ecs } from '../../../containers/types';

describe('rule getters', () => {
describe.each([
['getRuleId', getRuleId],
['getRuleName', getRuleName],
])('%s null checks', (name, funcToExec) => {
it('returns null if the comment field is an empty string', () => {
const comment = {
rule: {
id: '',
name: '',
},
} as unknown as SnakeToCamelCase<CommentResponseAlertsType>;

expect(funcToExec(comment)).toBeNull();
});

it('returns null if the comment field is an empty string in an array', () => {
const comment = {
rule: {
id: [''],
name: [''],
},
} as unknown as SnakeToCamelCase<CommentResponseAlertsType>;

expect(funcToExec(comment)).toBeNull();
});

it('returns null if the comment does not have a rule field', () => {
const comment = {} as unknown as SnakeToCamelCase<CommentResponseAlertsType>;

expect(funcToExec(comment)).toBeNull();
});

it('returns null if the signals and alert field is an empty string', () => {
const comment = {} as unknown as SnakeToCamelCase<CommentResponseAlertsType>;
const alert = {
signal: { rule: { id: '', name: '' } },
kibana: { alert: { rule: { uuid: '', name: '' } } },
} as unknown as Ecs;

expect(funcToExec(comment, alert)).toBeNull();
});
});

describe.each([
['getRuleId', getRuleId, '1'],
['getRuleName', getRuleName, 'Rule name1'],
])('%s', (name, funcToExec, expectedResult) => {
it('returns the first entry in the comment field', () => {
const comment = {
rule: {
id: ['1', '2'],
name: ['Rule name1', 'Rule name2'],
},
} as unknown as SnakeToCamelCase<CommentResponseAlertsType>;

expect(funcToExec(comment)).toEqual(expectedResult);
});

it('returns signal field', () => {
const comment = {} as unknown as SnakeToCamelCase<CommentResponseAlertsType>;
const alert = { signal: { rule: { id: '1', name: 'Rule name1' } } } as unknown as Ecs;

expect(funcToExec(comment, alert)).toEqual(expectedResult);
});

it('returns kibana alert field', () => {
const comment = {} as unknown as SnakeToCamelCase<CommentResponseAlertsType>;
const alert = {
kibana: { alert: { rule: { uuid: '1', name: 'Rule name1' } } },
} as unknown as Ecs;

expect(funcToExec(comment, alert)).toEqual(expectedResult);
});

it('returns signal field even when kibana alert field is defined', () => {
const comment = {} as unknown as SnakeToCamelCase<CommentResponseAlertsType>;
const alert = {
signal: { rule: { id: '1', name: 'Rule name1' } },
kibana: { alert: { rule: { uuid: 'rule id1', name: 'other rule name1' } } },
} as unknown as Ecs;

expect(funcToExec(comment, alert)).toEqual(expectedResult);
});

it('returns the first entry in the signals field', () => {
const comment = {} as unknown as SnakeToCamelCase<CommentResponseAlertsType>;
const alert = {
signal: { rule: { id: '1', name: 'Rule name1' } },
kibana: { alert: { rule: { uuid: 'rule id1', name: 'other rule name1' } } },
} as unknown as Ecs;

expect(funcToExec(comment, alert)).toEqual(expectedResult);
});

it('returns the alert field if the signals field is an empty string', () => {
const comment = {} as unknown as SnakeToCamelCase<CommentResponseAlertsType>;
const alert = {
signal: { rule: { id: '', name: '' } },
kibana: { alert: { rule: { uuid: '1', name: 'Rule name1' } } },
} as unknown as Ecs;

expect(funcToExec(comment, alert)).toEqual(expectedResult);
});

it('returns the alert field if the signals field is an empty string in an array', () => {
const comment = {} as unknown as SnakeToCamelCase<CommentResponseAlertsType>;
const alert = {
signal: { rule: { id: [''], name: [''] } },
kibana: { alert: { rule: { uuid: '1', name: 'Rule name1' } } },
} as unknown as Ecs;

expect(funcToExec(comment, alert)).toEqual(expectedResult);
});

it('returns the alert field first item if the signals field is an empty string in an array', () => {
const comment = {} as unknown as SnakeToCamelCase<CommentResponseAlertsType>;
const alert = {
signal: { rule: { id: [''], name: [''] } },
kibana: { alert: { rule: { uuid: ['1', '2'], name: ['Rule name1', 'Rule name2'] } } },
} as unknown as Ecs;

expect(funcToExec(comment, alert)).toEqual(expectedResult);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { UserActionUsernameWithAvatar } from '../avatar_username';
import { AlertCommentEvent } from './alert_event';
import { UserActionCopyLink } from '../copy_link';
import { UserActionShowAlert } from './show_alert';
import { Ecs } from '../../../containers/types';

type BuilderArgs = Pick<
UserActionBuilderArgs,
Expand All @@ -29,9 +30,6 @@ type BuilderArgs = Pick<
| 'onShowAlertDetails'
> & { comment: SnakeToCamelCase<CommentResponseAlertsType> };

const getFirstItem = (items: string | string[]) =>
Array.isArray(items) ? (items.length > 0 ? items[0] : '') : items;

export const createAlertAttachmentUserActionBuilder = ({
userAction,
comment,
Expand All @@ -42,24 +40,16 @@ export const createAlertAttachmentUserActionBuilder = ({
onShowAlertDetails,
}: BuilderArgs): ReturnType<UserActionBuilder> => ({
build: () => {
const alertId = getFirstItem(comment.alertId);
const alertIndex = getFirstItem(comment.index);
const alertId = getNonEmptyField(comment.alertId);
const alertIndex = getNonEmptyField(comment.index);

if (isEmpty(alertId)) {
if (!alertId || !alertIndex) {
return [];
}

const ruleId =
comment?.rule?.id ??
alertData[alertId]?.signal?.rule?.id?.[0] ??
get(alertData[alertId], ALERT_RULE_UUID)[0] ??
null;

const ruleName =
comment?.rule?.name ??
alertData[alertId]?.signal?.rule?.name?.[0] ??
get(alertData[alertId], ALERT_RULE_NAME)[0] ??
null;
const alertField: Ecs | undefined = alertData[alertId];
const ruleId = getRuleId(comment, alertField);
const ruleName = getRuleName(comment, alertField);

return [
{
Expand Down Expand Up @@ -104,3 +94,51 @@ export const createAlertAttachmentUserActionBuilder = ({
];
},
});

const getFirstItem = (items?: string | string[] | null): string | null => {
return Array.isArray(items) ? items[0] : items ?? null;
};

export const getRuleId = (comment: BuilderArgs['comment'], alertData?: Ecs): string | null =>
getRuleField({
commentRuleField: comment?.rule?.id,
alertData,
signalRuleFieldPath: 'signal.rule.id',
kibanaAlertFieldPath: ALERT_RULE_UUID,
});

export const getRuleName = (comment: BuilderArgs['comment'], alertData?: Ecs): string | null =>
getRuleField({
commentRuleField: comment?.rule?.name,
alertData,
signalRuleFieldPath: 'signal.rule.name',
kibanaAlertFieldPath: ALERT_RULE_NAME,
});

const getRuleField = ({
commentRuleField,
alertData,
signalRuleFieldPath,
kibanaAlertFieldPath,
}: {
commentRuleField: string | string[] | null | undefined;
alertData: Ecs | undefined;
signalRuleFieldPath: string;
kibanaAlertFieldPath: string;
}): string | null => {
const field =
getNonEmptyField(commentRuleField) ??
getNonEmptyField(get(alertData, signalRuleFieldPath)) ??
getNonEmptyField(get(alertData, kibanaAlertFieldPath));

return field;
};

function getNonEmptyField(field: string | string[] | undefined | null): string | null {
const firstItem = getFirstItem(field);
if (firstItem == null || isEmpty(firstItem)) {
return null;
}

return firstItem;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import {
createCommentsMigrations,
mergeMigrationFunctionMaps,
migrateByValueLensVisualizations,
removeRuleInformation,
stringifyCommentWithoutTrailingNewline,
} from './comments';
import {
getLensVisualizations,
parseCommentString,
} from '../../../common/utils/markdown_plugins/utils';
import { CommentType } from '../../../common/api';

import { savedObjectsServiceMock } from '../../../../../../src/core/server/mocks';
import { makeLensEmbeddableFactory } from '../../../../lens/server/embeddable/make_lens_embeddable_factory';
Expand Down Expand Up @@ -396,4 +398,79 @@ describe('comments migrations', () => {
);
});
});

describe('removeRuleInformation', () => {
it('does not modify non-alert comment', () => {
const doc = {
id: '123',
attributes: {
type: 'user',
},
type: 'abc',
references: [],
};

expect(removeRuleInformation(doc)).toEqual(doc);
});

it('sets the rule fields to null', () => {
const doc = {
id: '123',
type: 'abc',
attributes: {
type: CommentType.alert,
rule: {
id: '123',
name: 'hello',
},
},
};

expect(removeRuleInformation(doc)).toEqual({
...doc,
attributes: { ...doc.attributes, rule: { id: null, name: null } },
references: [],
});
});

it('sets the rule fields to null for a generated alert', () => {
const doc = {
id: '123',
type: 'abc',
attributes: {
type: CommentType.generatedAlert,
rule: {
id: '123',
name: 'hello',
},
},
};

expect(removeRuleInformation(doc)).toEqual({
...doc,
attributes: { ...doc.attributes, rule: { id: null, name: null } },
references: [],
});
});

it('preserves the references field', () => {
const doc = {
id: '123',
type: 'abc',
attributes: {
type: CommentType.alert,
rule: {
id: '123',
name: 'hello',
},
},
references: [{ id: '123', name: 'hi', type: 'awesome' }],
};

expect(removeRuleInformation(doc)).toEqual({
...doc,
attributes: { ...doc.attributes, rule: { id: null, name: null } },
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,15 @@ export const createCommentsMigrations = (
): SavedObjectSanitizedDoc<SanitizedCaseOwner> => {
return addOwnerToSO(doc);
},
/*
* This is to fix the issue here: https://github.com/elastic/kibana/issues/123089
* Instead of migrating the rule information in the references array which was risky for 8.0
* we decided to remove the information since the UI will do the look up for the rule information if
* the backend returns it as null.
*
* The downside is it incurs extra query overhead.
**/
'8.0.0': removeRuleInformation,
};

return mergeMigrationFunctionMaps(commentsMigrations, embeddableMigrations);
Expand Down Expand Up @@ -175,3 +184,29 @@ export const mergeMigrationFunctionMaps = (

return mergeWith({ ...obj1 }, obj2, customizer);
};

export const removeRuleInformation = (
doc: SavedObjectUnsanitizedDoc<Record<string, unknown>>
): SavedObjectSanitizedDoc<unknown> => {
if (
doc.attributes.type === CommentType.alert ||
doc.attributes.type === CommentType.generatedAlert
) {
return {
...doc,
attributes: {
...doc.attributes,
rule: {
id: null,
name: null,
},
},
references: doc.references ?? [],
};
}

return {
...doc,
references: doc.references ?? [],
};
};
Loading

0 comments on commit d1eb0df

Please sign in to comment.