Skip to content

Commit

Permalink
[Security team: AWP] Session view: Alert details tab (#127500)
Browse files Browse the repository at this point in the history
* alerts tab work. list view done

* View mode toggle + group view implemented

* tests written

* clean up

* addressed @opauloh comments

* fixed weird bug due to importing assests from a test into its component

* empty state added for alerts tab

* react-query caching keys updated to include sessionEntityId

* rule_registry added as a dependency in order to use AlertsClient in alerts_route.ts

* fixed build/test errors due to merge. events route now orders by process.start then @timestamp

* plumbing for the alert details tie in done.

* removed rule_registry ecs mappings. kqualters PR will add this.

* alerts index merge conflict fix

Co-authored-by: mitodrummer <karlgodard@elastic.co>
  • Loading branch information
mitodrummer and mitodrummer authored Mar 23, 2022
1 parent d48b82a commit c55bb91
Show file tree
Hide file tree
Showing 32 changed files with 1,555 additions and 102 deletions.
3 changes: 2 additions & 1 deletion x-pack/plugins/session_view/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
*/

export const PROCESS_EVENTS_ROUTE = '/internal/session_view/process_events_route';
export const ALERTS_ROUTE = '/internal/session_view/alerts_route';
export const ALERT_STATUS_ROUTE = '/internal/session_view/alert_status_route';
export const SESSION_ENTRY_LEADERS_ROUTE = '/internal/session_view/session_entry_leaders_route';
export const PROCESS_EVENTS_INDEX = 'logs-endpoint.events.process-default';
export const ALERTS_INDEX = '.siem-signals-default';
export const ALERTS_INDEX = '.alerts-security.alerts-default'; // TODO: changes to remove this and use AlertsClient instead to get indices.
export const ENTRY_SESSION_ENTITY_ID_PROPERTY = 'process.entry_leader.entity_id';
export const ALERT_UUID_PROPERTY = 'kibana.alert.uuid';
export const KIBANA_DATE_FORMAT = 'MMM DD, YYYY @ hh:mm:ss.SSS';
Expand Down
7 changes: 4 additions & 3 deletions x-pack/plugins/session_view/kibana.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
{
"id": "sessionView",
"version": "8.0.0",
"version": "1.0.0",
"kibanaVersion": "kibana",
"owner": {
"name": "Security Team",
"githubTeam": "security-team"
},
"requiredPlugins": [
"data",
"timelines"
"timelines",
"ruleRegistry"
],
"requiredBundles": [
"kibanaReact",
"kibanaReact",
"esUiShared"
],
"server": true,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* 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 React from 'react';
import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
import {
DetailPanelAlertActions,
BUTTON_TEST_ID,
SHOW_DETAILS_TEST_ID,
JUMP_TO_PROCESS_TEST_ID,
} from './index';
import { mockAlerts } from '../../../common/mocks/constants/session_view_process.mock';
import userEvent from '@testing-library/user-event';
import { ProcessImpl } from '../process_tree/hooks';

describe('DetailPanelAlertActions component', () => {
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
let mockedContext: AppContextTestRender;
let mockShowAlertDetails = jest.fn((uuid) => uuid);
let mockOnProcessSelected = jest.fn((process) => process);

beforeEach(() => {
mockedContext = createAppRootMockRenderer();
mockShowAlertDetails = jest.fn((uuid) => uuid);
mockOnProcessSelected = jest.fn((process) => process);
});

describe('When DetailPanelAlertActions is mounted', () => {
it('renders a popover when button is clicked', async () => {
const mockEvent = mockAlerts[0];

renderResult = mockedContext.render(
<DetailPanelAlertActions
event={mockEvent}
onProcessSelected={mockOnProcessSelected}
onShowAlertDetails={mockShowAlertDetails}
/>
);

userEvent.click(renderResult.getByTestId(BUTTON_TEST_ID));
expect(renderResult.queryByTestId(SHOW_DETAILS_TEST_ID)).toBeTruthy();
expect(renderResult.queryByTestId(JUMP_TO_PROCESS_TEST_ID)).toBeTruthy();
expect(mockShowAlertDetails.mock.calls.length).toBe(0);
expect(mockOnProcessSelected.mock.calls.length).toBe(0);
});

it('calls alert flyout callback when View details clicked', async () => {
const mockEvent = mockAlerts[0];

renderResult = mockedContext.render(
<DetailPanelAlertActions
event={mockEvent}
onProcessSelected={mockOnProcessSelected}
onShowAlertDetails={mockShowAlertDetails}
/>
);

userEvent.click(renderResult.getByTestId(BUTTON_TEST_ID));
userEvent.click(renderResult.getByTestId(SHOW_DETAILS_TEST_ID));
expect(mockShowAlertDetails.mock.calls.length).toBe(1);
expect(mockShowAlertDetails.mock.results[0].value).toBe(mockEvent.kibana?.alert.uuid);
expect(mockOnProcessSelected.mock.calls.length).toBe(0);
});

it('calls onProcessSelected when Jump to process clicked', async () => {
const mockEvent = mockAlerts[0];

renderResult = mockedContext.render(
<DetailPanelAlertActions
event={mockEvent}
onProcessSelected={mockOnProcessSelected}
onShowAlertDetails={mockShowAlertDetails}
/>
);

userEvent.click(renderResult.getByTestId(BUTTON_TEST_ID));
userEvent.click(renderResult.getByTestId(JUMP_TO_PROCESS_TEST_ID));
expect(mockOnProcessSelected.mock.calls.length).toBe(1);
expect(mockOnProcessSelected.mock.results[0].value).toBeInstanceOf(ProcessImpl);
expect(mockShowAlertDetails.mock.calls.length).toBe(0);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* 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 React, { useState, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiPopover, EuiContextMenuPanel, EuiButtonIcon, EuiContextMenuItem } from '@elastic/eui';
import { Process, ProcessEvent } from '../../../common/types/process_tree';
import { ProcessImpl } from '../process_tree/hooks';

export const BUTTON_TEST_ID = 'sessionView:detailPanelAlertActionsBtn';
export const SHOW_DETAILS_TEST_ID = 'sessionView:detailPanelAlertActionShowDetails';
export const JUMP_TO_PROCESS_TEST_ID = 'sessionView:detailPanelAlertActionJumpToProcess';

interface DetailPanelAlertActionsDeps {
event: ProcessEvent;
onShowAlertDetails: (alertId: string) => void;
onProcessSelected: (process: Process) => void;
}

/**
* Detail panel alert context menu actions
*/
export const DetailPanelAlertActions = ({
event,
onShowAlertDetails,
onProcessSelected,
}: DetailPanelAlertActionsDeps) => {
const [isPopoverOpen, setPopover] = useState(false);

const onClosePopover = useCallback(() => {
setPopover(false);
}, []);

const onToggleMenu = useCallback(() => {
setPopover(!isPopoverOpen);
}, [isPopoverOpen]);

const onJumpToAlert = useCallback(() => {
const process = new ProcessImpl(event.process.entity_id);
process.addEvent(event);

onProcessSelected(process);
setPopover(false);
}, [event, onProcessSelected]);

const onShowDetails = useCallback(() => {
if (event.kibana) {
onShowAlertDetails(event.kibana.alert.uuid);
setPopover(false);
}
}, [event, onShowAlertDetails]);

if (!event.kibana) {
return null;
}

const { uuid } = event.kibana.alert;

const menuItems = [
<EuiContextMenuItem key="details" data-test-subj={SHOW_DETAILS_TEST_ID} onClick={onShowDetails}>
<FormattedMessage
id="xpack.sessionView.detailPanelAlertListItem.showDetailsAction"
defaultMessage="View alert details"
/>
</EuiContextMenuItem>,
<EuiContextMenuItem
key="jumpTo"
data-test-subj={JUMP_TO_PROCESS_TEST_ID}
onClick={onJumpToAlert}
>
<FormattedMessage
id="xpack.sessionView.detailPanelAlertListItem.jumpToAlert"
defaultMessage="Jump to alerted process"
/>
</EuiContextMenuItem>,
];

return (
<EuiPopover
id={uuid}
button={
<EuiButtonIcon
display="empty"
size="s"
iconType="boxesHorizontal"
aria-label={i18n.translate('xpack.sessionView.detailPanelAlertListItem.moreButton', {
defaultMessage: 'More',
})}
data-test-subj={BUTTON_TEST_ID}
onClick={onToggleMenu}
/>
}
isOpen={isPopoverOpen}
closePopover={onClosePopover}
panelPaddingSize="none"
anchorPosition="leftCenter"
>
<EuiContextMenuPanel size="s" items={menuItems} />
</EuiPopover>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* 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 { useMemo } from 'react';
import { useEuiTheme, transparentize } from '@elastic/eui';
import { CSSObject, css } from '@emotion/react';

interface StylesDeps {
minimal?: boolean;
isInvestigated?: boolean;
}

export const useStyles = ({ minimal = false, isInvestigated = false }: StylesDeps) => {
const { euiTheme } = useEuiTheme();

const cached = useMemo(() => {
const { colors, font, size, border } = euiTheme;

const dangerBorder = transparentize(colors.danger, 0.2);
const dangerBackground = transparentize(colors.danger, 0.08);
const borderThickness = border.width.thin;
const mediumPadding = size.m;

let alertTitleColor = colors.text;
let borderColor = colors.lightShade;

if (isInvestigated) {
alertTitleColor = colors.primaryText;
borderColor = dangerBorder;
}

const alertItem = css`
border: ${borderThickness} solid ${borderColor};
padding: ${mediumPadding};
border-radius: ${border.radius.medium};
margin: 0 ${mediumPadding} ${mediumPadding} ${mediumPadding};
background-color: ${colors.emptyShade};
& .euiAccordion__buttonContent {
width: 100%;
}
& .euiAccordion__button {
min-width: 0;
width: calc(100% - ${size.l});
}
& .euiAccordion__childWrapper {
overflow: visible;
}
`;

const alertTitle: CSSObject = {
display: minimal ? 'none' : 'initial',
color: alertTitleColor,
fontWeight: font.weight.semiBold,
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
};

const alertIcon: CSSObject = {
marginRight: size.s,
};

const alertAccordionButton: CSSObject = {
width: `calc(100% - ${size.l})`,
minWidth: 0,
};

const processPanel: CSSObject = {
border: `${borderThickness} solid ${colors.lightShade}`,
fontFamily: font.familyCode,
marginTop: mediumPadding,
padding: `${size.xs} ${size.s}`,
};

const investigatedLabel: CSSObject = {
position: 'relative',
zIndex: 1,
bottom: `-${mediumPadding}`,
left: `-${mediumPadding}`,
width: `calc(100% + ${mediumPadding} * 2)`,
borderTop: `${borderThickness} solid ${dangerBorder}`,
borderBottomLeftRadius: border.radius.medium,
borderBottomRightRadius: border.radius.medium,
backgroundColor: dangerBackground,
textAlign: 'center',
};

return {
alertItem,
alertTitle,
alertIcon,
alertAccordionButton,
processPanel,
investigatedLabel,
};
}, [euiTheme, isInvestigated, minimal]);

return cached;
};
Loading

0 comments on commit c55bb91

Please sign in to comment.