Skip to content

Commit

Permalink
[RAM] add Alert fly-out from security solution (#133691)
Browse files Browse the repository at this point in the history
* add security solution flyout

* fix types

* performance on browserfield

* cleanup

* miss to rmv something

* fix types + unit test

* fix tets

* review Jiawei

* review II + cast a type

* eui review

* formatting

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
XavierM and kibanamachine authored Jun 9, 2022
1 parent fa4d102 commit 3cd5257
Show file tree
Hide file tree
Showing 22 changed files with 936 additions and 276 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default function AlertsFlyoutBody(props: FlyoutProps) {
const { observabilityRuleTypeRegistry } = usePluginContext();
const alert = props.alert.start
? props.alert
: parseAlert(observabilityRuleTypeRegistry)(props.alert);
: parseAlert(observabilityRuleTypeRegistry)(props.alert as unknown as Record<string, unknown>);
const { services } = useKibana();
const { http } = services;
const dateFormat = useUiSetting<string>('dateFormat');
Expand Down
7 changes: 6 additions & 1 deletion x-pack/plugins/rule_registry/common/search_strategy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,12 @@ type DotNestedKeys<T, D extends number = 10> = [D] extends [never]
: never;

export type EcsFields = DotNestedKeys<Omit<Ecs, 'ecs'>>;

export interface BasicFields {
_id: string;
_index: string;
}
export type EcsFieldsResponse = {
[Property in EcsFields]: string[];
};
} & BasicFields;
export type RuleRegistrySearchResponse = IEsSearchResponse<EcsFieldsResponse>;
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@
*/

import { Storage } from '@kbn/kibana-utils-plugin/public';
import { AlertsTableConfigurationRegistryContract } from '@kbn/triggers-actions-ui-plugin/public';
import type {
AlertsTableConfigurationRegistryContract,
GetRenderCellValue,
} from '@kbn/triggers-actions-ui-plugin/public';

import { APP_ID } from '../../../../common/constants';
import { getTimelinesInStorageByIds } from '../../../timelines/containers/local_storage';
import { TimelineId } from '../../../../common/types';
import { columns } from '../../../detections/configurations/security_solution_detections';
import { useRenderCellValue } from '../../../detections/configurations/security_solution_detections/render_cell_value';
import { useToGetInternalFlyout } from '../../../timelines/components/side_panel/event_details/flyout';

const registerAlertsTableConfiguration = (
registry: AlertsTableConfigurationRegistryContract,
Expand All @@ -21,10 +26,17 @@ const registerAlertsTableConfiguration = (
return;
}
const timelineStorage = getTimelinesInStorageByIds(storage, [TimelineId.detectionsPage]);
const alertColumns = timelineStorage?.[TimelineId.detectionsPage]?.columns ?? columns;
const columnsFormStorage = timelineStorage?.[TimelineId.detectionsPage]?.columns ?? [];
const alertColumns = columnsFormStorage.length ? columnsFormStorage : columns;

registry.register({
id: APP_ID,
columns: alertColumns,
getRenderCellValue: useRenderCellValue as GetRenderCellValue,
useInternalFlyout: () => {
const { header, body, footer } = useToGetInternalFlyout();
return { header, body, footer };
},
});
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

import { EuiDataGridCellValueElementProps } from '@elastic/eui';
import React from 'react';
import { TimelineId } from '../../../../common/types';
import { useSourcererDataView } from '../../../common/containers/sourcerer';
import { SourcererScopeName } from '../../../common/store/sourcerer/model';

import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering';
import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
Expand Down Expand Up @@ -59,3 +62,62 @@ export const RenderCellValue: React.FC<
truncate={truncate}
/>
);

export const useRenderCellValue = ({
setFlyoutAlert,
}: {
setFlyoutAlert?: (data: never) => void;
}) => {
const { browserFields } = useSourcererDataView(SourcererScopeName.detections);
return ({
columnId,
colIndex,
data,
ecsData,
eventId,
globalFilters,
header,
isDetails = false,
isDraggable = false,
isExpandable,
isExpanded,
linkValues,
rowIndex,
rowRenderers,
setCellProps,
truncate = true,
}: CellValueElementProps) => {
const splitColumnId = columnId.split('.');
let myHeader = header ?? { id: columnId };
if (splitColumnId.length > 1 && browserFields[splitColumnId[0]]) {
const attr = (browserFields[splitColumnId[0]].fields ?? {})[columnId] ?? {};
myHeader = { ...myHeader, ...attr };
} else if (splitColumnId.length === 1) {
const attr = (browserFields.base.fields ?? {})[columnId] ?? {};
myHeader = { ...myHeader, ...attr };
}

return (
<DefaultCellRenderer
browserFields={browserFields}
columnId={columnId}
data={data}
ecsData={ecsData}
eventId={eventId}
globalFilters={globalFilters}
header={myHeader}
isDetails={isDetails}
isDraggable={isDraggable}
isExpandable={isExpandable}
isExpanded={isExpanded}
linkValues={linkValues}
rowIndex={rowIndex}
colIndex={colIndex}
rowRenderers={rowRenderers}
setCellProps={setCellProps}
timelineId={TimelineId.casePage}
truncate={truncate}
/>
);
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { EuiButtonEmpty, EuiText, EuiTitle } from '@elastic/eui';
import React from 'react';
import {
ISOLATE_HOST,
UNISOLATE_HOST,
} from '../../../../../detections/components/host_isolation/translations';
import { ALERT_DETAILS } from '../translations';

const BackToAlertDetailsLinkComponent = ({
showAlertDetails,
isolateAction,
}: {
showAlertDetails: () => void;
isolateAction: 'isolateHost' | 'unisolateHost';
}) => {
return (
<>
<EuiButtonEmpty iconType="arrowLeft" iconSide="left" flush="left" onClick={showAlertDetails}>
<EuiText size="xs">
<p>{ALERT_DETAILS}</p>
</EuiText>
</EuiButtonEmpty>
<EuiTitle>
<h2>{isolateAction === 'isolateHost' ? ISOLATE_HOST : UNISOLATE_HOST}</h2>
</EuiTitle>
</>
);
};

const BackToAlertDetailsLink = React.memo(BackToAlertDetailsLinkComponent);

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

import { EuiFlyoutBody } from '@elastic/eui';
import styled from 'styled-components';
import React from 'react';
import { EndpointIsolateSuccess } from '../../../../../common/components/endpoint/host_isolation';
import { HostIsolationPanel } from '../../../../../detections/components/host_isolation';
import { BrowserFields, TimelineEventsDetailsItem } from '../../../../../../common/search_strategy';
import { ExpandableEvent, HandleOnEventClosed } from '../expandable_event';
import { HostRisk } from '../../../../../risk_score/containers';

const StyledEuiFlyoutBody = styled(EuiFlyoutBody)`
.euiFlyoutBody__overflow {
display: flex;
flex: 1;
overflow: hidden;
.euiFlyoutBody__overflowContent {
flex: 1;
overflow: hidden;
padding: ${({ theme }) => `0 ${theme.eui.paddingSizes.m} ${theme.eui.paddingSizes.m}`};
}
}
`;

interface FlyoutBodyComponentProps {
alertId: string;
browserFields: BrowserFields;
detailsData: TimelineEventsDetailsItem[] | null;
event: { eventId: string; indexName: string };
handleIsolationActionSuccess: () => void;
handleOnEventClosed: HandleOnEventClosed;
hostName: string;
hostRisk: HostRisk | null;
isAlert: boolean;
isDraggable?: boolean;
isReadOnly?: boolean;
isolateAction: 'isolateHost' | 'unisolateHost';
isIsolateActionSuccessBannerVisible: boolean;
isHostIsolationPanelOpen: boolean;
loading: boolean;
rawEventData: object | undefined;
showAlertDetails: () => void;
timelineId: string;
}

const FlyoutBodyComponent = ({
alertId,
browserFields,
detailsData,
event,
handleIsolationActionSuccess,
handleOnEventClosed,
hostName,
hostRisk,
isAlert,
isDraggable,
isReadOnly,
isolateAction,
isHostIsolationPanelOpen,
isIsolateActionSuccessBannerVisible,
loading,
rawEventData,
showAlertDetails,
timelineId,
}: FlyoutBodyComponentProps) => {
return (
<StyledEuiFlyoutBody>
{isIsolateActionSuccessBannerVisible && (
<EndpointIsolateSuccess
hostName={hostName}
alertId={alertId}
isolateAction={isolateAction}
/>
)}
{isHostIsolationPanelOpen ? (
<HostIsolationPanel
details={detailsData}
cancelCallback={showAlertDetails}
successCallback={handleIsolationActionSuccess}
isolateAction={isolateAction}
/>
) : (
<ExpandableEvent
browserFields={browserFields}
detailsData={detailsData}
event={event}
isAlert={isAlert}
isDraggable={isDraggable}
loading={loading}
rawEventData={rawEventData}
timelineId={timelineId}
timelineTabType="flyout"
hostRisk={hostRisk}
handleOnEventClosed={handleOnEventClosed}
isReadOnly={isReadOnly}
/>
)}
</StyledEuiFlyoutBody>
);
};

const FlyoutBody = React.memo(FlyoutBodyComponent);

export { FlyoutBody };
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
*/
import React from 'react';
import { render } from '@testing-library/react';
import { EventDetailsFooter } from './footer';
import '../../../../common/mock/match_media';
import { TestProviders } from '../../../../common/mock';
import { TimelineId } from '../../../../../common/types/timeline';
import { Ecs } from '../../../../../common/ecs';
import { mockAlertDetailsData } from '../../../../common/components/event_details/__mocks__';
import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy';
import { KibanaServices, useKibana } from '../../../../common/lib/kibana';
import { FlyoutFooter } from './footer';
import '../../../../../common/mock/match_media';
import { TestProviders } from '../../../../../common/mock';
import { TimelineId } from '../../../../../../common/types/timeline';
import { Ecs } from '../../../../../../common/ecs';
import { mockAlertDetailsData } from '../../../../../common/components/event_details/__mocks__';
import type { TimelineEventsDetailsItem } from '../../../../../../common/search_strategy';
import { KibanaServices, useKibana } from '../../../../../common/lib/kibana';
import { coreMock } from '@kbn/core/public/mocks';
import { mockCasesContract } from '@kbn/cases-plugin/public/mocks';

Expand All @@ -38,14 +38,14 @@ const mockAlertDetailsDataWithIsObject = mockAlertDetailsData.map((detail) => {
};
}) as TimelineEventsDetailsItem[];

jest.mock('../../../../../common/endpoint/service/host_isolation/utils', () => {
jest.mock('../../../../../../common/endpoint/service/host_isolation/utils', () => {
return {
isIsolationSupported: jest.fn().mockReturnValue(true),
};
});

jest.mock(
'../../../../detections/containers/detection_engine/alerts/use_host_isolation_status',
'../../../../../detections/containers/detection_engine/alerts/use_host_isolation_status',
() => {
return {
useHostIsolationStatus: jest.fn().mockReturnValue({
Expand All @@ -57,30 +57,30 @@ jest.mock(
}
);

jest.mock('../../../../common/hooks/use_experimental_features', () => ({
jest.mock('../../../../../common/hooks/use_experimental_features', () => ({
useIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(true),
}));

jest.mock('../../../../detections/components/user_info', () => ({
jest.mock('../../../../../detections/components/user_info', () => ({
useUserData: jest.fn().mockReturnValue([{ canUserCRUD: true, hasIndexWrite: true }]),
}));
jest.mock('../../../../common/lib/kibana');
jest.mock('../../../../../common/lib/kibana');
jest.mock(
'../../../../detections/containers/detection_engine/alerts/use_alerts_privileges',
'../../../../../detections/containers/detection_engine/alerts/use_alerts_privileges',
() => ({
useAlertsPrivileges: jest.fn().mockReturnValue({ hasIndexWrite: true, hasKibanaCRUD: true }),
})
);
jest.mock('../../../../cases/components/use_insert_timeline');
jest.mock('../../../../../cases/components/use_insert_timeline');

jest.mock('../../../../common/utils/endpoint_alert_check', () => {
jest.mock('../../../../../common/utils/endpoint_alert_check', () => {
return {
isAlertFromEndpointAlert: jest.fn().mockReturnValue(true),
isAlertFromEndpointEvent: jest.fn().mockReturnValue(true),
};
});
jest.mock(
'../../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline',
'../../../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline',
() => {
return {
useInvestigateInTimeline: jest.fn().mockReturnValue({
Expand All @@ -90,7 +90,7 @@ jest.mock(
};
}
);
jest.mock('../../../../detections/components/alerts_table/actions');
jest.mock('../../../../../detections/components/alerts_table/actions');

const defaultProps = {
timelineId: TimelineId.test,
Expand Down Expand Up @@ -126,7 +126,7 @@ describe('event details footer component', () => {
test('it renders the take action dropdown', () => {
const wrapper = render(
<TestProviders>
<EventDetailsFooter {...defaultProps} />
<FlyoutFooter {...defaultProps} />
</TestProviders>
);
expect(wrapper.getByTestId('take-action-dropdown-btn')).toBeTruthy();
Expand Down
Loading

0 comments on commit 3cd5257

Please sign in to comment.