Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SIEM][CASES] Configure cases: Final #59358

Merged
merged 81 commits into from
Mar 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
eda3276
Create action schema
cnasikas Feb 21, 2020
9e55a10
Create createRequestHandler util function
cnasikas Feb 21, 2020
7111443
Add actions plugins
cnasikas Feb 21, 2020
9dc7db2
Create action
cnasikas Feb 21, 2020
e82835b
Validate actionTypeId
cnasikas Feb 21, 2020
8615dd1
[SIEM][CASE] Add find actions schema
cnasikas Feb 24, 2020
7ac0768
Create find actions route
cnasikas Feb 24, 2020
6973173
Create HttpRequestError
cnasikas Feb 26, 2020
256811a
Support http status codes
cnasikas Feb 26, 2020
20b49f7
Create check action health types
cnasikas Feb 26, 2020
19c6b2f
Create check action health route
cnasikas Feb 26, 2020
82fa7e8
Show field mapping
cnasikas Mar 2, 2020
69e1c85
Leave spaces between sections
cnasikas Mar 2, 2020
fb6d111
Export CasesConfiguration from servicenow action type
cnasikas Mar 4, 2020
22cf1e1
Create IdSchema
cnasikas Mar 4, 2020
2863aaf
Create UpdateCaseConfiguration interface
cnasikas Mar 4, 2020
f543315
Create update action route
cnasikas Mar 4, 2020
3aa35b2
Add constants
cnasikas Mar 4, 2020
aef1723
Create fetchConnectors api function
cnasikas Mar 4, 2020
fd5f46d
Create useConnector
cnasikas Mar 4, 2020
b0b715e
Create reducer
cnasikas Mar 4, 2020
ba06828
Dynamic connectors
cnasikas Mar 4, 2020
b2d07dd
Fix conflicts
cnasikas Mar 6, 2020
3aba277
Create servicenow connector
cnasikas Feb 26, 2020
61d60ea
Register servicenow connector
cnasikas Feb 26, 2020
b377505
Add ServiceNow logo
cnasikas Feb 27, 2020
9a6c579
Create connnectors mapping
cnasikas Feb 27, 2020
9d0d31d
Create validators in utils
cnasikas Feb 27, 2020
4fe368c
Use validators in connectors
cnasikas Feb 27, 2020
134505f
Validate URL
cnasikas Feb 27, 2020
ba45d2a
Use connectors from config
cnasikas Feb 27, 2020
82a4cb4
Enable triggers_aciton_ui plugin
cnasikas Mar 6, 2020
46d7258
Show flyout
cnasikas Mar 6, 2020
ab987f2
Add closures options
cnasikas Mar 6, 2020
086bac8
cleanup configure api
XavierM Mar 6, 2020
06d6dee
simplify UI + add configure API
XavierM Mar 6, 2020
6178e60
Add mapping to flyout
cnasikas Mar 6, 2020
27b3c1f
Fix error
cnasikas Mar 6, 2020
8b7a55c
add all plumbing and main functionality to get configure working
XavierM Mar 7, 2020
15f6028
fix config update
XavierM Mar 7, 2020
a83f690
Fix naming
cnasikas Mar 7, 2020
df2dd67
Fix tests
cnasikas Mar 7, 2020
cf218a5
Show error when failed
cnasikas Mar 7, 2020
377ec74
Remove version from query
cnasikas Mar 7, 2020
ded01e0
Disable when loading connectors
cnasikas Mar 7, 2020
10da7c8
Fix flyout
cnasikas Mar 7, 2020
6bbc64e
fix two bugs
XavierM Mar 7, 2020
c99a950
Change defaults
cnasikas Mar 7, 2020
9159071
Disable closure options when no connector is selected
cnasikas Mar 7, 2020
484629b
Use default mappings from lib
cnasikas Mar 8, 2020
80eb857
Set mapping if empty
cnasikas Mar 8, 2020
d61c78f
Reset connector to none if deleted from settings
cnasikas Mar 8, 2020
137e349
Change lib structure
cnasikas Mar 9, 2020
4e44ce7
fix type
XavierM Mar 9, 2020
6380f83
review with christos
XavierM Mar 9, 2020
b7630e2
Do not patch connector with id none
cnasikas Mar 10, 2020
c71d144
Fix bug
cnasikas Mar 10, 2020
3325ee6
Show icon in dropdown
cnasikas Mar 10, 2020
1845c9a
Rename variable
cnasikas Mar 10, 2020
18b5ff5
Show callout when connectors does not exists
cnasikas Mar 10, 2020
cf4a17b
Adapt to new error handling
cnasikas Mar 10, 2020
4018945
Fix rebase wrong resolve
cnasikas Mar 11, 2020
6e1fff9
Improve errors
cnasikas Mar 11, 2020
e23e103
Remove async
cnasikas Mar 11, 2020
7441479
Fix spelling
cnasikas Mar 11, 2020
ec4c002
Refactor hooks
cnasikas Mar 12, 2020
ffa4876
Fix naming
cnasikas Mar 12, 2020
8c24037
Fix functional tests
cnasikas Mar 12, 2020
2fe8b40
Better translation
cnasikas Mar 12, 2020
2553897
Fix bug with different action type attributes
cnasikas Mar 12, 2020
c10d1f4
Fix linting errors
cnasikas Mar 12, 2020
5238e66
Remove unnecessary comment
cnasikas Mar 12, 2020
0f3760d
Fix translation
cnasikas Mar 12, 2020
d199d26
Normalized mapping before updating connector
cnasikas Mar 12, 2020
f2bff31
Fix type
cnasikas Mar 12, 2020
03cb51b
Merge branch 'master' into configure_cases_all
cnasikas Mar 13, 2020
f45a343
Revert test config
cnasikas Mar 13, 2020
e4819b6
Memoized capitalized
cnasikas Mar 13, 2020
307dedb
Dynamic data-subj-test variable
cnasikas Mar 13, 2020
742a8ea
Fix routes
cnasikas Mar 13, 2020
7af5d96
Merge branch 'master' into configure_cases_all
cnasikas Mar 13, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
cnasikas marked this conversation as resolved.
Show resolved Hide resolved

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