Skip to content

Commit

Permalink
[Security Solution] [Platform] Migrate pre-8.0 action connector ids i…
Browse files Browse the repository at this point in the history
…n rule params on import to post-8.0 _id (#120975) (#122302)

Security Solution import route needed to find actions where the action SO id was the old, pre-8.0 _id so we find references to the _id and the originId to make sure the rule we are importing will not error out.

Co-authored-by: Kibana Machine <[email protected]>

Co-authored-by: Devin W. Hurley <[email protected]>
  • Loading branch information
kibanamachine and dhurley14 authored Jan 5, 2022
1 parent f7f133d commit 6f97f07
Show file tree
Hide file tree
Showing 7 changed files with 1,002 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export const action = t.exact(
})
);

export type Action = t.TypeOf<typeof action>;

export const actions = t.array(action);
export type Actions = t.TypeOf<typeof actions>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ import {
buildSiemResponse,
} from '../utils';

import { getTupleDuplicateErrorsAndUniqueRules, getInvalidConnectors } from './utils';
import {
getTupleDuplicateErrorsAndUniqueRules,
getInvalidConnectors,
migrateLegacyActionsIds,
} from './utils';
import { createRulesAndExceptionsStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson';
import { buildRouteValidation } from '../../../../utils/build_validation/route_validation';
import { HapiReadableStream } from '../../rules/types';
Expand Down Expand Up @@ -74,6 +78,9 @@ export const importRulesRoute = (
const rulesClient = context.alerting.getRulesClient();
const actionsClient = context.actions.getActionsClient();
const esClient = context.core.elasticsearch.client;
const actionSOClient = context.core.savedObjects.getClient({
includedHiddenTypes: ['action'],
});
const savedObjectsClient = context.core.savedObjects.client;
const siemClient = context.securitySolution.getAppClient();
const exceptionsClient = context.lists?.getExceptionListClient();
Expand Down Expand Up @@ -127,8 +134,13 @@ export const importRulesRoute = (
const [duplicateIdErrors, parsedObjectsWithoutDuplicateErrors] =
getTupleDuplicateErrorsAndUniqueRules(rules, request.query.overwrite);

const [nonExistentActionErrors, uniqueParsedObjects] = await getInvalidConnectors(
const migratedParsedObjectsWithoutDuplicateErrors = await migrateLegacyActionsIds(
parsedObjectsWithoutDuplicateErrors,
actionSOClient
);

const [nonExistentActionErrors, uniqueParsedObjects] = await getInvalidConnectors(
migratedParsedObjectsWithoutDuplicateErrors,
actionsClient
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { Readable } from 'stream';
import { createPromiseFromStreams } from '@kbn/utils';
import { Action, ThreatMapping } from '@kbn/securitysolution-io-ts-alerting-types';

import {
transformAlertToRule,
Expand All @@ -19,6 +20,8 @@ import {
getDuplicates,
getTupleDuplicateErrorsAndUniqueRules,
getInvalidConnectors,
swapActionIds,
migrateLegacyActionsIds,
} from './utils';
import { getAlertMock } from '../__mocks__/request_responses';
import { INTERNAL_IDENTIFIER } from '../../../../../common/constants';
Expand All @@ -30,7 +33,6 @@ import { createRulesAndExceptionsStreamFromNdJson } from '../../rules/create_rul
import { RuleAlertType } from '../../rules/types';
import { ImportRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/import_rules_schema';
import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock';
import { ThreatMapping } from '@kbn/securitysolution-io-ts-alerting-types';
import { CreateRulesBulkSchema } from '../../../../../common/detection_engine/schemas/request';
import {
getMlRuleParams,
Expand Down Expand Up @@ -652,6 +654,208 @@ describe.each([
});
});

describe('swapActionIds', () => {
const mockAction: Action = {
group: 'group string',
id: 'some-7.x-id',
action_type_id: '.slack',
params: {},
};
const soClient = clients.core.savedObjects.getClient();
beforeEach(() => {
soClient.find.mockReset();
soClient.find.mockClear();
});

test('returns original action if Elasticsearch query fails', async () => {
clients.core.savedObjects
.getClient()
.find.mockRejectedValueOnce(new Error('failed to query'));
const result = await swapActionIds(mockAction, soClient);
expect(result).toEqual(mockAction);
});

test('returns original action if Elasticsearch query returns no hits', async () => {
soClient.find.mockImplementationOnce(async () => ({
total: 0,
per_page: 0,
page: 1,
saved_objects: [],
}));
const result = await swapActionIds(mockAction, soClient);
expect(result).toEqual(mockAction);
});

test('returns error if conflicting action connectors are found -> two hits found with same originId', async () => {
soClient.find.mockImplementationOnce(async () => ({
total: 0,
per_page: 0,
page: 1,
saved_objects: [
{ score: 0, id: 'fake id 1', type: 'action', attributes: {}, references: [] },
{ score: 0, id: 'fake id 2', type: 'action', attributes: {}, references: [] },
],
}));
const result = await swapActionIds(mockAction, soClient);
expect(result instanceof Error).toBeTruthy();
expect((result as unknown as Error).message).toEqual(
'Found two action connectors with originId or _id: some-7.x-id The upload cannot be completed unless the _id or the originId of the action connector is changed. See https://www.elastic.co/guide/en/kibana/current/sharing-saved-objects.html for more details'
);
});

test('returns action with new migrated _id if a single hit is found when querying by action connector originId', async () => {
soClient.find.mockImplementationOnce(async () => ({
total: 0,
per_page: 0,
page: 1,
saved_objects: [
{ score: 0, id: 'new-post-8.0-id', type: 'action', attributes: {}, references: [] },
],
}));
const result = await swapActionIds(mockAction, soClient);
expect(result).toEqual({ ...mockAction, id: 'new-post-8.0-id' });
});
});

describe('migrateLegacyActionsIds', () => {
const mockAction: Action = {
group: 'group string',
id: 'some-7.x-id',
action_type_id: '.slack',
params: {},
};
const soClient = clients.core.savedObjects.getClient();
beforeEach(() => {
soClient.find.mockReset();
soClient.find.mockClear();
});
test('returns import rules schemas + migrated action', async () => {
const rule: ReturnType<typeof getCreateRulesSchemaMock> = {
...getCreateRulesSchemaMock('rule-1'),
actions: [mockAction],
};
soClient.find.mockImplementationOnce(async () => ({
total: 0,
per_page: 0,
page: 1,
saved_objects: [
{ score: 0, id: 'new-post-8.0-id', type: 'action', attributes: {}, references: [] },
],
}));

const res = await migrateLegacyActionsIds(
// @ts-expect-error
[rule],
soClient
);
expect(res).toEqual([{ ...rule, actions: [{ ...mockAction, id: 'new-post-8.0-id' }] }]);
});

test('returns import rules schemas + multiple migrated action', async () => {
const rule: ReturnType<typeof getCreateRulesSchemaMock> = {
...getCreateRulesSchemaMock('rule-1'),
actions: [mockAction, { ...mockAction, id: 'different-id' }],
};
soClient.find.mockImplementation(async () => ({
total: 0,
per_page: 0,
page: 1,
saved_objects: [
{ score: 0, id: 'new-post-8.0-id', type: 'action', attributes: {}, references: [] },
],
}));

const res = await migrateLegacyActionsIds(
// @ts-expect-error
[rule],
soClient
);
expect(res).toEqual([
{
...rule,
actions: [
{ ...mockAction, id: 'new-post-8.0-id' },
{ ...mockAction, id: 'new-post-8.0-id' },
],
},
]);
});

test('returns import rules schemas + migrated action resulting in error', async () => {
const rule: ReturnType<typeof getCreateRulesSchemaMock> = {
...getCreateRulesSchemaMock('rule-1'),
actions: [mockAction],
};
soClient.find.mockImplementationOnce(async () => ({
total: 0,
per_page: 0,
page: 1,
saved_objects: [
{ score: 0, id: 'new-post-8.0-id', type: 'action', attributes: {}, references: [] },
{ score: 0, id: 'new-post-8.0-id-2', type: 'action', attributes: {}, references: [] },
],
}));

const res = await migrateLegacyActionsIds(
// @ts-expect-error
[rule],
soClient
);
expect(res[0] instanceof Error).toBeTruthy();
expect((res[0] as unknown as Error).message).toEqual(
JSON.stringify({
rule_id: 'rule-1',
error: {
status_code: 409,
message:
'Found two action connectors with originId or _id: some-7.x-id The upload cannot be completed unless the _id or the originId of the action connector is changed. See https://www.elastic.co/guide/en/kibana/current/sharing-saved-objects.html for more details',
},
})
);
});
test('returns import multiple rules schemas + migrated action, one success and one error', async () => {
const rule: ReturnType<typeof getCreateRulesSchemaMock> = {
...getCreateRulesSchemaMock('rule-1'),
actions: [mockAction],
};

soClient.find.mockImplementationOnce(async () => ({
total: 0,
per_page: 0,
page: 1,
saved_objects: [
{ score: 0, id: 'new-post-8.0-id', type: 'action', attributes: {}, references: [] },
],
}));
soClient.find.mockImplementationOnce(async () => ({
total: 0,
per_page: 0,
page: 1,
saved_objects: [
{ score: 0, id: 'new-post-8.0-id', type: 'action', attributes: {}, references: [] },
{ score: 0, id: 'new-post-8.0-id-2', type: 'action', attributes: {}, references: [] },
],
}));

const res = await migrateLegacyActionsIds(
// @ts-expect-error
[rule, rule],
soClient
);
expect(res[0]).toEqual({ ...rule, actions: [{ ...mockAction, id: 'new-post-8.0-id' }] });
expect(res[1] instanceof Error).toBeTruthy();
expect((res[1] as unknown as Error).message).toEqual(
JSON.stringify({
rule_id: 'rule-1',
error: {
status_code: 409,
message:
'Found two action connectors with originId or _id: some-7.x-id The upload cannot be completed unless the _id or the originId of the action connector is changed. See https://www.elastic.co/guide/en/kibana/current/sharing-saved-objects.html for more details',
},
})
);
});
});
describe('getInvalidConnectors', () => {
beforeEach(() => {
clients.actionsClient.getAll.mockReset();
Expand Down
Loading

0 comments on commit 6f97f07

Please sign in to comment.