Skip to content

Commit

Permalink
Merge branch 'case_action' of github.com:elastic/kibana into case_action
Browse files Browse the repository at this point in the history
  • Loading branch information
cnasikas committed Nov 8, 2023
2 parents a035154 + d5bbd8e commit bc79e00
Show file tree
Hide file tree
Showing 20 changed files with 752 additions and 0 deletions.
28 changes: 28 additions & 0 deletions packages/kbn-check-mappings-update-cli/current_mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -1465,6 +1465,34 @@
"dynamic": false,
"properties": {}
},
"cases-oracle": {
"dynamic": false,
"properties": {
"cases": {
"properties": {
"id": {
"type": "keyword"
}
}
},
"counter": {
"type": "unsigned_long"
},
"createdAt": {
"type": "date"
},
"rules": {
"properties": {
"id": {
"type": "keyword"
}
}
},
"updatedAt": {
"type": "date"
}
}
},
"infrastructure-monitoring-log-view": {
"dynamic": false,
"properties": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"cases-comments": "5cb0a421588831c2a950e50f486048b8aabbae25",
"cases-configure": "44ed7b8e0f44df39516b8870589b89e32224d2bf",
"cases-connector-mappings": "f9d1ac57e484e69506c36a8051e4d61f4a8cfd25",
"cases-oracle": "afd99cd22b5551ac336b7c0f30f9ee31aa2b9f20",
"cases-telemetry": "f219eb7e26772884342487fc9602cfea07b3cedc",
"cases-user-actions": "483f10db9b3bd1617948d7032a98b7791bf87414",
"config": "179b3e2bc672626aafce3cf92093a113f456af38",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const previouslyRegisteredTypes = [
'cases-comments',
'cases-configure',
'cases-connector-mappings',
'cases-oracle',
'cases-sub-case',
'cases-user-actions',
'cases-telemetry',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ describe('split .kibana index into multiple system indices', () => {
"cases-comments",
"cases-configure",
"cases-connector-mappings",
"cases-oracle",
"cases-telemetry",
"cases-user-actions",
"config",
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/cases/common/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT = 'cases-connector-mappings' a
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_ORACLE_SAVED_OBJECT = 'cases-oracle' as const;

/**
* If more values are added here please also add them here: x-pack/test/cases_api_integration/common/plugins
Expand Down
42 changes: 42 additions & 0 deletions x-pack/plugins/cases/server/connectors/cases/cases_connector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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 { ServiceParams } from '@kbn/actions-plugin/server';
import { SubActionConnector } from '@kbn/actions-plugin/server';
import { CASES_CONNECTOR_SUB_ACTION } from './constants';
import type { CasesConnectorConfig, CasesConnectorSecrets } from './types';
import { CasesConnectorParamsSchema } from './schema';

export class CasesConnector extends SubActionConnector<
CasesConnectorConfig,
CasesConnectorSecrets
> {
constructor(params: ServiceParams<CasesConnectorConfig, CasesConnectorSecrets>) {
super(params);

this.registerSubActions();
}

private registerSubActions() {
this.registerSubAction({
name: CASES_CONNECTOR_SUB_ACTION.RUN,
method: 'run',
schema: CasesConnectorParamsSchema,
});
}

/**
* Method is not needed for the Case Connector.
* The function throws an error as a reminder to
* implement it if we need it in the future.
*/
protected getResponseErrorMessage(): string {
throw new Error('Method not implemented.');
}

public async run() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
/*
* 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 { createHash } from 'node:crypto';
import stringify from 'json-stable-stringify';
import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks';
import { loggerMock } from '@kbn/logging-mocks';

import { CasesOracleService } from './cases_oracle_service';
import { CASE_ORACLE_SAVED_OBJECT } from '../../../common/constants';
import { isEmpty, set } from 'lodash';

describe('CasesOracleService', () => {
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const mockLogger = loggerMock.create();

let service: CasesOracleService;

beforeEach(() => {
jest.resetAllMocks();
service = new CasesOracleService({ unsecuredSavedObjectsClient, log: mockLogger });
});

describe('getRecordId', () => {
it('return the record ID correctly', async () => {
const ruleId = 'test-rule-id';
const spaceId = 'default';
const owner = 'cases';
const grouping = { 'host.ip': '0.0.0.1' };

const payload = `${ruleId}:${spaceId}:${owner}:${stringify(grouping)}`;
const hash = createHash('sha256');

hash.update(payload);

const hex = hash.digest('hex');

expect(service.getRecordId({ ruleId, spaceId, owner, grouping })).toEqual(hex);
});

it('sorts the grouping definition correctly', async () => {
const ruleId = 'test-rule-id';
const spaceId = 'default';
const owner = 'cases';
const grouping = { 'host.ip': '0.0.0.1', 'agent.id': '8a4f500d' };
const sortedGrouping = { 'agent.id': '8a4f500d', 'host.ip': '0.0.0.1' };

const payload = `${ruleId}:${spaceId}:${owner}:${stringify(sortedGrouping)}`;
const hash = createHash('sha256');

hash.update(payload);

const hex = hash.digest('hex');

expect(service.getRecordId({ ruleId, spaceId, owner, grouping })).toEqual(hex);
});

it('return the record ID correctly without grouping', async () => {
const ruleId = 'test-rule-id';
const spaceId = 'default';
const owner = 'cases';

const payload = `${ruleId}:${spaceId}:${owner}`;
const hash = createHash('sha256');

hash.update(payload);

const hex = hash.digest('hex');

expect(service.getRecordId({ ruleId, spaceId, owner })).toEqual(hex);
});

it('return the record ID correctly with empty grouping', async () => {
const ruleId = 'test-rule-id';
const spaceId = 'default';
const owner = 'cases';
const grouping = {};

const payload = `${ruleId}:${spaceId}:${owner}:${stringify(grouping)}`;
const hash = createHash('sha256');

hash.update(payload);

const hex = hash.digest('hex');

expect(service.getRecordId({ ruleId, spaceId, owner, grouping })).toEqual(hex);
});

it('return the record ID correctly without rule', async () => {
const spaceId = 'default';
const owner = 'cases';
const grouping = { 'host.ip': '0.0.0.1' };

const payload = `${spaceId}:${owner}:${stringify(grouping)}`;
const hash = createHash('sha256');

hash.update(payload);

const hex = hash.digest('hex');

expect(service.getRecordId({ spaceId, owner, grouping })).toEqual(hex);
});

it('throws an error when the ruleId and the grouping is missing', async () => {
const spaceId = 'default';
const owner = 'cases';

// @ts-expect-error: ruleId and grouping are omitted for testing
expect(() => service.getRecordId({ spaceId, owner })).toThrowErrorMatchingInlineSnapshot(
`"ruleID or grouping is required"`
);
});

it.each(['ruleId', 'spaceId', 'owner'])(
'return the record ID correctly with empty string for %s',
async (key) => {
const getPayloadValue = (value: string) => (isEmpty(value) ? '' : `${value}:`);

const params = {
ruleId: 'test-rule-id',
spaceId: 'default',
owner: 'cases',
};

const grouping = { 'host.ip': '0.0.0.1' };

set(params, key, '');

const payload = `${getPayloadValue(params.ruleId)}${getPayloadValue(
params.spaceId
)}${getPayloadValue(params.owner)}${stringify(grouping)}`;

const hash = createHash('sha256');

hash.update(payload);

const hex = hash.digest('hex');

expect(service.getRecordId({ ...params, grouping })).toEqual(hex);
}
);
});

describe('getRecord', () => {
const cases = [{ id: 'test-case-id' }];
const rules = [{ id: 'test-rule-id' }];
const grouping = { 'host.ip': '0.0.0.1', 'agent.id': '8a4f500d' };

const oracleSO = {
id: 'so-id',
version: 'so-version',
attributes: {
counter: 1,
cases,
rules,
grouping,
createdAt: '2023-10-10T10:23:42.769Z',
updatedAt: '2023-10-10T10:23:42.769Z',
},
type: CASE_ORACLE_SAVED_OBJECT,
references: [],
};

beforeEach(() => {
unsecuredSavedObjectsClient.get.mockResolvedValue(oracleSO);
});

it('gets a record correctly', async () => {
const record = await service.getRecord('so-id');

expect(record).toEqual({ ...oracleSO.attributes, id: 'so-id', version: 'so-version' });
});
});

describe('createRecord', () => {
const cases = [{ id: 'test-case-id' }];
const rules = [{ id: 'test-rule-id' }];
const grouping = { 'host.ip': '0.0.0.1', 'agent.id': '8a4f500d' };

const oracleSO = {
id: 'so-id',
version: 'so-version',
attributes: {
counter: 1,
cases,
rules,
grouping,
createdAt: '2023-10-10T10:23:42.769Z',
updatedAt: '2023-10-10T10:23:42.769Z',
},
type: CASE_ORACLE_SAVED_OBJECT,
references: [],
};

beforeEach(() => {
unsecuredSavedObjectsClient.create.mockResolvedValue(oracleSO);
});

it('creates a record correctly', async () => {
const record = await service.createRecord('so-id', { cases, rules, grouping });

expect(record).toEqual({ ...oracleSO.attributes, id: 'so-id', version: 'so-version' });
});

it('calls the unsecuredSavedObjectsClient.create method correctly', async () => {
const id = 'so-id';

await service.createRecord(id, { cases, rules, grouping });

expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith(
'cases-oracle',
{
cases,
counter: 1,
createdAt: expect.anything(),
rules,
grouping,
updatedAt: null,
},
{ id }
);
});
});

describe('increaseCounter', () => {
const cases = [{ id: 'test-case-id' }];
const rules = [{ id: 'test-rule-id' }];
const grouping = { 'host.ip': '0.0.0.1', 'agent.id': '8a4f500d' };

const oracleSO = {
id: 'so-id',
version: 'so-version',
attributes: {
counter: 1,
cases,
rules,
grouping,
createdAt: '2023-10-10T10:23:42.769Z',
updatedAt: '2023-10-10T10:23:42.769Z',
},
type: CASE_ORACLE_SAVED_OBJECT,
references: [],
};

const oracleSOWithIncreasedCounter = {
...oracleSO,
attributes: { ...oracleSO.attributes, counter: 2 },
};

beforeEach(() => {
unsecuredSavedObjectsClient.get.mockResolvedValue(oracleSO);
unsecuredSavedObjectsClient.update.mockResolvedValue(oracleSOWithIncreasedCounter);
});

it('increases the counter correctly', async () => {
const record = await service.increaseCounter('so-id');

expect(record).toEqual({
...oracleSO.attributes,
id: 'so-id',
version: 'so-version',
counter: 2,
});
});

it('calls the unsecuredSavedObjectsClient.update method correctly', async () => {
await service.increaseCounter('so-id');

expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith(
'cases-oracle',
'so-id',
{
counter: 2,
},
{ version: 'so-version' }
);
});
});
});
Loading

0 comments on commit bc79e00

Please sign in to comment.