Skip to content

Commit 5fb747e

Browse files
cnasikasXavierM
andauthored
[SIEM][CASES] Configure cases: Final (#59358)
* 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>
1 parent 00de79b commit 5fb747e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+2079
-166
lines changed

x-pack/legacy/plugins/siem/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export const siem = (kibana: any) => {
4040
id: APP_ID,
4141
configPrefix: 'xpack.siem',
4242
publicDir: resolve(__dirname, 'public'),
43-
require: ['kibana', 'elasticsearch', 'alerting', 'actions'],
43+
require: ['kibana', 'elasticsearch', 'alerting', 'actions', 'triggers_actions_ui'],
4444
uiExports: {
4545
app: {
4646
description: i18n.translate('xpack.siem.securityDescription', {
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { isEmpty } from 'lodash/fp';
8+
import {
9+
CasesConnectorsFindResult,
10+
CasesConfigurePatch,
11+
CasesConfigureResponse,
12+
CasesConfigureRequest,
13+
} from '../../../../../../../plugins/case/common/api';
14+
import { KibanaServices } from '../../../lib/kibana';
15+
16+
import { CASES_CONFIGURE_URL } from '../constants';
17+
import { ApiProps } from '../types';
18+
import { convertToCamelCase, decodeCaseConfigureResponse } from '../utils';
19+
import { CaseConfigure, PatchConnectorProps } from './types';
20+
21+
export const fetchConnectors = async ({ signal }: ApiProps): Promise<CasesConnectorsFindResult> => {
22+
const response = await KibanaServices.get().http.fetch(
23+
`${CASES_CONFIGURE_URL}/connectors/_find`,
24+
{
25+
method: 'GET',
26+
signal,
27+
}
28+
);
29+
30+
return response;
31+
};
32+
33+
export const getCaseConfigure = async ({ signal }: ApiProps): Promise<CaseConfigure | null> => {
34+
const response = await KibanaServices.get().http.fetch<CasesConfigureResponse>(
35+
CASES_CONFIGURE_URL,
36+
{
37+
method: 'GET',
38+
signal,
39+
}
40+
);
41+
42+
return !isEmpty(response)
43+
? convertToCamelCase<CasesConfigureResponse, CaseConfigure>(
44+
decodeCaseConfigureResponse(response)
45+
)
46+
: null;
47+
};
48+
49+
export const postCaseConfigure = async (
50+
caseConfiguration: CasesConfigureRequest,
51+
signal: AbortSignal
52+
): Promise<CaseConfigure> => {
53+
const response = await KibanaServices.get().http.fetch<CasesConfigureResponse>(
54+
CASES_CONFIGURE_URL,
55+
{
56+
method: 'POST',
57+
body: JSON.stringify(caseConfiguration),
58+
signal,
59+
}
60+
);
61+
return convertToCamelCase<CasesConfigureResponse, CaseConfigure>(
62+
decodeCaseConfigureResponse(response)
63+
);
64+
};
65+
66+
export const patchCaseConfigure = async (
67+
caseConfiguration: CasesConfigurePatch,
68+
signal: AbortSignal
69+
): Promise<CaseConfigure> => {
70+
const response = await KibanaServices.get().http.fetch<CasesConfigureResponse>(
71+
CASES_CONFIGURE_URL,
72+
{
73+
method: 'PATCH',
74+
body: JSON.stringify(caseConfiguration),
75+
signal,
76+
}
77+
);
78+
return convertToCamelCase<CasesConfigureResponse, CaseConfigure>(
79+
decodeCaseConfigureResponse(response)
80+
);
81+
};
82+
83+
export const patchConfigConnector = async ({
84+
connectorId,
85+
config,
86+
signal,
87+
}: PatchConnectorProps): Promise<CasesConnectorsFindResult> => {
88+
const response = await KibanaServices.get().http.fetch(
89+
`${CASES_CONFIGURE_URL}/connectors/${connectorId}`,
90+
{
91+
method: 'PATCH',
92+
body: JSON.stringify(config),
93+
signal,
94+
}
95+
);
96+
97+
return response;
98+
};
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { ElasticUser, ApiProps } from '../types';
8+
import {
9+
ActionType,
10+
CasesConnectorConfiguration,
11+
CasesConfigurationMaps,
12+
CaseField,
13+
ClosureType,
14+
Connector,
15+
ThirdPartyField,
16+
} from '../../../../../../../plugins/case/common/api';
17+
18+
export { ActionType, CasesConfigurationMaps, CaseField, ClosureType, Connector, ThirdPartyField };
19+
20+
export interface CasesConfigurationMapping {
21+
source: CaseField;
22+
target: ThirdPartyField;
23+
actionType: ActionType;
24+
}
25+
26+
export interface CaseConfigure {
27+
createdAt: string;
28+
createdBy: ElasticUser;
29+
connectorId: string;
30+
closureType: ClosureType;
31+
updatedAt: string;
32+
updatedBy: ElasticUser;
33+
version: string;
34+
}
35+
36+
export interface PatchConnectorProps extends ApiProps {
37+
connectorId: string;
38+
config: CasesConnectorConfiguration;
39+
}
40+
41+
export interface CCMapsCombinedActionAttributes extends CasesConfigurationMaps {
42+
actionType?: ActionType;
43+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { useState, useEffect, useCallback } from 'react';
8+
import { getCaseConfigure, patchCaseConfigure, postCaseConfigure } from './api';
9+
10+
import { useStateToaster, errorToToaster } from '../../../components/toasters';
11+
import * as i18n from '../translations';
12+
import { ClosureType } from './types';
13+
14+
interface PersistCaseConfigure {
15+
connectorId: string;
16+
closureType: ClosureType;
17+
}
18+
19+
export interface ReturnUseCaseConfigure {
20+
loading: boolean;
21+
refetchCaseConfigure: () => void;
22+
persistCaseConfigure: ({ connectorId, closureType }: PersistCaseConfigure) => unknown;
23+
persistLoading: boolean;
24+
}
25+
26+
interface UseCaseConfigure {
27+
setConnectorId: (newConnectorId: string) => void;
28+
setClosureType: (newClosureType: ClosureType) => void;
29+
}
30+
31+
export const useCaseConfigure = ({
32+
setConnectorId,
33+
setClosureType,
34+
}: UseCaseConfigure): ReturnUseCaseConfigure => {
35+
const [, dispatchToaster] = useStateToaster();
36+
const [loading, setLoading] = useState(true);
37+
const [persistLoading, setPersistLoading] = useState(false);
38+
const [version, setVersion] = useState('');
39+
40+
const refetchCaseConfigure = useCallback(() => {
41+
let didCancel = false;
42+
const abortCtrl = new AbortController();
43+
44+
const fetchCaseConfiguration = async () => {
45+
try {
46+
setLoading(true);
47+
const res = await getCaseConfigure({ signal: abortCtrl.signal });
48+
if (!didCancel) {
49+
setLoading(false);
50+
if (res != null) {
51+
setConnectorId(res.connectorId);
52+
setClosureType(res.closureType);
53+
setVersion(res.version);
54+
}
55+
}
56+
} catch (error) {
57+
if (!didCancel) {
58+
setLoading(false);
59+
errorToToaster({
60+
title: i18n.ERROR_TITLE,
61+
error: error.body && error.body.message ? new Error(error.body.message) : error,
62+
dispatchToaster,
63+
});
64+
}
65+
}
66+
};
67+
68+
fetchCaseConfiguration();
69+
70+
return () => {
71+
didCancel = true;
72+
abortCtrl.abort();
73+
};
74+
}, []);
75+
76+
const persistCaseConfigure = useCallback(
77+
async ({ connectorId, closureType }: PersistCaseConfigure) => {
78+
let didCancel = false;
79+
const abortCtrl = new AbortController();
80+
const saveCaseConfiguration = async () => {
81+
try {
82+
setPersistLoading(true);
83+
const res =
84+
version.length === 0
85+
? await postCaseConfigure(
86+
{ connector_id: connectorId, closure_type: closureType },
87+
abortCtrl.signal
88+
)
89+
: await patchCaseConfigure(
90+
{ connector_id: connectorId, closure_type: closureType, version },
91+
abortCtrl.signal
92+
);
93+
if (!didCancel) {
94+
setPersistLoading(false);
95+
setConnectorId(res.connectorId);
96+
setClosureType(res.closureType);
97+
setVersion(res.version);
98+
}
99+
} catch (error) {
100+
if (!didCancel) {
101+
setPersistLoading(false);
102+
errorToToaster({
103+
title: i18n.ERROR_TITLE,
104+
error: error.body && error.body.message ? new Error(error.body.message) : error,
105+
dispatchToaster,
106+
});
107+
}
108+
}
109+
};
110+
saveCaseConfiguration();
111+
return () => {
112+
didCancel = true;
113+
abortCtrl.abort();
114+
};
115+
},
116+
[version]
117+
);
118+
119+
useEffect(() => {
120+
refetchCaseConfigure();
121+
}, []);
122+
123+
return {
124+
loading,
125+
refetchCaseConfigure,
126+
persistCaseConfigure,
127+
persistLoading,
128+
};
129+
};
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { useState, useEffect, useCallback } from 'react';
8+
9+
import { useStateToaster, errorToToaster } from '../../../components/toasters';
10+
import * as i18n from '../translations';
11+
import { fetchConnectors, patchConfigConnector } from './api';
12+
import { CasesConfigurationMapping, Connector } from './types';
13+
14+
export interface ReturnConnectors {
15+
loading: boolean;
16+
connectors: Connector[];
17+
refetchConnectors: () => void;
18+
updateConnector: (connectorId: string, mappings: CasesConfigurationMapping[]) => unknown;
19+
}
20+
21+
export const useConnectors = (): ReturnConnectors => {
22+
const [, dispatchToaster] = useStateToaster();
23+
const [loading, setLoading] = useState(true);
24+
const [connectors, setConnectors] = useState<Connector[]>([]);
25+
26+
const refetchConnectors = useCallback(() => {
27+
let didCancel = false;
28+
const abortCtrl = new AbortController();
29+
const getConnectors = async () => {
30+
try {
31+
setLoading(true);
32+
const res = await fetchConnectors({ signal: abortCtrl.signal });
33+
if (!didCancel) {
34+
setLoading(false);
35+
setConnectors(res.data);
36+
}
37+
} catch (error) {
38+
if (!didCancel) {
39+
setLoading(false);
40+
setConnectors([]);
41+
errorToToaster({
42+
title: i18n.ERROR_TITLE,
43+
error: error.body && error.body.message ? new Error(error.body.message) : error,
44+
dispatchToaster,
45+
});
46+
}
47+
}
48+
};
49+
getConnectors();
50+
return () => {
51+
didCancel = true;
52+
abortCtrl.abort();
53+
};
54+
}, []);
55+
56+
const updateConnector = useCallback(
57+
(connectorId: string, mappings: CasesConfigurationMapping[]) => {
58+
if (connectorId === 'none') {
59+
return;
60+
}
61+
62+
let didCancel = false;
63+
const abortCtrl = new AbortController();
64+
const update = async () => {
65+
try {
66+
setLoading(true);
67+
await patchConfigConnector({
68+
connectorId,
69+
config: {
70+
cases_configuration: {
71+
mapping: mappings.map(m => ({
72+
source: m.source,
73+
target: m.target,
74+
action_type: m.actionType,
75+
})),
76+
},
77+
},
78+
signal: abortCtrl.signal,
79+
});
80+
if (!didCancel) {
81+
setLoading(false);
82+
refetchConnectors();
83+
}
84+
} catch (error) {
85+
if (!didCancel) {
86+
setLoading(false);
87+
refetchConnectors();
88+
errorToToaster({
89+
title: i18n.ERROR_TITLE,
90+
error: error.body && error.body.message ? new Error(error.body.message) : error,
91+
dispatchToaster,
92+
});
93+
}
94+
}
95+
};
96+
update();
97+
return () => {
98+
didCancel = true;
99+
abortCtrl.abort();
100+
};
101+
},
102+
[]
103+
);
104+
105+
useEffect(() => {
106+
refetchConnectors();
107+
}, []);
108+
109+
return {
110+
loading,
111+
connectors,
112+
refetchConnectors,
113+
updateConnector,
114+
};
115+
};

0 commit comments

Comments
 (0)