Skip to content

Commit

Permalink
TheHive Connector for Cases (#180931)
Browse files Browse the repository at this point in the history
## Summary

This PR implements the case support for TheHive connector.
Depends on : #180138

## Screenshots

**List of connectors**

![image](https://github.com/elastic/kibana/assets/123942796/7236eaed-d1d5-4506-80b1-c4e43c6ce36a)

**Configure thehive connector from cases**

![image](https://github.com/elastic/kibana/assets/123942796/bb4843d5-9f29-47c2-baa3-77db81a35319)


### Checklist

Delete any items that are not applicable to this PR.

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[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
- [x] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

### For maintainers

- [ ] 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: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Christos Nasikas <christos.nasikas@elastic.co>
  • Loading branch information
4 people authored Sep 10, 2024
1 parent 7cc3eae commit 32d7316
Show file tree
Hide file tree
Showing 20 changed files with 445 additions and 15 deletions.
4 changes: 2 additions & 2 deletions docs/management/action-types.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@ a| <<swimlane-action-type,{swimlane}>>

| Create an incident in {swimlane}.

a| <<thehive-action-type,{thehive}>>
a| <<thehive-action-type,TheHive>>

| Create cases and alerts in {thehive}.
| Create cases and alerts in TheHive.

a| <<tines-action-type,Tines>>

Expand Down
19 changes: 16 additions & 3 deletions docs/management/connectors/action-types/thehive.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ URL:: TheHive instance URL.
API Key:: TheHive API key for authentication.

[float]
[[TheHive-action-configuration]]
[[thehive-action-configuration]]
=== Test connectors

You can test connectors as you're creating or editing the connector in {kib}. For example:
You can test connectors for creating a case or an alert with the <<execute-connector-api,run connector API>> or
as you're creating or editing the connector in {kib}. For example:

[role="screenshot"]
image::management/connectors/images/thehive-params-case-test.png[TheHive case params test]
Expand All @@ -54,11 +55,23 @@ Description:: The details about the incident.
Severity:: Severity of the incident. This can be one of `LOW`, `MEDIUM`(default), `HIGH` or `CRITICAL`.
TLP:: Traffic Light Protocol designation for the incident. This can be one of `CLEAR`, `GREEN`, `AMBER`(default), `AMBER+STRICT` or `RED`.
Tags:: The keywords or tags about the incident.
Additional comments:: Additional information about the Case.
Additional comments:: Additional information about the Case.
Type:: Type of the Alert.
Source:: Source of the Alert.
Source Reference:: Source reference of the Alert.

[float]
[[thehive-features]]
=== Features

1. Rule base creation of alerts and cases.
2. Create case, Update case.

[NOTE]
====
* For update case, status of the case is not sync with the kibana case.
====

[float]
[[thehive-connector-networking-configuration]]
=== Connector networking configuration
Expand Down
19 changes: 19 additions & 0 deletions x-pack/plugins/cases/common/types/domain/connector/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export enum ConnectorTypes {
serviceNowITSM = '.servicenow',
serviceNowSIR = '.servicenow-sir',
swimlane = '.swimlane',
theHive = '.thehive',
}

const ConnectorCasesWebhookTypeFieldsRt = rt.strict({
Expand Down Expand Up @@ -118,6 +119,21 @@ const ConnectorSwimlaneTypeFieldsRt = rt.strict({
fields: rt.union([SwimlaneFieldsRt, rt.null]),
});

/**
* Thehive
*/

export const TheHiveFieldsRt = rt.strict({
tlp: rt.union([rt.number, rt.null]),
});

export type TheHiveFieldsType = rt.TypeOf<typeof TheHiveFieldsRt>;

const ConnectorTheHiveTypeFieldsRt = rt.strict({
type: rt.literal(ConnectorTypes.theHive),
fields: rt.union([TheHiveFieldsRt, rt.null]),
});

/**
* None connector
*/
Expand All @@ -135,6 +151,7 @@ export const ConnectorTypeFieldsRt = rt.union([
ConnectorServiceNowITSMTypeFieldsRt,
ConnectorServiceNowSIRTypeFieldsRt,
ConnectorSwimlaneTypeFieldsRt,
ConnectorTheHiveTypeFieldsRt,
]);

/**
Expand All @@ -148,6 +165,7 @@ export const CaseUserActionConnectorRt = rt.union([
rt.intersection([ConnectorServiceNowITSMTypeFieldsRt, rt.strict({ name: rt.string })]),
rt.intersection([ConnectorServiceNowSIRTypeFieldsRt, rt.strict({ name: rt.string })]),
rt.intersection([ConnectorSwimlaneTypeFieldsRt, rt.strict({ name: rt.string })]),
rt.intersection([ConnectorTheHiveTypeFieldsRt, rt.strict({ name: rt.string })]),
]);

export const CaseConnectorRt = rt.intersection([
Expand Down Expand Up @@ -205,3 +223,4 @@ export type ConnectorServiceNowITSMTypeFields = rt.TypeOf<
typeof ConnectorServiceNowITSMTypeFieldsRt
>;
export type ConnectorServiceNowSIRTypeFields = rt.TypeOf<typeof ConnectorServiceNowSIRTypeFieldsRt>;
export type ConnectorTheHiveTypeFields = rt.TypeOf<typeof ConnectorTheHiveTypeFieldsRt>;
3 changes: 3 additions & 0 deletions x-pack/plugins/cases/public/components/connectors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ import { getCaseConnector as getSwimlaneCaseConnector } from './swimlane';
import { getCaseConnector as getResilientCaseConnector } from './resilient';
import { getCaseConnector as getCasesWebhookCaseConnector } from './cases_webhook';
import { getServiceNowITSMCaseConnector, getServiceNowSIRCaseConnector } from './servicenow';
import { getCaseConnector as getTheHiveCaseConnector } from './thehive';
import type {
JiraFieldsType,
ServiceNowITSMFieldsType,
ServiceNowSIRFieldsType,
ResilientFieldsType,
SwimlaneFieldsType,
TheHiveFieldsType,
} from '../../../common/types/domain';

export * from './types';
Expand All @@ -43,6 +45,7 @@ class CaseConnectors {
this.caseConnectorsRegistry.register<ServiceNowSIRFieldsType>(getServiceNowSIRCaseConnector());
this.caseConnectorsRegistry.register<SwimlaneFieldsType>(getSwimlaneCaseConnector());
this.caseConnectorsRegistry.register<null>(getCasesWebhookCaseConnector());
this.caseConnectorsRegistry.register<TheHiveFieldsType>(getTheHiveCaseConnector());
}

registry(): CaseConnectorsRegistry {
Expand Down
10 changes: 10 additions & 0 deletions x-pack/plugins/cases/public/components/connectors/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ export const swimlaneConnector = {
isSystemAction: false,
};

export const theHiveConnector = {
id: '123',
name: 'My connector',
actionTypeId: '.thehive',
config: {},
isPreconfigured: false,
isDeprecated: false,
isSystemAction: false,
};

export const issues = [
{ id: 'personId', title: 'Person Task', key: 'personKey' },
{ id: 'womanId', title: 'Woman Task', key: 'womanKey' },
Expand Down
Original file line number Diff line number Diff line change
@@ -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 React from 'react';
import userEvent from '@testing-library/user-event';
import { screen } from '@testing-library/react';
import Fields from './case_fields';
import { theHiveConnector as connector } from '../mock';
import { MockFormWrapperComponent } from '../test_utils';
import type { AppMockRenderer } from '../../../common/mock';
import { createAppMockRenderer } from '../../../common/mock';
import { TheHiveTLP } from './types';

describe('TheHive Cases Fields', () => {
const fields = {
TLP: 1,
};

let appMockRenderer: AppMockRenderer;

beforeEach(() => {
appMockRenderer = createAppMockRenderer();
jest.clearAllMocks();
});

it('all params fields are rendered', () => {
appMockRenderer.render(
<MockFormWrapperComponent fields={fields}>
<Fields connector={connector} />
</MockFormWrapperComponent>
);

expect(screen.getByText('TLP')).toBeInTheDocument();
});

it('sets TLP correctly', async () => {
appMockRenderer.render(
<MockFormWrapperComponent fields={fields}>
<Fields connector={connector} />
</MockFormWrapperComponent>
);

userEvent.selectOptions(screen.getByTestId('tlp-field'), '4');
expect(await screen.findByTestId('tlp-field')).toHaveValue(TheHiveTLP.RED.toString());
});
});
Original file line number Diff line number Diff line change
@@ -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 React from 'react';
import { SelectField } from '@kbn/es-ui-shared-plugin/static/forms/components';
import { UseField, useFormContext } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
import type { ConnectorFieldsProps } from '../types';
import * as i18n from './translations';
import { TheHiveTLP } from './types';

const { emptyField } = fieldValidators;

const tlpOptions = Object.entries(TheHiveTLP).reduce<Array<{ text: string; value: number }>>(
(acc, [key, value]) => (typeof value === 'number' ? [...acc, { text: key, value }] : acc),
[]
);

const TheHiveFieldsComponent: React.FunctionComponent<ConnectorFieldsProps> = () => {
const form = useFormContext();

const onTLPChange: (value: string) => void = (value: string) => {
form.setFieldValue('fields.tlp', parseInt(value, 10));
};

return (
<div data-test-subj={'connector-fields-Thehive'}>
<UseField
path="fields.tlp"
component={SelectField}
config={{
label: i18n.TLP_LABEL,
validations: [
{
validator: emptyField(i18n.TLP_REQUIRED),
},
],
defaultValue: TheHiveTLP.AMBER,
}}
onChange={onTLPChange}
componentProps={{
euiFieldProps: {
'data-test-subj': 'tlp-field',
options: tlpOptions,
fullWidth: true,
},
}}
/>
</div>
);
};

TheHiveFieldsComponent.displayName = 'ThehiveFields';

// eslint-disable-next-line import/no-default-export
export { TheHiveFieldsComponent as default };
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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 React from 'react';
import { screen } from '@testing-library/react';
import FieldsPreview from './case_fields_preview';
import type { AppMockRenderer } from '../../../common/mock';
import { theHiveConnector } from '../mock';
import { createAppMockRenderer } from '../../../common/mock';
import { createQueryWithMarkup } from '../../../common/test_utils';

describe('TheHive Fields: Preview', () => {
const fields = {
tlp: 1,
};

let appMockRenderer: AppMockRenderer;

beforeEach(() => {
appMockRenderer = createAppMockRenderer();
jest.clearAllMocks();
});

it('renders all fields correctly', () => {
appMockRenderer.render(<FieldsPreview connector={theHiveConnector} fields={fields} />);

const getByText = createQueryWithMarkup(screen.getByText);
expect(getByText('TLP: GREEN')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* 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 React, { useMemo } from 'react';

import type { TheHiveFieldsType } from '../../../../common/types/domain';
import { ConnectorTypes } from '../../../../common/types/domain';
import type { ConnectorFieldsPreviewProps } from '../types';
import { ConnectorCard } from '../card';
import * as i18n from './translations';
import { TheHiveTLP } from './types';

const mapTLP = (tlpValue: number): string => {
const entry = Object.entries(TheHiveTLP).find(([_, value]) => value === tlpValue);
return entry?.[0] ?? 'AMBER';
};

const TheHiveFieldsPreviewComponent: React.FunctionComponent<
ConnectorFieldsPreviewProps<TheHiveFieldsType>
> = ({ fields, connector }) => {
const { tlp } = fields ?? {};

const listItems = useMemo(
() => [
...(tlp !== null
? [
{
title: i18n.TLP_LABEL,
description: mapTLP(tlp),
},
]
: []),
],
[tlp]
);

return (
<ConnectorCard
connectorType={ConnectorTypes.theHive}
isLoading={false}
listItems={listItems}
title={connector.name}
/>
);
};

TheHiveFieldsPreviewComponent.displayName = 'TheHiveFieldsPreview';

// eslint-disable-next-line import/no-default-export
export { TheHiveFieldsPreviewComponent as default };
20 changes: 20 additions & 0 deletions x-pack/plugins/cases/public/components/connectors/thehive/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* 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 { lazy } from 'react';

import type { CaseConnector } from '../types';
import type { TheHiveFieldsType } from '../../../../common/types/domain';
import { ConnectorTypes } from '../../../../common/types/domain';

export * from './types';

export const getCaseConnector = (): CaseConnector<TheHiveFieldsType> => ({
id: ConnectorTypes.theHive,
fieldsComponent: lazy(() => import('./case_fields')),
previewComponent: lazy(() => import('./case_fields_preview')),
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* 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 { i18n } from '@kbn/i18n';

export const TLP_LABEL = i18n.translate('xpack.cases.connectors.thehive.tlpLable', {
defaultMessage: 'TLP',
});

export const TLP_REQUIRED = i18n.translate('xpack.cases.connectors.thehive.tlpLableRequired', {
defaultMessage: 'TLP is required',
});
14 changes: 14 additions & 0 deletions x-pack/plugins/cases/public/components/connectors/thehive/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* 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 enum TheHiveTLP {
CLEAR = 0,
GREEN = 1,
AMBER = 2,
'AMBER+STRICT' = 3,
RED = 4,
}
Loading

0 comments on commit 32d7316

Please sign in to comment.