mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
[Cases] Introduces the incremental_id
field and the id incrementer SO (#219757)
## Summary This is the first PR related to https://github.com/elastic/kibana/issues/212570. (more up-to-date information in the [design doc](https://docs.google.com/document/d/1DZKTPl7UryYjpjVMNhIYbE82OADVOg93-d02f0ZQtUI/edit?tab=t.0)) It only introduces the `incremental_id` field to the cases object and introduces the new "id incrementer" saved object. Tests and migrations have been changed accordingly and the `incremental_id` field is removed from cases import/export. The motivation behind releasing these changes first is so that serverless deployments will know about the new field and SO once the other changes are coming. ### Checklist - [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 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
bee60b3a41
commit
57bd2fa637
44 changed files with 862 additions and 224 deletions
|
@ -174,6 +174,7 @@
|
|||
"external_service.pushed_by.full_name",
|
||||
"external_service.pushed_by.profile_uid",
|
||||
"external_service.pushed_by.username",
|
||||
"incremental_id",
|
||||
"observables",
|
||||
"observables.typeKey",
|
||||
"observables.value",
|
||||
|
@ -216,6 +217,11 @@
|
|||
"cases-connector-mappings": [
|
||||
"owner"
|
||||
],
|
||||
"cases-incrementing-id": [
|
||||
"@timestamp",
|
||||
"last_id",
|
||||
"updated_at"
|
||||
],
|
||||
"cases-rules": [
|
||||
"counter",
|
||||
"createdAt",
|
||||
|
|
|
@ -600,6 +600,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"incremental_id": {
|
||||
"type": "unsigned_long"
|
||||
},
|
||||
"observables": {
|
||||
"properties": {
|
||||
"typeKey": {
|
||||
|
@ -733,6 +736,20 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"cases-incrementing-id": {
|
||||
"dynamic": false,
|
||||
"properties": {
|
||||
"@timestamp": {
|
||||
"type": "date"
|
||||
},
|
||||
"last_id": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "date"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cases-rules": {
|
||||
"dynamic": false,
|
||||
"properties": {
|
||||
|
|
|
@ -11,4 +11,4 @@ export { registerCoreObjectTypes } from './registration';
|
|||
|
||||
// set minimum number of registered saved objects to ensure no object types are removed after 8.8
|
||||
// declared in internal implementation exclicilty to prevent unintended changes.
|
||||
export const SAVED_OBJECT_TYPES_COUNT = 132 as const;
|
||||
export const SAVED_OBJECT_TYPES_COUNT = 133 as const;
|
||||
|
|
|
@ -76,10 +76,11 @@ describe('checking migration metadata changes on all registered SO types', () =>
|
|||
"canvas-element": "cdedc2123eb8a1506b87a56b0bcce60f4ec08bc8",
|
||||
"canvas-workpad": "9d82aafb19586b119e5c9382f938abe28c26ca5c",
|
||||
"canvas-workpad-template": "c077b0087346776bb3542b51e1385d172cb24179",
|
||||
"cases": "91771732e2e488e4c1b1ac468057925d1c6b32b5",
|
||||
"cases": "1a51230a1b00364e5a534b6d481779be1b751384",
|
||||
"cases-comments": "5cb0a421588831c2a950e50f486048b8aabbae25",
|
||||
"cases-configure": "44ed7b8e0f44df39516b8870589b89e32224d2bf",
|
||||
"cases-connector-mappings": "f9d1ac57e484e69506c36a8051e4d61f4a8cfd25",
|
||||
"cases-incrementing-id": "3785df401fb95aedd0ab94fd047871386a568f5e",
|
||||
"cases-rules": "6d1776f5c46a99e1a0f3085c537146c1cdfbc829",
|
||||
"cases-telemetry": "f219eb7e26772884342487fc9602cfea07b3cedc",
|
||||
"cases-user-actions": "483f10db9b3bd1617948d7032a98b7791bf87414",
|
||||
|
|
|
@ -38,6 +38,7 @@ const previouslyRegisteredTypes = [
|
|||
'cases-comments',
|
||||
'cases-configure',
|
||||
'cases-connector-mappings',
|
||||
'cases-incrementing-id', // Added in 8.19/9.1 to allow for incremental numerical ids in cases
|
||||
'cases-rules',
|
||||
'cases-sub-case',
|
||||
'cases-user-actions',
|
||||
|
|
|
@ -25,6 +25,7 @@ export const CASE_USER_ACTION_SAVED_OBJECT = 'cases-user-actions' as const;
|
|||
export const CASE_COMMENT_SAVED_OBJECT = 'cases-comments' as const;
|
||||
export const CASE_CONFIGURE_SAVED_OBJECT = 'cases-configure' as const;
|
||||
export const CASE_RULES_SAVED_OBJECT = 'cases-rules' as const;
|
||||
export const CASE_ID_INCREMENTER_SAVED_OBJECT = 'cases-incrementing-id' as const;
|
||||
|
||||
/**
|
||||
* If more values are added here please also add them here: x-pack/test/cases_api_integration/common/plugins
|
||||
|
|
|
@ -134,6 +134,7 @@ const basicCase: Case = {
|
|||
description: null,
|
||||
},
|
||||
],
|
||||
incremental_id: 123,
|
||||
};
|
||||
|
||||
describe('CasePostRequestRt', () => {
|
||||
|
|
|
@ -92,6 +92,7 @@ const basicCase = {
|
|||
},
|
||||
],
|
||||
observables: [],
|
||||
incremental_id: undefined,
|
||||
};
|
||||
|
||||
describe('RelatedCaseRt', () => {
|
||||
|
@ -206,6 +207,7 @@ describe('CaseAttributesRt', () => {
|
|||
},
|
||||
],
|
||||
observables: [],
|
||||
incremental_id: undefined,
|
||||
};
|
||||
|
||||
it('has expected attributes in request', () => {
|
||||
|
|
|
@ -53,6 +53,8 @@ export const CaseSettingsRt = rt.strict({
|
|||
syncAlerts: rt.boolean,
|
||||
});
|
||||
|
||||
export const CaseIncrementalId = rt.union([rt.number, rt.undefined]);
|
||||
|
||||
const CaseBaseFields = {
|
||||
/**
|
||||
* The description of the case
|
||||
|
@ -95,6 +97,10 @@ const CaseBaseFields = {
|
|||
* Observables
|
||||
*/
|
||||
observables: rt.array(CaseObservableRt),
|
||||
/**
|
||||
* Incremental ID
|
||||
*/
|
||||
incremental_id: CaseIncrementalId,
|
||||
};
|
||||
|
||||
export const CaseBaseOptionalFieldsRt = rt.exact(
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './v1';
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 * as rt from 'io-ts';
|
||||
|
||||
export const CaseIdIncrementerAttributesRt = rt.strict({
|
||||
'@timestamp': rt.number,
|
||||
updated_at: rt.number,
|
||||
last_id: rt.number,
|
||||
});
|
|
@ -256,6 +256,7 @@ export const basicCase: CaseUI = {
|
|||
category: null,
|
||||
customFields: [],
|
||||
observables: [],
|
||||
incrementalId: undefined,
|
||||
};
|
||||
|
||||
export const basicFileMock: FileJSON = {
|
||||
|
@ -380,6 +381,7 @@ export const mockCase: CaseUI = {
|
|||
category: null,
|
||||
customFields: [],
|
||||
observables: [],
|
||||
incrementalId: undefined,
|
||||
};
|
||||
|
||||
export const basicCasePost: CaseUI = {
|
||||
|
@ -565,6 +567,7 @@ export const basicCaseSnake: Case = {
|
|||
updated_by: elasticUserSnake,
|
||||
owner: SECURITY_SOLUTION_OWNER,
|
||||
customFields: [],
|
||||
incremental_id: undefined,
|
||||
} as Case;
|
||||
|
||||
export const caseWithAlertsSnake = {
|
||||
|
|
165
x-pack/platform/plugins/shared/cases/scripts/generate_cases.js
Normal file
165
x-pack/platform/plugins/shared/cases/scripts/generate_cases.js
Normal file
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const http = require('http');
|
||||
const pMap = require('p-map');
|
||||
const yargs = require('yargs');
|
||||
|
||||
const username = 'elastic';
|
||||
const password = 'changeme';
|
||||
|
||||
const makeRequest = async ({ options, data }) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reqData = JSON.stringify(data);
|
||||
const reqOptions = {
|
||||
...options,
|
||||
rejectUnauthorized: false,
|
||||
requestCert: true,
|
||||
agent: false,
|
||||
headers: {
|
||||
...options.headers,
|
||||
Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': reqData.length,
|
||||
'kbn-xsrf': 'true',
|
||||
},
|
||||
};
|
||||
|
||||
const req = http.request(reqOptions, (res) => {
|
||||
const body = [];
|
||||
|
||||
res.on('data', (chunk) => body.push(chunk));
|
||||
res.on('end', () => {
|
||||
const resString = Buffer.concat(body).toString();
|
||||
try {
|
||||
if (resString != null && resString.length > 0) {
|
||||
const res = JSON.parse(resString);
|
||||
if (res.statusCode && res.statusCode === 400) {
|
||||
reject(new Error(res.message));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
resolve(resString);
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
reject(new Error('Request time out'));
|
||||
});
|
||||
|
||||
req.write(reqData);
|
||||
req.end();
|
||||
});
|
||||
};
|
||||
|
||||
const getHostAndPort = () => ({
|
||||
host: '127.0.0.1',
|
||||
port: 5601,
|
||||
});
|
||||
|
||||
const createCase = (counter, owner) => ({
|
||||
title: `Sample Case ${counter}`,
|
||||
tags: [],
|
||||
severity: 'low',
|
||||
description: `Auto generated case ${counter}`,
|
||||
assignees: [],
|
||||
connector: {
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: '.none',
|
||||
fields: null,
|
||||
},
|
||||
settings: {
|
||||
syncAlerts: false,
|
||||
},
|
||||
owner: owner ?? 'cases',
|
||||
customFields: [],
|
||||
});
|
||||
|
||||
const generateCases = async (cases, space) => {
|
||||
try {
|
||||
console.log(`Creating ${cases.length} cases in ${space ? `space: ${space}` : 'default space'}`);
|
||||
const path = space ? `/s/${space}/api/cases` : '/api/cases';
|
||||
await pMap(
|
||||
cases,
|
||||
(theCase) => {
|
||||
const options = {
|
||||
...getHostAndPort(),
|
||||
path,
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
return makeRequest({ options, data: theCase });
|
||||
},
|
||||
{ concurrency: 100 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
try {
|
||||
const argv = yargs.help().options({
|
||||
count: {
|
||||
alias: 'c',
|
||||
describe: 'number of cases to generate',
|
||||
type: 'number',
|
||||
default: 10,
|
||||
},
|
||||
owners: {
|
||||
alias: 'o',
|
||||
describe:
|
||||
'solutions where the cases should be created. combination of securitySolution, observability, or cases',
|
||||
default: 'cases',
|
||||
type: 'array',
|
||||
},
|
||||
space: {
|
||||
alias: 's',
|
||||
describe: 'space where the cases should be created',
|
||||
default: '',
|
||||
type: 'string',
|
||||
},
|
||||
}).argv;
|
||||
|
||||
const { count, owners, space } = argv;
|
||||
const numCasesToCreate = Number(count);
|
||||
const potentialOwners = new Set(['securitySolution', 'observability', 'cases']);
|
||||
|
||||
const invalidOwnerProvided = owners.some((owner) => !potentialOwners.has(owner));
|
||||
|
||||
if (invalidOwnerProvided) {
|
||||
console.error('Only valid owners are securitySolution, observability, and cases');
|
||||
// eslint-disable-next-line no-process-exit
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const cases = Array(numCasesToCreate)
|
||||
.fill(null)
|
||||
.map((_, index) => {
|
||||
const owner = owners[Math.floor(Math.random() * owners.length)];
|
||||
return createCase(index + 1, owner);
|
||||
});
|
||||
|
||||
await generateCases(cases, space);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
main();
|
||||
|
||||
process.on('uncaughtException', function (err) {
|
||||
console.log(err);
|
||||
});
|
|
@ -121,6 +121,7 @@ describe('bulkCreate', () => {
|
|||
"duration": null,
|
||||
"external_service": null,
|
||||
"id": "mock-id-1",
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
@ -165,6 +166,7 @@ describe('bulkCreate', () => {
|
|||
"duration": null,
|
||||
"external_service": null,
|
||||
"id": "mock-id-1",
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
@ -245,6 +247,7 @@ describe('bulkCreate', () => {
|
|||
"duration": null,
|
||||
"external_service": null,
|
||||
"id": "mock-saved-object-id",
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
@ -284,6 +287,7 @@ describe('bulkCreate', () => {
|
|||
"duration": null,
|
||||
"external_service": null,
|
||||
"id": "mock-saved-object-id",
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
|
|
@ -901,6 +901,7 @@ describe('update', () => {
|
|||
"duration": null,
|
||||
"external_service": null,
|
||||
"id": "mock-id-1",
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
@ -945,6 +946,7 @@ describe('update', () => {
|
|||
"duration": null,
|
||||
"external_service": null,
|
||||
"id": "mock-id-2",
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
|
|
@ -18,12 +18,7 @@ import { SECURITY_SOLUTION_OWNER } from '../../../common';
|
|||
import { mockCases } from '../../mocks';
|
||||
import { createCasesClientMock, createCasesClientMockArgs } from '../mocks';
|
||||
import { create } from './create';
|
||||
import {
|
||||
CaseSeverity,
|
||||
CaseStatuses,
|
||||
ConnectorTypes,
|
||||
CustomFieldTypes,
|
||||
} from '../../../common/types/domain';
|
||||
import { CaseSeverity, ConnectorTypes, CustomFieldTypes } from '../../../common/types/domain';
|
||||
|
||||
import type { CaseCustomFields } from '../../../common/types/domain';
|
||||
import { omit } from 'lodash';
|
||||
|
@ -244,24 +239,9 @@ describe('create', () => {
|
|||
|
||||
expect(clientArgs.services.caseService.createCase).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
attributes: {
|
||||
...theCase,
|
||||
closed_by: null,
|
||||
closed_at: null,
|
||||
attributes: expect.objectContaining({
|
||||
title: 'title with spaces',
|
||||
created_at: expect.any(String),
|
||||
created_by: expect.any(Object),
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
external_service: null,
|
||||
duration: null,
|
||||
status: CaseStatuses.open,
|
||||
category: null,
|
||||
customFields: [],
|
||||
observables: [],
|
||||
},
|
||||
id: expect.any(String),
|
||||
refresh: false,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@ -322,24 +302,9 @@ describe('create', () => {
|
|||
|
||||
expect(clientArgs.services.caseService.createCase).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
attributes: {
|
||||
...theCase,
|
||||
closed_by: null,
|
||||
closed_at: null,
|
||||
attributes: expect.objectContaining({
|
||||
description: 'this is a description with spaces!!',
|
||||
created_at: expect.any(String),
|
||||
created_by: expect.any(Object),
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
external_service: null,
|
||||
duration: null,
|
||||
status: CaseStatuses.open,
|
||||
category: null,
|
||||
customFields: [],
|
||||
observables: [],
|
||||
},
|
||||
id: expect.any(String),
|
||||
refresh: false,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@ -402,24 +367,9 @@ describe('create', () => {
|
|||
|
||||
expect(clientArgs.services.caseService.createCase).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
attributes: {
|
||||
...theCase,
|
||||
closed_by: null,
|
||||
closed_at: null,
|
||||
attributes: expect.objectContaining({
|
||||
tags: ['pepsi', 'coke'],
|
||||
created_at: expect.any(String),
|
||||
created_by: expect.any(Object),
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
external_service: null,
|
||||
duration: null,
|
||||
status: CaseStatuses.open,
|
||||
category: null,
|
||||
customFields: [],
|
||||
observables: [],
|
||||
},
|
||||
id: expect.any(String),
|
||||
refresh: false,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@ -470,23 +420,9 @@ describe('create', () => {
|
|||
|
||||
expect(clientArgs.services.caseService.createCase).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
attributes: {
|
||||
...theCase,
|
||||
closed_by: null,
|
||||
closed_at: null,
|
||||
attributes: expect.objectContaining({
|
||||
category: 'reporting',
|
||||
created_at: expect.any(String),
|
||||
created_by: expect.any(Object),
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
external_service: null,
|
||||
duration: null,
|
||||
status: CaseStatuses.open,
|
||||
customFields: [],
|
||||
observables: [],
|
||||
},
|
||||
id: expect.any(String),
|
||||
refresh: false,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@ -550,23 +486,9 @@ describe('create', () => {
|
|||
|
||||
expect(clientArgs.services.caseService.createCase).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
attributes: {
|
||||
...theCase,
|
||||
closed_by: null,
|
||||
closed_at: null,
|
||||
category: null,
|
||||
created_at: expect.any(String),
|
||||
created_by: expect.any(Object),
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
external_service: null,
|
||||
duration: null,
|
||||
status: CaseStatuses.open,
|
||||
attributes: expect.objectContaining({
|
||||
customFields: theCustomFields,
|
||||
observables: [],
|
||||
},
|
||||
id: expect.any(String),
|
||||
refresh: false,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@ -576,26 +498,12 @@ describe('create', () => {
|
|||
|
||||
expect(clientArgs.services.caseService.createCase).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
attributes: {
|
||||
...theCase,
|
||||
closed_by: null,
|
||||
closed_at: null,
|
||||
category: null,
|
||||
created_at: expect.any(String),
|
||||
created_by: expect.any(Object),
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
external_service: null,
|
||||
duration: null,
|
||||
status: CaseStatuses.open,
|
||||
attributes: expect.objectContaining({
|
||||
customFields: [
|
||||
{ key: 'first_key', type: 'text', value: 'default value' },
|
||||
{ key: 'second_key', type: 'toggle', value: null },
|
||||
],
|
||||
observables: [],
|
||||
},
|
||||
id: expect.any(String),
|
||||
refresh: false,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
|
|
@ -50,6 +50,7 @@ export interface CasePersistedAttributes {
|
|||
category?: string | null;
|
||||
customFields?: CasePersistedCustomFields;
|
||||
observables?: Observable[];
|
||||
incremental_id?: number;
|
||||
}
|
||||
|
||||
type CasePersistedCustomFields = Array<{
|
||||
|
|
|
@ -150,6 +150,7 @@ describe('common utils', () => {
|
|||
"description": "A description",
|
||||
"duration": null,
|
||||
"external_service": null,
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
@ -206,6 +207,7 @@ describe('common utils', () => {
|
|||
"description": "A description",
|
||||
"duration": null,
|
||||
"external_service": null,
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
@ -266,6 +268,7 @@ describe('common utils', () => {
|
|||
"description": "A description",
|
||||
"duration": null,
|
||||
"external_service": null,
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
@ -332,6 +335,7 @@ describe('common utils', () => {
|
|||
"description": "A description",
|
||||
"duration": null,
|
||||
"external_service": null,
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
@ -393,6 +397,7 @@ describe('common utils', () => {
|
|||
"duration": null,
|
||||
"external_service": null,
|
||||
"id": "mock-id-1",
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
@ -437,6 +442,7 @@ describe('common utils', () => {
|
|||
"duration": null,
|
||||
"external_service": null,
|
||||
"id": "mock-id-2",
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
@ -485,6 +491,7 @@ describe('common utils', () => {
|
|||
"duration": null,
|
||||
"external_service": null,
|
||||
"id": "mock-id-3",
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
@ -537,6 +544,7 @@ describe('common utils', () => {
|
|||
"duration": null,
|
||||
"external_service": null,
|
||||
"id": "mock-id-4",
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
@ -618,6 +626,7 @@ describe('common utils', () => {
|
|||
"duration": null,
|
||||
"external_service": null,
|
||||
"id": "mock-id-1",
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
@ -687,6 +696,7 @@ describe('common utils', () => {
|
|||
"duration": null,
|
||||
"external_service": null,
|
||||
"id": "mock-id-3",
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
@ -747,6 +757,7 @@ describe('common utils', () => {
|
|||
"duration": null,
|
||||
"external_service": null,
|
||||
"id": "mock-id-3",
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
@ -830,6 +841,7 @@ describe('common utils', () => {
|
|||
"duration": null,
|
||||
"external_service": null,
|
||||
"id": "mock-id-3",
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
@ -888,6 +900,7 @@ describe('common utils', () => {
|
|||
"duration": null,
|
||||
"external_service": null,
|
||||
"id": "mock-id-1",
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
@ -951,6 +964,7 @@ describe('common utils', () => {
|
|||
"duration": null,
|
||||
"external_service": null,
|
||||
"id": "mock-id-1",
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
|
|
@ -89,6 +89,7 @@ export const transformNewCase = ({
|
|||
category: newCase.category ?? null,
|
||||
customFields: newCase.customFields ?? [],
|
||||
observables: [],
|
||||
incremental_id: undefined,
|
||||
});
|
||||
|
||||
export const transformCases = ({
|
||||
|
|
|
@ -147,6 +147,7 @@ export const mockCases: CaseSavedObjectTransformed[] = [
|
|||
duration: null,
|
||||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
external_service: null,
|
||||
incremental_id: undefined,
|
||||
title: 'Super Bad Security Issue',
|
||||
status: CaseStatuses.open,
|
||||
tags: ['defacement'],
|
||||
|
@ -191,6 +192,7 @@ export const mockCases: CaseSavedObjectTransformed[] = [
|
|||
duration: null,
|
||||
description: 'Oh no, a bad meanie destroying data!',
|
||||
external_service: null,
|
||||
incremental_id: undefined,
|
||||
title: 'Damaging Data Destruction Detected',
|
||||
status: CaseStatuses.open,
|
||||
tags: ['Data Destruction'],
|
||||
|
@ -235,6 +237,7 @@ export const mockCases: CaseSavedObjectTransformed[] = [
|
|||
duration: null,
|
||||
description: 'Oh no, a bad meanie going LOLBins all over the place!',
|
||||
external_service: null,
|
||||
incremental_id: undefined,
|
||||
title: 'Another bad one',
|
||||
status: CaseStatuses.open,
|
||||
tags: ['LOLBins'],
|
||||
|
@ -283,6 +286,7 @@ export const mockCases: CaseSavedObjectTransformed[] = [
|
|||
duration: null,
|
||||
description: 'Oh no, a bad meanie going LOLBins all over the place!',
|
||||
external_service: null,
|
||||
incremental_id: undefined,
|
||||
status: CaseStatuses.closed,
|
||||
title: 'Another bad one',
|
||||
tags: ['LOLBins'],
|
||||
|
|
|
@ -17,7 +17,8 @@ import { CASE_SAVED_OBJECT } from '../../../common/constants';
|
|||
import type { CasePersistedAttributes } from '../../common/types/case';
|
||||
import { handleExport } from '../import_export/export';
|
||||
import { caseMigrations } from '../migrations';
|
||||
import { modelVersion1, modelVersion2 } from './model_versions';
|
||||
import { modelVersion1, modelVersion2, modelVersion3 } from './model_versions';
|
||||
import { handleImport } from '../import_export/import';
|
||||
|
||||
export const createCaseSavedObjectType = (
|
||||
coreSetup: CoreSetup,
|
||||
|
@ -239,12 +240,16 @@ export const createCaseSavedObjectType = (
|
|||
},
|
||||
},
|
||||
},
|
||||
incremental_id: {
|
||||
type: 'unsigned_long',
|
||||
},
|
||||
},
|
||||
},
|
||||
migrations: caseMigrations,
|
||||
modelVersions: {
|
||||
1: modelVersion1,
|
||||
2: modelVersion2,
|
||||
3: modelVersion3,
|
||||
},
|
||||
management: {
|
||||
importableAndExportable: true,
|
||||
|
@ -255,5 +260,6 @@ export const createCaseSavedObjectType = (
|
|||
context: SavedObjectsExportTransformContext,
|
||||
objects: Array<SavedObject<CasePersistedAttributes>>
|
||||
) => handleExport({ context, objects, coreSetup, logger }),
|
||||
onImport: (objects: Array<SavedObject<CasePersistedAttributes>>) => handleImport({ objects }),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { modelVersion1 } from './model_version_1';
|
||||
export { modelVersion2 } from './model_version_2';
|
||||
export { modelVersion3 } from './model_version_3';
|
|
@ -6,8 +6,7 @@
|
|||
*/
|
||||
|
||||
import type { SavedObjectsModelVersion } from '@kbn/core-saved-objects-server';
|
||||
import { casesSchemaV1, casesSchemaV2 } from './schemas';
|
||||
|
||||
import { casesSchemaV1 } from '../schemas';
|
||||
/**
|
||||
* Adds custom fields to the cases SO.
|
||||
*/
|
||||
|
@ -58,30 +57,3 @@ export const modelVersion1: SavedObjectsModelVersion = {
|
|||
forwardCompatibility: casesSchemaV1.extends({}, { unknowns: 'ignore' }),
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds case observables to the cases SO.
|
||||
*/
|
||||
export const modelVersion2: SavedObjectsModelVersion = {
|
||||
changes: [
|
||||
{
|
||||
type: 'mappings_addition',
|
||||
addedMappings: {
|
||||
observables: {
|
||||
type: 'nested',
|
||||
properties: {
|
||||
typeKey: {
|
||||
type: 'keyword',
|
||||
},
|
||||
value: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
schemas: {
|
||||
forwardCompatibility: casesSchemaV2.extends({}, { unknowns: 'ignore' }),
|
||||
},
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 type { SavedObjectsModelVersion } from '@kbn/core-saved-objects-server';
|
||||
import { casesSchemaV2 } from '../schemas';
|
||||
|
||||
export const modelVersion2: SavedObjectsModelVersion = {
|
||||
changes: [
|
||||
{
|
||||
type: 'mappings_addition',
|
||||
addedMappings: {
|
||||
observables: {
|
||||
type: 'nested',
|
||||
properties: {
|
||||
typeKey: {
|
||||
type: 'keyword',
|
||||
},
|
||||
value: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
schemas: {
|
||||
forwardCompatibility: casesSchemaV2.extends({}, { unknowns: 'ignore' }),
|
||||
},
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 type { SavedObjectsModelVersion } from '@kbn/core-saved-objects-server';
|
||||
import { casesSchemaV3 } from '../schemas';
|
||||
|
||||
/**
|
||||
* Adds the incremental id to the cases SO.
|
||||
*/
|
||||
export const modelVersion3: SavedObjectsModelVersion = {
|
||||
changes: [
|
||||
{
|
||||
type: 'mappings_addition',
|
||||
addedMappings: {
|
||||
incremental_id: {
|
||||
type: 'unsigned_long',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
schemas: {
|
||||
forwardCompatibility: casesSchemaV3.extends({}, { unknowns: 'ignore' }),
|
||||
},
|
||||
};
|
|
@ -5,11 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { modelVersion1, modelVersion2 } from './model_versions';
|
||||
import { modelVersion1, modelVersion2, modelVersion3 } from '.';
|
||||
|
||||
describe('Model versions', () => {
|
||||
describe('1', () => {
|
||||
it('returns the model version correctly', () => {
|
||||
describe('version 1', () => {
|
||||
it('returns version 1 changes correctly', () => {
|
||||
expect(modelVersion1.changes).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
|
@ -57,26 +57,45 @@ describe('Model versions', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('2', () => {
|
||||
expect(modelVersion2.changes).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"addedMappings": Object {
|
||||
"observables": Object {
|
||||
"properties": Object {
|
||||
"typeKey": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
"value": Object {
|
||||
"type": "keyword",
|
||||
describe('version 2', () => {
|
||||
it('returns version 2 changes correctly', () => {
|
||||
expect(modelVersion2.changes).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"addedMappings": Object {
|
||||
"observables": Object {
|
||||
"properties": Object {
|
||||
"typeKey": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
"value": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
},
|
||||
"type": "nested",
|
||||
},
|
||||
},
|
||||
"type": "mappings_addition",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('version 3', () => {
|
||||
it('returns version 3 changes correctly', () => {
|
||||
expect(modelVersion3.changes).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"addedMappings": Object {
|
||||
"incremental_id": Object {
|
||||
"type": "unsigned_long",
|
||||
},
|
||||
"type": "nested",
|
||||
},
|
||||
"type": "mappings_addition",
|
||||
},
|
||||
"type": "mappings_addition",
|
||||
},
|
||||
]
|
||||
`);
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -9,3 +9,4 @@ export * from './latest';
|
|||
|
||||
export { casesSchema as casesSchemaV1 } from './v1';
|
||||
export { casesSchema as casesSchemaV2 } from './v2';
|
||||
export { casesSchema as casesSchemaV3 } from './v3';
|
||||
|
|
|
@ -5,4 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './v2';
|
||||
export * from './v3';
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
import { casesSchema as casesSchemaV2 } from './v2';
|
||||
|
||||
export const casesSchema = casesSchemaV2.extends({
|
||||
incremental_id: schema.maybe(schema.nullable(schema.number())),
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 { coreMock } from '@kbn/core/server/mocks';
|
||||
import { createCaseSavedObjectType } from './cases';
|
||||
import {
|
||||
createModelVersionTestMigrator,
|
||||
type ModelVersionTestMigrator,
|
||||
} from '@kbn/core-test-helpers-model-versions';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import { createCaseSavedObjectResponse } from '../../services/test_utils';
|
||||
|
||||
const mockLogger = loggerMock.create();
|
||||
const mockCoreSetup = coreMock.createSetup();
|
||||
const caseSavedObjectType = createCaseSavedObjectType(mockCoreSetup, mockLogger);
|
||||
|
||||
describe('caseSavedObjectType model version transformations', () => {
|
||||
let migrator: ModelVersionTestMigrator;
|
||||
|
||||
beforeEach(() => {
|
||||
migrator = createModelVersionTestMigrator({ type: caseSavedObjectType });
|
||||
});
|
||||
|
||||
describe('Model version 1 to 2', () => {
|
||||
const version2Fields = ['observables'];
|
||||
it('by default does not add the new fields to the object', () => {
|
||||
const caseObj = createCaseSavedObjectResponse();
|
||||
|
||||
const migrated = migrator.migrate({
|
||||
document: caseObj,
|
||||
fromVersion: 1,
|
||||
toVersion: 2,
|
||||
});
|
||||
|
||||
version2Fields.forEach((field) => {
|
||||
expect(migrated.attributes).not.toHaveProperty(field);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Model version 2 to 3', () => {
|
||||
const version3Fields = ['incremental_id'];
|
||||
|
||||
it('by default does not add the new fields to the object', () => {
|
||||
const migrated = migrator.migrate({
|
||||
document: createCaseSavedObjectResponse(),
|
||||
fromVersion: 2,
|
||||
toVersion: 3,
|
||||
});
|
||||
|
||||
version3Fields.forEach((field) => {
|
||||
expect(migrated.attributes).not.toHaveProperty(field);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { ALERTING_CASES_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server';
|
||||
import type { SavedObjectsType } from '@kbn/core/server';
|
||||
import { CASE_ID_INCREMENTER_SAVED_OBJECT } from '../../common/constants';
|
||||
|
||||
export const caseIdIncrementerSavedObjectType: SavedObjectsType = {
|
||||
name: CASE_ID_INCREMENTER_SAVED_OBJECT,
|
||||
indexPattern: ALERTING_CASES_SAVED_OBJECT_INDEX,
|
||||
hidden: true,
|
||||
namespaceType: 'multiple-isolated',
|
||||
mappings: {
|
||||
dynamic: false,
|
||||
properties: {
|
||||
last_id: {
|
||||
type: 'keyword',
|
||||
},
|
||||
'@timestamp': {
|
||||
type: 'date',
|
||||
},
|
||||
updated_at: {
|
||||
type: 'date',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 { httpServerMock, loggingSystemMock, coreMock } from '@kbn/core/server/mocks';
|
||||
import type { SavedObjectsExportTransformContext } from '@kbn/core/server';
|
||||
import { handleExport } from './export';
|
||||
import { mockCases } from '../../mocks';
|
||||
import type { CaseSavedObjectTransformed } from '../../common/types/case';
|
||||
|
||||
jest.mock('./utils', () => {
|
||||
return {
|
||||
getAttachmentsAndUserActionsForCases: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
});
|
||||
|
||||
describe('case export', () => {
|
||||
const testRequest = httpServerMock.createFakeKibanaRequest({});
|
||||
const testContext: SavedObjectsExportTransformContext = { request: testRequest };
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
const testCases: CaseSavedObjectTransformed[] = mockCases.map((_case, idx) => ({
|
||||
..._case,
|
||||
attributes: {
|
||||
..._case.attributes,
|
||||
incremental_id: idx + 1,
|
||||
},
|
||||
}));
|
||||
|
||||
it('should remove `incremental_id` from cases when exporting', async () => {
|
||||
const exported = await handleExport({
|
||||
context: testContext,
|
||||
coreSetup: coreMock.createSetup(),
|
||||
// @ts-ignore: mock objects are not matching persisted objects
|
||||
objects: testCases,
|
||||
logger,
|
||||
});
|
||||
|
||||
const containsIncrementalId = exported.some((exportedCase) => {
|
||||
return (
|
||||
'incremental_id' in exportedCase.attributes &&
|
||||
exportedCase.attributes.incremental_id !== undefined
|
||||
);
|
||||
});
|
||||
|
||||
expect(containsIncrementalId).toBeFalsy();
|
||||
});
|
||||
});
|
|
@ -9,23 +9,16 @@ import type {
|
|||
CoreSetup,
|
||||
Logger,
|
||||
SavedObject,
|
||||
SavedObjectsClientContract,
|
||||
SavedObjectsExportTransformContext,
|
||||
} from '@kbn/core/server';
|
||||
import type {
|
||||
CaseUserActionWithoutReferenceIds,
|
||||
AttachmentAttributesWithoutRefs,
|
||||
} from '../../../common/types/domain';
|
||||
import {
|
||||
CASE_COMMENT_SAVED_OBJECT,
|
||||
CASE_SAVED_OBJECT,
|
||||
CASE_USER_ACTION_SAVED_OBJECT,
|
||||
MAX_DOCS_PER_PAGE,
|
||||
SAVED_OBJECT_TYPES,
|
||||
} from '../../../common/constants';
|
||||
import { defaultSortField } from '../../common/utils';
|
||||
import { SAVED_OBJECT_TYPES } from '../../../common/constants';
|
||||
import { createCaseError } from '../../common/error';
|
||||
import type { CasePersistedAttributes } from '../../common/types/case';
|
||||
import { getAttachmentsAndUserActionsForCases } from './utils';
|
||||
|
||||
export async function handleExport({
|
||||
context,
|
||||
|
@ -49,18 +42,25 @@ export async function handleExport({
|
|||
return [];
|
||||
}
|
||||
|
||||
const cleanedObjects: Array<SavedObject<CasePersistedAttributes>> = objects.map((obj) => ({
|
||||
...obj,
|
||||
attributes: {
|
||||
...obj.attributes,
|
||||
incremental_id: undefined,
|
||||
},
|
||||
}));
|
||||
const [{ savedObjects }] = await coreSetup.getStartServices();
|
||||
const savedObjectsClient = savedObjects.getScopedClient(context.request, {
|
||||
includedHiddenTypes: SAVED_OBJECT_TYPES,
|
||||
});
|
||||
|
||||
const caseIds = objects.map((caseObject) => caseObject.id);
|
||||
const caseIds = cleanedObjects.map((caseObject) => caseObject.id);
|
||||
const attachmentsAndUserActionsForCases = await getAttachmentsAndUserActionsForCases(
|
||||
savedObjectsClient,
|
||||
caseIds
|
||||
);
|
||||
|
||||
return [...objects, ...attachmentsAndUserActionsForCases.flat()];
|
||||
return [...cleanedObjects, ...attachmentsAndUserActionsForCases.flat()];
|
||||
} catch (error) {
|
||||
throw createCaseError({
|
||||
message: `Failed to retrieve associated objects for exporting of cases: ${error}`,
|
||||
|
@ -69,57 +69,3 @@ export async function handleExport({
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function getAttachmentsAndUserActionsForCases(
|
||||
savedObjectsClient: SavedObjectsClientContract,
|
||||
caseIds: string[]
|
||||
): Promise<
|
||||
Array<SavedObject<AttachmentAttributesWithoutRefs | CaseUserActionWithoutReferenceIds>>
|
||||
> {
|
||||
const [attachments, userActions] = await Promise.all([
|
||||
getAssociatedObjects<AttachmentAttributesWithoutRefs>({
|
||||
savedObjectsClient,
|
||||
caseIds,
|
||||
sortField: defaultSortField,
|
||||
type: CASE_COMMENT_SAVED_OBJECT,
|
||||
}),
|
||||
getAssociatedObjects<CaseUserActionWithoutReferenceIds>({
|
||||
savedObjectsClient,
|
||||
caseIds,
|
||||
sortField: defaultSortField,
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
}),
|
||||
]);
|
||||
|
||||
return [...attachments, ...userActions];
|
||||
}
|
||||
|
||||
async function getAssociatedObjects<T>({
|
||||
savedObjectsClient,
|
||||
caseIds,
|
||||
sortField,
|
||||
type,
|
||||
}: {
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
caseIds: string[];
|
||||
sortField: string;
|
||||
type: string;
|
||||
}): Promise<Array<SavedObject<T>>> {
|
||||
const references = caseIds.map((id) => ({ type: CASE_SAVED_OBJECT, id }));
|
||||
|
||||
const finder = savedObjectsClient.createPointInTimeFinder<T>({
|
||||
type,
|
||||
hasReferenceOperator: 'OR',
|
||||
hasReference: references,
|
||||
perPage: MAX_DOCS_PER_PAGE,
|
||||
sortField,
|
||||
sortOrder: 'asc',
|
||||
});
|
||||
|
||||
let result: Array<SavedObject<T>> = [];
|
||||
for await (const findResults of finder.find()) {
|
||||
result = result.concat(findResults.saved_objects);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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 type { CaseSavedObjectTransformed } from '../../common/types/case';
|
||||
import { mockCases } from '../../mocks';
|
||||
import { handleImport } from './import';
|
||||
|
||||
describe('case import', () => {
|
||||
it('should raise a warning when import contains a case with `incremental_id`', () => {
|
||||
const testCases: CaseSavedObjectTransformed[] = mockCases.map((_case, idx) => ({
|
||||
..._case,
|
||||
attributes: {
|
||||
..._case.attributes,
|
||||
incremental_id: idx + 1,
|
||||
},
|
||||
}));
|
||||
// @ts-ignore: cases attribtue types are not correct
|
||||
expect(handleImport({ objects: testCases })).toEqual(
|
||||
expect.objectContaining({
|
||||
warnings: expect.arrayContaining([
|
||||
{ message: 'The `incremental_id` field is not supported on importing.', type: 'simple' },
|
||||
]),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should raise a warning when import contains a case with `incremental_id` set to 0', () => {
|
||||
const testCases: CaseSavedObjectTransformed[] = mockCases.map((_case) => ({
|
||||
..._case,
|
||||
attributes: {
|
||||
..._case.attributes,
|
||||
incremental_id: 0,
|
||||
},
|
||||
}));
|
||||
// @ts-ignore: cases attribtue types are not correct
|
||||
expect(handleImport({ objects: testCases })).toEqual(
|
||||
expect.objectContaining({
|
||||
warnings: expect.arrayContaining([
|
||||
{ message: 'The `incremental_id` field is not supported on importing.', type: 'simple' },
|
||||
]),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should raise a warning when import contains a case with `incremental_id` set to a negative value', () => {
|
||||
const testCases: CaseSavedObjectTransformed[] = mockCases.map((_case) => ({
|
||||
..._case,
|
||||
attributes: {
|
||||
..._case.attributes,
|
||||
incremental_id: -1,
|
||||
},
|
||||
}));
|
||||
// @ts-ignore: cases attribtue types are not correct
|
||||
expect(handleImport({ objects: testCases })).toEqual(
|
||||
expect.objectContaining({
|
||||
warnings: expect.arrayContaining([
|
||||
{ message: 'The `incremental_id` field is not supported on importing.', type: 'simple' },
|
||||
]),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should not raise a warning when import contains no case with `incremental_id`', () => {
|
||||
const testCases: CaseSavedObjectTransformed[] = mockCases.map((_case) => ({
|
||||
..._case,
|
||||
attributes: {
|
||||
..._case.attributes,
|
||||
incremental_id: undefined,
|
||||
},
|
||||
}));
|
||||
// @ts-ignore: cases attribtue types are not correct
|
||||
expect(handleImport({ objects: testCases })).not.toEqual(
|
||||
expect.objectContaining({
|
||||
warnings: expect.arrayContaining([
|
||||
{ message: 'The `incremental_id` field is not supported on importing.', type: 'simple' },
|
||||
]),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 type { SavedObject, SavedObjectsImportHookResult } from '@kbn/core/server';
|
||||
import type { CasePersistedAttributes } from '../../common/types/case';
|
||||
|
||||
export function handleImport({
|
||||
objects,
|
||||
}: {
|
||||
objects: Array<SavedObject<CasePersistedAttributes>>;
|
||||
}): SavedObjectsImportHookResult {
|
||||
const hasObjectsWithIncrementalId = objects.some(
|
||||
(obj) => obj.attributes.incremental_id !== undefined
|
||||
);
|
||||
if (hasObjectsWithIncrementalId) {
|
||||
return {
|
||||
warnings: [
|
||||
{
|
||||
type: 'simple',
|
||||
message: 'The `incremental_id` field is not supported on importing.',
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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 type { SavedObject, SavedObjectsClientContract } from '@kbn/core/server';
|
||||
import type {
|
||||
CaseUserActionWithoutReferenceIds,
|
||||
AttachmentAttributesWithoutRefs,
|
||||
} from '../../../common/types/domain';
|
||||
import {
|
||||
CASE_COMMENT_SAVED_OBJECT,
|
||||
CASE_SAVED_OBJECT,
|
||||
CASE_USER_ACTION_SAVED_OBJECT,
|
||||
MAX_DOCS_PER_PAGE,
|
||||
} from '../../../common/constants';
|
||||
import { defaultSortField } from '../../common/utils';
|
||||
|
||||
export async function getAttachmentsAndUserActionsForCases(
|
||||
savedObjectsClient: SavedObjectsClientContract,
|
||||
caseIds: string[]
|
||||
): Promise<
|
||||
Array<SavedObject<AttachmentAttributesWithoutRefs | CaseUserActionWithoutReferenceIds>>
|
||||
> {
|
||||
const [attachments, userActions] = await Promise.all([
|
||||
getAssociatedObjects<AttachmentAttributesWithoutRefs>({
|
||||
savedObjectsClient,
|
||||
caseIds,
|
||||
sortField: defaultSortField,
|
||||
type: CASE_COMMENT_SAVED_OBJECT,
|
||||
}),
|
||||
getAssociatedObjects<CaseUserActionWithoutReferenceIds>({
|
||||
savedObjectsClient,
|
||||
caseIds,
|
||||
sortField: defaultSortField,
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
}),
|
||||
]);
|
||||
|
||||
return [...attachments, ...userActions];
|
||||
}
|
||||
|
||||
async function getAssociatedObjects<T>({
|
||||
savedObjectsClient,
|
||||
caseIds,
|
||||
sortField,
|
||||
type,
|
||||
}: {
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
caseIds: string[];
|
||||
sortField: string;
|
||||
type: string;
|
||||
}): Promise<Array<SavedObject<T>>> {
|
||||
const references = caseIds.map((id) => ({ type: CASE_SAVED_OBJECT, id }));
|
||||
const finder = savedObjectsClient.createPointInTimeFinder<T>({
|
||||
type,
|
||||
hasReferenceOperator: 'OR',
|
||||
hasReference: references,
|
||||
perPage: MAX_DOCS_PER_PAGE,
|
||||
sortField,
|
||||
sortOrder: 'asc',
|
||||
});
|
||||
|
||||
let result: Array<SavedObject<T>> = [];
|
||||
for await (const findResults of finder.find()) {
|
||||
result = result.concat(findResults.saved_objects);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
|
@ -14,6 +14,7 @@ import { createCaseUserActionSavedObjectType } from './user_actions';
|
|||
import { caseConnectorMappingsSavedObjectType } from './connector_mappings';
|
||||
import { casesTelemetrySavedObjectType } from './telemetry';
|
||||
import { casesRulesSavedObjectType } from './cases_rules';
|
||||
import { caseIdIncrementerSavedObjectType } from './id_incrementer';
|
||||
import type { PersistableStateAttachmentTypeRegistry } from '../attachment_framework/persistable_state_registry';
|
||||
|
||||
interface RegisterSavedObjectsArgs {
|
||||
|
@ -40,6 +41,7 @@ export const registerSavedObjects = ({
|
|||
|
||||
core.savedObjects.registerType(caseConfigureSavedObjectType);
|
||||
core.savedObjects.registerType(caseConnectorMappingsSavedObjectType);
|
||||
core.savedObjects.registerType(caseIdIncrementerSavedObjectType);
|
||||
core.savedObjects.registerType(createCaseSavedObjectType(core, logger));
|
||||
core.savedObjects.registerType(
|
||||
createCaseUserActionSavedObjectType({
|
||||
|
|
|
@ -238,6 +238,7 @@ describe('CasesService', () => {
|
|||
"username": "elastic",
|
||||
},
|
||||
},
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
@ -1694,6 +1695,7 @@ describe('CasesService', () => {
|
|||
"username": "elastic",
|
||||
},
|
||||
},
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
@ -2208,7 +2210,8 @@ describe('CasesService', () => {
|
|||
'external_service',
|
||||
'category',
|
||||
'customFields',
|
||||
'observables'
|
||||
'observables',
|
||||
'incremental_id'
|
||||
);
|
||||
|
||||
describe('getCaseIdsByAlertId', () => {
|
||||
|
@ -2308,6 +2311,7 @@ describe('CasesService', () => {
|
|||
"description": "This is a brand new case of a bad meanie defacing data",
|
||||
"duration": null,
|
||||
"external_service": null,
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
@ -2411,6 +2415,7 @@ describe('CasesService', () => {
|
|||
"username": "elastic",
|
||||
},
|
||||
},
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
@ -2505,6 +2510,7 @@ describe('CasesService', () => {
|
|||
"username": "elastic",
|
||||
},
|
||||
},
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
@ -2599,6 +2605,7 @@ describe('CasesService', () => {
|
|||
"username": "elastic",
|
||||
},
|
||||
},
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
@ -2706,6 +2713,7 @@ describe('CasesService', () => {
|
|||
"username": "elastic",
|
||||
},
|
||||
},
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
@ -2763,6 +2771,7 @@ describe('CasesService', () => {
|
|||
"username": "elastic",
|
||||
},
|
||||
},
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
@ -2866,6 +2875,7 @@ describe('CasesService', () => {
|
|||
"username": "elastic",
|
||||
},
|
||||
},
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
@ -2985,6 +2995,7 @@ describe('CasesService', () => {
|
|||
"username": "elastic",
|
||||
},
|
||||
},
|
||||
"incremental_id": undefined,
|
||||
"observables": Array [],
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
|
@ -3212,6 +3223,18 @@ describe('CasesService', () => {
|
|||
const persistedAttributes = unsecuredSavedObjectsClient.create.mock.calls[0][1];
|
||||
expect(persistedAttributes).not.toHaveProperty('foo');
|
||||
});
|
||||
|
||||
it('sets `incremental_id` field to undefined when it is passed', async () => {
|
||||
const attributes = {
|
||||
...createCasePostParams({ connector: createJiraConnector() }),
|
||||
incremental_id: 200,
|
||||
};
|
||||
|
||||
await expect(service.createCase({ id: 'a', attributes })).resolves.not.toThrow();
|
||||
|
||||
const persistedAttributes = unsecuredSavedObjectsClient.create.mock.calls[0][1];
|
||||
expect((persistedAttributes as CaseAttributes).incremental_id).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulkCreateCases', () => {
|
||||
|
@ -3253,6 +3276,22 @@ describe('CasesService', () => {
|
|||
|
||||
expect(persistedAttributes).not.toHaveProperty('foo');
|
||||
});
|
||||
|
||||
it('sets `incremental_id` field to undefined when it is passed', async () => {
|
||||
const attributes = {
|
||||
...createCasePostParams({ connector: createJiraConnector() }),
|
||||
incremental_id: 200,
|
||||
};
|
||||
|
||||
await expect(
|
||||
service.bulkCreateCases({ cases: [{ id: 'a', ...attributes }] })
|
||||
).resolves.not.toThrow();
|
||||
|
||||
const persistedAttributes =
|
||||
unsecuredSavedObjectsClient.bulkCreate.mock.calls[0][0][0].attributes;
|
||||
|
||||
expect((persistedAttributes as CaseAttributes).incremental_id).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('patch case', () => {
|
||||
|
@ -3294,6 +3333,24 @@ describe('CasesService', () => {
|
|||
const persistedAttributes = unsecuredSavedObjectsClient.update.mock.calls[0][2];
|
||||
expect(persistedAttributes).not.toHaveProperty('foo');
|
||||
});
|
||||
|
||||
it('removes incremental_id value', async () => {
|
||||
const updatedAttributes = {
|
||||
...createCasePostParams({ connector: createJiraConnector() }),
|
||||
incremental_id: 200,
|
||||
};
|
||||
|
||||
await expect(
|
||||
service.patchCase({
|
||||
caseId: '1',
|
||||
updatedAttributes,
|
||||
originalCase: {} as CaseSavedObjectTransformed,
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
|
||||
const persistedAttributes = unsecuredSavedObjectsClient.update.mock.calls[0][2];
|
||||
expect((persistedAttributes as CaseAttributes).incremental_id).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('patch cases', () => {
|
||||
|
@ -3345,6 +3402,30 @@ describe('CasesService', () => {
|
|||
|
||||
expect(persistedAttributes).not.toHaveProperty('foo');
|
||||
});
|
||||
|
||||
it('removes incremental_id values', async () => {
|
||||
const updatedAttributes = {
|
||||
...createCasePostParams({ connector: createJiraConnector() }),
|
||||
incremental_id: 200,
|
||||
};
|
||||
|
||||
await expect(
|
||||
service.patchCases({
|
||||
cases: [
|
||||
{
|
||||
caseId: '1',
|
||||
updatedAttributes,
|
||||
originalCase: {} as CaseSavedObjectTransformed,
|
||||
},
|
||||
],
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
|
||||
const persistedAttributes =
|
||||
unsecuredSavedObjectsClient.bulkUpdate.mock.calls[0][0][0].attributes;
|
||||
|
||||
expect((persistedAttributes as CaseAttributes).incremental_id).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -432,6 +432,12 @@ describe('case transforms', () => {
|
|||
'status'
|
||||
);
|
||||
});
|
||||
|
||||
it('removes the incremental_id property', () => {
|
||||
expect(transformAttributesToESModel({ incremental_id: 100 }).attributes).not.toHaveProperty(
|
||||
'incremental_id'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformSavedObjectToExternalModel', () => {
|
||||
|
@ -626,5 +632,29 @@ describe('case transforms', () => {
|
|||
transformSavedObjectToExternalModel(CaseSOResponseWithObservables).attributes.observables
|
||||
).toMatchInlineSnapshot(`Array []`);
|
||||
});
|
||||
|
||||
it('returns incremental_id when it is defined', () => {
|
||||
const CaseSOResponseWithObservables = createCaseSavedObjectResponse({
|
||||
overrides: {
|
||||
incremental_id: 100,
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
transformSavedObjectToExternalModel(CaseSOResponseWithObservables).attributes.incremental_id
|
||||
).toBe(100);
|
||||
});
|
||||
|
||||
it('returns undefined for `inceremental_id` when it is not defined', () => {
|
||||
const CaseSOResponseWithObservables = createCaseSavedObjectResponse({
|
||||
overrides: {
|
||||
incremental_id: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
transformSavedObjectToExternalModel(CaseSOResponseWithObservables).attributes.incremental_id
|
||||
).not.toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -100,6 +100,11 @@ export function transformAttributesToESModel(caseAttributes: Partial<CaseTransfo
|
|||
const { connector, external_service, severity, status, ...restAttributes } = caseAttributes;
|
||||
const { connector_id: pushConnectorId, ...restExternalService } = external_service ?? {};
|
||||
|
||||
// remove incremental_id, this one's reserved to be set by the system
|
||||
if ('incremental_id' in restAttributes) {
|
||||
delete restAttributes.incremental_id;
|
||||
}
|
||||
|
||||
const transformedConnector = {
|
||||
...(connector && {
|
||||
connector: {
|
||||
|
@ -182,6 +187,7 @@ export function transformSavedObjectToExternalModel(
|
|||
? []
|
||||
: (caseSavedObjectAttributes.customFields as CaseCustomFields);
|
||||
const observables = caseSavedObjectAttributes.observables ?? [];
|
||||
const incremental_id = caseSavedObjectAttributes.incremental_id ?? undefined;
|
||||
|
||||
return {
|
||||
...caseSavedObject,
|
||||
|
@ -194,6 +200,7 @@ export function transformSavedObjectToExternalModel(
|
|||
category,
|
||||
customFields,
|
||||
observables,
|
||||
incremental_id,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -166,6 +166,7 @@ export const basicCaseFields: CaseAttributes = {
|
|||
category: null,
|
||||
customFields: [],
|
||||
observables: [],
|
||||
incremental_id: undefined,
|
||||
};
|
||||
|
||||
export const createCaseSavedObjectResponse = ({
|
||||
|
|
|
@ -82,6 +82,7 @@
|
|||
"@kbn/code-editor-mock",
|
||||
"@kbn/monaco",
|
||||
"@kbn/code-editor",
|
||||
"@kbn/core-test-helpers-model-versions",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -1431,6 +1431,7 @@ const createCaseWithId = async ({
|
|||
total_alerts: 0,
|
||||
total_comments: 0,
|
||||
observables: [],
|
||||
incremental_id: undefined,
|
||||
},
|
||||
overwrite: false,
|
||||
});
|
||||
|
|
|
@ -104,6 +104,7 @@ export const getCaseResponse = (): Case => ({
|
|||
version: 'test-version',
|
||||
category: null,
|
||||
observables: [],
|
||||
incremental_id: undefined,
|
||||
});
|
||||
|
||||
export const getServiceNowConnector = (): Connector => ({
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue