Skip to content

Commit

Permalink
[SIEM][CASES] Configure cases: Final (#59358)
Browse files Browse the repository at this point in the history
* Create action schema

* Create createRequestHandler util function

* Add actions plugins

* Create action

* Validate actionTypeId

* [SIEM][CASE] Add find actions schema

* Create find actions route

* Create HttpRequestError

* Support http status codes

* Create check action health types

* Create check action health route

* Show field mapping

* Leave spaces between sections

* Export CasesConfiguration from servicenow action type

* Create IdSchema

* Create UpdateCaseConfiguration interface

* Create update action route

* Add constants

* Create fetchConnectors api function

* Create useConnector

* Create reducer

* Dynamic connectors

* Fix conflicts

* Create servicenow connector

* Register servicenow connector

* Add ServiceNow logo

* Create connnectors mapping

* Create validators in utils

* Use validators in connectors

* Validate URL

* Use connectors from config

* Enable triggers_aciton_ui plugin

* Show flyout

* Add closures options

* cleanup configure api

* simplify UI + add configure API

* Add mapping to flyout

* Fix error

* add all plumbing and main functionality to get configure working

* Fix naming

* Fix tests

* Show error when failed

* Remove version from query

* Disable when loading connectors

* fix config update

* Fix flyout

* fix two bugs

* Change defaults

* Disable closure options when no connector is selected

* Use default mappings from lib

* Set mapping if empty

* Reset connector to none if deleted from settings

* Change lib structure

* fix type

* review with christos

* Do not patch connector with id none

* Fix bug

* Show icon in dropdown

* Rename variable

* Show callout when connectors does not exists

* Adapt to new error handling

* Fix rebase wrong resolve

* Improve errors

* Remove async

* Fix spelling

* Refactor hooks

* Fix naming

* Better translation

* Fix bug with different action type attributes

* Fix linting errors

* Remove unnecessary comment

* Fix translation

* Normalized mapping before updating connector

* Fix type

* Memoized capitalized

* Dynamic data-subj-test variable

* Fix routes

Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
  • Loading branch information
cnasikas and XavierM committed Mar 13, 2020
1 parent ff820cb commit 70f5a30
Show file tree
Hide file tree
Showing 49 changed files with 2,079 additions and 166 deletions.
2 changes: 1 addition & 1 deletion x-pack/legacy/plugins/siem/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const siem = (kibana: any) => {
id: APP_ID,
configPrefix: 'xpack.siem',
publicDir: resolve(__dirname, 'public'),
require: ['kibana', 'elasticsearch', 'alerting', 'actions'],
require: ['kibana', 'elasticsearch', 'alerting', 'actions', 'triggers_actions_ui'],
uiExports: {
app: {
description: i18n.translate('xpack.siem.securityDescription', {
Expand Down
98 changes: 98 additions & 0 deletions x-pack/legacy/plugins/siem/public/containers/case/configure/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { isEmpty } from 'lodash/fp';
import {
CasesConnectorsFindResult,
CasesConfigurePatch,
CasesConfigureResponse,
CasesConfigureRequest,
} from '../../../../../../../plugins/case/common/api';
import { KibanaServices } from '../../../lib/kibana';

import { CASES_CONFIGURE_URL } from '../constants';
import { ApiProps } from '../types';
import { convertToCamelCase, decodeCaseConfigureResponse } from '../utils';
import { CaseConfigure, PatchConnectorProps } from './types';

export const fetchConnectors = async ({ signal }: ApiProps): Promise<CasesConnectorsFindResult> => {
const response = await KibanaServices.get().http.fetch(
`${CASES_CONFIGURE_URL}/connectors/_find`,
{
method: 'GET',
signal,
}
);

return response;
};

export const getCaseConfigure = async ({ signal }: ApiProps): Promise<CaseConfigure | null> => {
const response = await KibanaServices.get().http.fetch<CasesConfigureResponse>(
CASES_CONFIGURE_URL,
{
method: 'GET',
signal,
}
);

return !isEmpty(response)
? convertToCamelCase<CasesConfigureResponse, CaseConfigure>(
decodeCaseConfigureResponse(response)
)
: null;
};

export const postCaseConfigure = async (
caseConfiguration: CasesConfigureRequest,
signal: AbortSignal
): Promise<CaseConfigure> => {
const response = await KibanaServices.get().http.fetch<CasesConfigureResponse>(
CASES_CONFIGURE_URL,
{
method: 'POST',
body: JSON.stringify(caseConfiguration),
signal,
}
);
return convertToCamelCase<CasesConfigureResponse, CaseConfigure>(
decodeCaseConfigureResponse(response)
);
};

export const patchCaseConfigure = async (
caseConfiguration: CasesConfigurePatch,
signal: AbortSignal
): Promise<CaseConfigure> => {
const response = await KibanaServices.get().http.fetch<CasesConfigureResponse>(
CASES_CONFIGURE_URL,
{
method: 'PATCH',
body: JSON.stringify(caseConfiguration),
signal,
}
);
return convertToCamelCase<CasesConfigureResponse, CaseConfigure>(
decodeCaseConfigureResponse(response)
);
};

export const patchConfigConnector = async ({
connectorId,
config,
signal,
}: PatchConnectorProps): Promise<CasesConnectorsFindResult> => {
const response = await KibanaServices.get().http.fetch(
`${CASES_CONFIGURE_URL}/connectors/${connectorId}`,
{
method: 'PATCH',
body: JSON.stringify(config),
signal,
}
);

return response;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { ElasticUser, ApiProps } from '../types';
import {
ActionType,
CasesConnectorConfiguration,
CasesConfigurationMaps,
CaseField,
ClosureType,
Connector,
ThirdPartyField,
} from '../../../../../../../plugins/case/common/api';

export { ActionType, CasesConfigurationMaps, CaseField, ClosureType, Connector, ThirdPartyField };

export interface CasesConfigurationMapping {
source: CaseField;
target: ThirdPartyField;
actionType: ActionType;
}

export interface CaseConfigure {
createdAt: string;
createdBy: ElasticUser;
connectorId: string;
closureType: ClosureType;
updatedAt: string;
updatedBy: ElasticUser;
version: string;
}

export interface PatchConnectorProps extends ApiProps {
connectorId: string;
config: CasesConnectorConfiguration;
}

export interface CCMapsCombinedActionAttributes extends CasesConfigurationMaps {
actionType?: ActionType;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { useState, useEffect, useCallback } from 'react';
import { getCaseConfigure, patchCaseConfigure, postCaseConfigure } from './api';

import { useStateToaster, errorToToaster } from '../../../components/toasters';
import * as i18n from '../translations';
import { ClosureType } from './types';

interface PersistCaseConfigure {
connectorId: string;
closureType: ClosureType;
}

export interface ReturnUseCaseConfigure {
loading: boolean;
refetchCaseConfigure: () => void;
persistCaseConfigure: ({ connectorId, closureType }: PersistCaseConfigure) => unknown;
persistLoading: boolean;
}

interface UseCaseConfigure {
setConnectorId: (newConnectorId: string) => void;
setClosureType: (newClosureType: ClosureType) => void;
}

export const useCaseConfigure = ({
setConnectorId,
setClosureType,
}: UseCaseConfigure): ReturnUseCaseConfigure => {
const [, dispatchToaster] = useStateToaster();
const [loading, setLoading] = useState(true);
const [persistLoading, setPersistLoading] = useState(false);
const [version, setVersion] = useState('');

const refetchCaseConfigure = useCallback(() => {
let didCancel = false;
const abortCtrl = new AbortController();

const fetchCaseConfiguration = async () => {
try {
setLoading(true);
const res = await getCaseConfigure({ signal: abortCtrl.signal });
if (!didCancel) {
setLoading(false);
if (res != null) {
setConnectorId(res.connectorId);
setClosureType(res.closureType);
setVersion(res.version);
}
}
} catch (error) {
if (!didCancel) {
setLoading(false);
errorToToaster({
title: i18n.ERROR_TITLE,
error: error.body && error.body.message ? new Error(error.body.message) : error,
dispatchToaster,
});
}
}
};

fetchCaseConfiguration();

return () => {
didCancel = true;
abortCtrl.abort();
};
}, []);

const persistCaseConfigure = useCallback(
async ({ connectorId, closureType }: PersistCaseConfigure) => {
let didCancel = false;
const abortCtrl = new AbortController();
const saveCaseConfiguration = async () => {
try {
setPersistLoading(true);
const res =
version.length === 0
? await postCaseConfigure(
{ connector_id: connectorId, closure_type: closureType },
abortCtrl.signal
)
: await patchCaseConfigure(
{ connector_id: connectorId, closure_type: closureType, version },
abortCtrl.signal
);
if (!didCancel) {
setPersistLoading(false);
setConnectorId(res.connectorId);
setClosureType(res.closureType);
setVersion(res.version);
}
} catch (error) {
if (!didCancel) {
setPersistLoading(false);
errorToToaster({
title: i18n.ERROR_TITLE,
error: error.body && error.body.message ? new Error(error.body.message) : error,
dispatchToaster,
});
}
}
};
saveCaseConfiguration();
return () => {
didCancel = true;
abortCtrl.abort();
};
},
[version]
);

useEffect(() => {
refetchCaseConfigure();
}, []);

return {
loading,
refetchCaseConfigure,
persistCaseConfigure,
persistLoading,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { useState, useEffect, useCallback } from 'react';

import { useStateToaster, errorToToaster } from '../../../components/toasters';
import * as i18n from '../translations';
import { fetchConnectors, patchConfigConnector } from './api';
import { CasesConfigurationMapping, Connector } from './types';

export interface ReturnConnectors {
loading: boolean;
connectors: Connector[];
refetchConnectors: () => void;
updateConnector: (connectorId: string, mappings: CasesConfigurationMapping[]) => unknown;
}

export const useConnectors = (): ReturnConnectors => {
const [, dispatchToaster] = useStateToaster();
const [loading, setLoading] = useState(true);
const [connectors, setConnectors] = useState<Connector[]>([]);

const refetchConnectors = useCallback(() => {
let didCancel = false;
const abortCtrl = new AbortController();
const getConnectors = async () => {
try {
setLoading(true);
const res = await fetchConnectors({ signal: abortCtrl.signal });
if (!didCancel) {
setLoading(false);
setConnectors(res.data);
}
} catch (error) {
if (!didCancel) {
setLoading(false);
setConnectors([]);
errorToToaster({
title: i18n.ERROR_TITLE,
error: error.body && error.body.message ? new Error(error.body.message) : error,
dispatchToaster,
});
}
}
};
getConnectors();
return () => {
didCancel = true;
abortCtrl.abort();
};
}, []);

const updateConnector = useCallback(
(connectorId: string, mappings: CasesConfigurationMapping[]) => {
if (connectorId === 'none') {
return;
}

let didCancel = false;
const abortCtrl = new AbortController();
const update = async () => {
try {
setLoading(true);
await patchConfigConnector({
connectorId,
config: {
cases_configuration: {
mapping: mappings.map(m => ({
source: m.source,
target: m.target,
action_type: m.actionType,
})),
},
},
signal: abortCtrl.signal,
});
if (!didCancel) {
setLoading(false);
refetchConnectors();
}
} catch (error) {
if (!didCancel) {
setLoading(false);
refetchConnectors();
errorToToaster({
title: i18n.ERROR_TITLE,
error: error.body && error.body.message ? new Error(error.body.message) : error,
dispatchToaster,
});
}
}
};
update();
return () => {
didCancel = true;
abortCtrl.abort();
};
},
[]
);

useEffect(() => {
refetchConnectors();
}, []);

return {
loading,
connectors,
refetchConnectors,
updateConnector,
};
};
Loading

0 comments on commit 70f5a30

Please sign in to comment.