[RAC] [Observability] [Security Solution] Use correct url to management app for observability cases, use normalized ids (#108775)

* Use correct url to management app for observability cases, use normalized ids in timelines

* Update failing test

* Load alert details data to render flyout in case detail view
This commit is contained in:
Kevin Qualters 2021-08-17 13:49:59 -04:00 committed by GitHub
parent ab637303e7
commit 87c93abf1d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 151 additions and 51 deletions

View file

@ -4,9 +4,52 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useEffect, useState } from 'react';
import { isEmpty } from 'lodash';
import { usePluginContext } from '../../../../hooks/use_plugin_context';
import { parseAlert } from '../../../../pages/alerts/parse_alert';
import { TopAlert } from '../../../../pages/alerts/';
import { useKibana } from '../../../../utils/kibana_react';
import { Ecs } from '../../../../../../cases/common';
// no alerts in observability so far
// dummy hook for now as hooks cannot be called conditionally
export const useFetchAlertData = (): [boolean, Record<string, Ecs>] => [false, {}];
export const useFetchAlertDetail = (alertId: string): [boolean, TopAlert | null] => {
const { http } = useKibana().services;
const [loading, setLoading] = useState(false);
const { observabilityRuleTypeRegistry } = usePluginContext();
const [alert, setAlert] = useState<TopAlert | null>(null);
useEffect(() => {
const abortCtrl = new AbortController();
const fetchData = async () => {
try {
setLoading(true);
const response = await http.get('/internal/rac/alerts', {
query: {
id: alertId,
},
});
if (response) {
const parsedAlert = parseAlert(observabilityRuleTypeRegistry)(response);
setAlert(parsedAlert);
setLoading(false);
}
} catch (error) {
setAlert(null);
}
};
if (!isEmpty(alertId) && loading === false && alert === null) {
fetchData();
}
return () => {
abortCtrl.abort();
};
}, [http, alertId, alert, loading, observabilityRuleTypeRegistry]);
return [loading, alert];
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useCallback, useState } from 'react';
import React, { useCallback, useState, Suspense } from 'react';
import {
casesBreadcrumbs,
getCaseDetailsUrl,
@ -15,10 +15,12 @@ import {
useFormatUrl,
} from '../../../../pages/cases/links';
import { Case } from '../../../../../../cases/common';
import { useFetchAlertData } from './helpers';
import { useFetchAlertData, useFetchAlertDetail } from './helpers';
import { useKibana } from '../../../../utils/kibana_react';
import { usePluginContext } from '../../../../hooks/use_plugin_context';
import { useBreadcrumbs } from '../../../../hooks/use_breadcrumbs';
import { observabilityAppId } from '../../../../../common';
import { LazyAlertsFlyout } from '../../../..';
interface Props {
caseId: string;
@ -41,14 +43,17 @@ export interface CaseProps extends Props {
export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) => {
const [caseTitle, setCaseTitle] = useState<string | null>(null);
const { observabilityRuleTypeRegistry } = usePluginContext();
const {
cases: casesUi,
application: { getUrlForApp, navigateToUrl },
application: { getUrlForApp, navigateToUrl, navigateToApp },
} = useKibana().services;
const allCasesLink = getCaseUrl();
const { formatUrl } = useFormatUrl();
const href = formatUrl(allCasesLink);
const [selectedAlertId, setSelectedAlertId] = useState<string>('');
useBreadcrumbs([
{ ...casesBreadcrumbs.cases, href },
...(caseTitle !== null
@ -80,41 +85,78 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) =
}),
[caseId, formatUrl, subCaseId]
);
const casesUrl = `${getUrlForApp(observabilityAppId)}/cases`;
return casesUi.getCaseView({
allCasesNavigation: {
href: allCasesHref,
onClick: async (ev) => {
if (ev != null) {
ev.preventDefault();
}
return navigateToUrl(casesUrl);
},
},
caseDetailsNavigation: {
href: caseDetailsHref,
onClick: async (ev) => {
if (ev != null) {
ev.preventDefault();
}
return navigateToUrl(`${casesUrl}${getCaseDetailsUrl({ id: caseId })}`);
},
},
caseId,
configureCasesNavigation: {
href: configureCasesHref,
onClick: async (ev) => {
if (ev != null) {
ev.preventDefault();
}
return navigateToUrl(`${casesUrl}${configureCasesLink}`);
},
},
getCaseDetailHrefWithCommentId,
onCaseDataSuccess,
subCaseId,
useFetchAlertData,
userCanCrud,
});
const handleFlyoutClose = useCallback(() => {
setSelectedAlertId('');
}, []);
const [alertLoading, alert] = useFetchAlertDetail(selectedAlertId);
return (
<>
{alertLoading === false && alert && selectedAlertId !== '' && (
<Suspense fallback={null}>
<LazyAlertsFlyout
alert={alert}
observabilityRuleTypeRegistry={observabilityRuleTypeRegistry}
onClose={handleFlyoutClose}
/>
</Suspense>
)}
{casesUi.getCaseView({
allCasesNavigation: {
href: allCasesHref,
onClick: async (ev) => {
if (ev != null) {
ev.preventDefault();
}
return navigateToUrl(casesUrl);
},
},
caseDetailsNavigation: {
href: caseDetailsHref,
onClick: async (ev) => {
if (ev != null) {
ev.preventDefault();
}
return navigateToUrl(`${casesUrl}${getCaseDetailsUrl({ id: caseId })}`);
},
},
caseId,
configureCasesNavigation: {
href: configureCasesHref,
onClick: async (ev) => {
if (ev != null) {
ev.preventDefault();
}
return navigateToUrl(`${casesUrl}${configureCasesLink}`);
},
},
ruleDetailsNavigation: {
href: (ruleId) => {
return getUrlForApp('management', {
path: `/insightsAndAlerting/triggersActions/rule/${ruleId}`,
});
},
onClick: async (ruleId, ev) => {
if (ev != null) {
ev.preventDefault();
}
return navigateToApp('management', {
path: `/insightsAndAlerting/triggersActions/rule/${ruleId}`,
});
},
},
getCaseDetailHrefWithCommentId,
onCaseDataSuccess,
subCaseId,
useFetchAlertData,
showAlertDetails: (alertId) => {
setSelectedAlertId(alertId);
},
userCanCrud,
})}
</>
);
});

