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

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -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];
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import React, { useCallback, useState } from 'react';
import React, { useCallback, useState, Suspense } from 'react';
import {
casesBreadcrumbs,
getCaseDetailsUrl,
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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,
})}
</>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {
Expand All @@ -52,13 +51,14 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
} = useAddToCase({ event, useInsertTimeline, casePermissions, appId, onClose });

const getAllCasesSelectorModalProps = useMemo(() => {
const { ruleId, ruleName } = normalizedEventFields(event);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌🏾

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,
},
Expand All @@ -85,11 +85,10 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
goToCreateCase,
eventId,
eventIndex,
rule?.id,
rule?.name,
appId,
dispatch,
useInsertTimeline,
event,
]);

const closeCaseFlyoutOpen = useCallback(() => {
Expand Down
24 changes: 20 additions & 4 deletions x-pack/plugins/timelines/public/hooks/use_add_to_case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -162,16 +164,16 @@ 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,
},
updateCase,
});
}
},
[eventId, eventIndex, rule, appId, dispatch]
[eventId, eventIndex, appId, dispatch, event]
);
const onCaseSuccess = useCallback(
async (theCase: Case) => {
Expand Down Expand Up @@ -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];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible value could have a length of 0? perhaps consider :

Suggested change
const ruleIdValue = ruleId && ruleId.value && ruleId.value[0];
const ruleIdValue = getOr('', 'value[0]', ruleId);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It shouldn't as far as I know, if it ever blows up I'll come back to this 🤣

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,
};
}