View file

@ -100,7 +100,7 @@ describe('AddToCaseAction', () => {
{...props}
event={{
_id: 'test-id',
data: [],
data: [{ field: 'kibana.alert.rule.id', value: ['rule-id'] }],
ecs: {
_id: 'test-id',
_index: 'test-index',
@ -112,7 +112,7 @@ describe('AddToCaseAction', () => {
{...props}
event={{
_id: 'test-id',
data: [],
data: [{ field: 'kibana.alert.rule.id', value: ['rule-id'] }],
ecs: {
_id: 'test-id',
_index: 'test-index',

View file

@ -9,7 +9,7 @@ import React, { memo, useMemo, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { CaseStatuses, StatusAll } from '../../../../../../cases/common';
import { TimelineItem } from '../../../../../common/';
import { useAddToCase } from '../../../../hooks/use_add_to_case';
import { useAddToCase, normalizedEventFields } from '../../../../hooks/use_add_to_case';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import { TimelinesStartServices } from '../../../../types';
import { CreateCaseFlyout } from './create/flyout';
@ -38,7 +38,6 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
}) => {
const eventId = event?.ecs._id ?? '';
const eventIndex = event?.ecs._index ?? '';
const rule = event?.ecs.signal?.rule;
const dispatch = useDispatch();
const { cases } = useKibana<TimelinesStartServices>().services;
const {
@ -52,13 +51,14 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
} = useAddToCase({ event, useInsertTimeline, casePermissions, appId, onClose });
const getAllCasesSelectorModalProps = useMemo(() => {
const { ruleId, ruleName } = normalizedEventFields(event);
return {
alertData: {
alertId: eventId,
index: eventIndex ?? '',
rule: {
id: rule?.id != null ? rule.id[0] : null,
name: rule?.name != null ? rule.name[0] : null,
id: ruleId,
name: ruleName,
},
owner: appId,
},
@ -85,11 +85,10 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
goToCreateCase,
eventId,
eventIndex,
rule?.id,
rule?.name,
appId,
dispatch,
useInsertTimeline,
event,
]);
const closeCaseFlyoutOpen = useCallback(() => {

View file

@ -8,9 +8,11 @@ import { isEmpty } from 'lodash';
import { useState, useCallback, useMemo, SyntheticEvent } from 'react';
import { useLocation } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { ALERT_RULE_ID, ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils';
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
import { Case, SubCase } from '../../../cases/common';
import { TimelinesStartServices } from '../types';
import { TimelineItem } from '../../common/';
import { tGridActions } from '../store/t_grid';
import { useDeepEqualSelector } from './use_selector';
import { createUpdateSuccessToaster } from '../components/actions/timeline/cases/helpers';
@ -83,7 +85,6 @@ export const useAddToCase = ({
}: AddToCaseActionProps): UseAddToCase => {
const eventId = event?.ecs._id ?? '';
const eventIndex = event?.ecs._index ?? '';
const rule = event?.ecs.signal?.rule;
const dispatch = useDispatch();
// TODO: use correct value in standalone or integrated.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -154,6 +155,7 @@ export const useAddToCase = ({
updateCase?: (newCase: Case) => void
) => {
dispatch(tGridActions.setOpenAddToNewCase({ id: eventId, isOpen: false }));
const { ruleId, ruleName } = normalizedEventFields(event);
if (postComment) {
await postComment({
caseId: theCase.id,
@ -162,8 +164,8 @@ export const useAddToCase = ({
alertId: eventId,
index: eventIndex ?? '',
rule: {
id: rule?.id != null ? rule.id[0] : null,
name: rule?.name != null ? rule.name[0] : null,
id: ruleId,
name: ruleName,
},
owner: appId,
},
@ -171,7 +173,7 @@ export const useAddToCase = ({
});
}
},
[eventId, eventIndex, rule, appId, dispatch]
[eventId, eventIndex, appId, dispatch, event]
);
const onCaseSuccess = useCallback(
async (theCase: Case) => {
@ -239,3 +241,17 @@ export const useAddToCase = ({
isCreateCaseFlyoutOpen,
};
};
export function normalizedEventFields(event?: TimelineItem) {
const ruleId = event && event.data.find(({ field }) => field === ALERT_RULE_ID);
const ruleUuid = event && event.data.find(({ field }) => field === ALERT_RULE_UUID);
const ruleName = event && event.data.find(({ field }) => field === ALERT_RULE_NAME);
const ruleIdValue = ruleId && ruleId.value && ruleId.value[0];
const ruleUuidValue = ruleUuid && ruleUuid.value && ruleUuid.value[0];
const ruleNameValue = ruleName && ruleName.value && ruleName.value[0];
const idToUse = ruleIdValue ? ruleIdValue : ruleUuidValue;
return {
ruleId: idToUse ?? null,
ruleName: ruleNameValue ?? null,
};
}