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

[Security solution] [RAC] Add row renderer popover to alert table "reason" field #108054

Merged
merged 3 commits into from
Aug 12, 2021
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 @@ -31,6 +31,9 @@ export const RenderCellValue: React.FC<
rowIndex,
setCellProps,
timelineId,
ecsData,
rowRenderers,
browserFields,
}) => (
<DefaultCellRenderer
columnId={columnId}
Expand All @@ -45,5 +48,8 @@ export const RenderCellValue: React.FC<
rowIndex={rowIndex}
setCellProps={setCellProps}
timelineId={timelineId}
ecsData={ecsData}
rowRenderers={rowRenderers}
browserFields={browserFields}
/>
);
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,26 @@ const Link = ({ children, url }: { children: React.ReactNode; url: string }) =>
</EuiLink>
);

export const eventRendererNames: { [key in RowRendererId]: string } = {
[RowRendererId.alerts]: i18n.ALERTS_NAME,
[RowRendererId.auditd]: i18n.AUDITD_NAME,
[RowRendererId.auditd_file]: i18n.AUDITD_FILE_NAME,
[RowRendererId.library]: i18n.LIBRARY_NAME,
[RowRendererId.system_security_event]: i18n.AUTHENTICATION_NAME,
[RowRendererId.system_dns]: i18n.DNS_NAME,
[RowRendererId.netflow]: i18n.FLOW_NAME,
[RowRendererId.system]: i18n.SYSTEM_NAME,
[RowRendererId.system_endgame_process]: i18n.PROCESS,
[RowRendererId.registry]: i18n.REGISTRY_NAME,
[RowRendererId.system_fim]: i18n.FIM_NAME,
[RowRendererId.system_file]: i18n.FILE_NAME,
[RowRendererId.system_socket]: i18n.SOCKET_NAME,
[RowRendererId.suricata]: 'Suricata',
[RowRendererId.threat_match]: i18n.THREAT_MATCH_NAME,
[RowRendererId.zeek]: i18n.ZEEK_NAME,
[RowRendererId.plain]: '',
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want a default name for the plain renderer? We can do that in a follow up PR, but we should find out

Copy link
Member Author

Choose a reason for hiding this comment

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

I am struggling to find a scenario where we would display the plain RowRenderer. I think that it won't be displayed on the alert page.

When I call getRowRenderer and no ronRenderer is found it returns null Instead of returning plainRowRenderer.

I couldn't find any reference to plain_row_renderer.tsx on the code.

Here there is a comment mentioning it but the comment is outdated because plainRowRenderer is not included on defaultRowRenderers
Screenshot 2021-08-12 at 14 13 54

If I am right, we could delete plain_row_renderer.tsx

};

export interface RowRendererOption {
id: RowRendererId;
name: string;
Expand All @@ -51,14 +71,14 @@ export interface RowRendererOption {
export const renderers: RowRendererOption[] = [
{
id: RowRendererId.alerts,
name: i18n.ALERTS_NAME,
name: eventRendererNames[RowRendererId.alerts],
description: i18n.ALERTS_DESCRIPTION,
example: AlertsExample,
searchableDescription: i18n.ALERTS_DESCRIPTION,
},
{
id: RowRendererId.auditd,
name: i18n.AUDITD_NAME,
name: eventRendererNames[RowRendererId.auditd],
description: (
<span>
<Link url="https://www.elastic.co/guide/en/beats/auditbeat/current/auditbeat-module-auditd.html">
Expand All @@ -72,7 +92,7 @@ export const renderers: RowRendererOption[] = [
},
{
id: RowRendererId.auditd_file,
name: i18n.AUDITD_FILE_NAME,
name: eventRendererNames[RowRendererId.auditd_file],
description: (
<span>
<Link url="https://www.elastic.co/guide/en/beats/auditbeat/current/auditbeat-module-auditd.html">
Expand All @@ -86,14 +106,14 @@ export const renderers: RowRendererOption[] = [
},
{
id: RowRendererId.library,
name: i18n.LIBRARY_NAME,
name: eventRendererNames[RowRendererId.library],
description: i18n.LIBRARY_DESCRIPTION,
example: LibraryExample,
searchableDescription: i18n.LIBRARY_DESCRIPTION,
},
{
id: RowRendererId.system_security_event,
name: i18n.AUTHENTICATION_NAME,
name: eventRendererNames[RowRendererId.system_security_event],
description: (
<div>
<p>{i18n.AUTHENTICATION_DESCRIPTION_PART1}</p>
Expand All @@ -106,14 +126,14 @@ export const renderers: RowRendererOption[] = [
},
{
id: RowRendererId.system_dns,
name: i18n.DNS_NAME,
name: eventRendererNames[RowRendererId.system_dns],
description: i18n.DNS_DESCRIPTION_PART1,
example: SystemDnsExample,
searchableDescription: i18n.DNS_DESCRIPTION_PART1,
},
{
id: RowRendererId.netflow,
name: i18n.FLOW_NAME,
name: eventRendererNames[RowRendererId.netflow],
description: (
<div>
<p>{i18n.FLOW_DESCRIPTION_PART1}</p>
Expand All @@ -126,7 +146,7 @@ export const renderers: RowRendererOption[] = [
},
{
id: RowRendererId.system,
name: i18n.SYSTEM_NAME,
name: eventRendererNames[RowRendererId.system],
description: (
<div>
<p>
Expand All @@ -145,7 +165,7 @@ export const renderers: RowRendererOption[] = [
},
{
id: RowRendererId.system_endgame_process,
name: i18n.PROCESS,
name: eventRendererNames[RowRendererId.system_endgame_process],
description: (
<div>
<p>{i18n.PROCESS_DESCRIPTION_PART1}</p>
Expand All @@ -158,28 +178,28 @@ export const renderers: RowRendererOption[] = [
},
{
id: RowRendererId.registry,
name: i18n.REGISTRY_NAME,
name: eventRendererNames[RowRendererId.registry],
description: i18n.REGISTRY_DESCRIPTION,
example: RegistryExample,
searchableDescription: i18n.REGISTRY_DESCRIPTION,
},
{
id: RowRendererId.system_fim,
name: i18n.FIM_NAME,
name: eventRendererNames[RowRendererId.system_fim],
description: i18n.FIM_DESCRIPTION_PART1,
example: SystemFimExample,
searchableDescription: i18n.FIM_DESCRIPTION_PART1,
},
{
id: RowRendererId.system_file,
name: i18n.FILE_NAME,
name: eventRendererNames[RowRendererId.system_file],
description: i18n.FILE_DESCRIPTION_PART1,
example: SystemFileExample,
searchableDescription: i18n.FILE_DESCRIPTION_PART1,
},
{
id: RowRendererId.system_socket,
name: i18n.SOCKET_NAME,
name: eventRendererNames[RowRendererId.system_socket],
description: (
<div>
<p>{i18n.SOCKET_DESCRIPTION_PART1}</p>
Expand All @@ -192,7 +212,7 @@ export const renderers: RowRendererOption[] = [
},
{
id: RowRendererId.suricata,
name: 'Suricata',
name: eventRendererNames[RowRendererId.suricata],
description: (
<p>
{i18n.SURICATA_DESCRIPTION_PART1}{' '}
Expand All @@ -207,14 +227,14 @@ export const renderers: RowRendererOption[] = [
},
{
id: RowRendererId.threat_match,
name: i18n.THREAT_MATCH_NAME,
name: eventRendererNames[RowRendererId.threat_match],
description: i18n.THREAT_MATCH_DESCRIPTION,
example: ThreatMatchExample,
searchableDescription: `${i18n.THREAT_MATCH_NAME} ${i18n.THREAT_MATCH_DESCRIPTION}`,
},
{
id: RowRendererId.zeek,
name: i18n.ZEEK_NAME,
name: eventRendererNames[RowRendererId.zeek],
description: (
<p>
{i18n.ZEEK_DESCRIPTION_PART1}{' '}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
*/

import type React from 'react';
import { ColumnHeaderOptions } from '../../../../../../common';

import { BrowserFields, ColumnHeaderOptions, RowRenderer } from '../../../../../../common';
import { Ecs } from '../../../../../../common/ecs';
import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';

export interface ColumnRenderer {
Expand All @@ -29,5 +31,8 @@ export interface ColumnRenderer {
truncate?: boolean;
values: string[] | null | undefined;
linkValues?: string[] | null | undefined;
ecsData?: Ecs;
rowRenderers?: RowRenderer[];
browserFields?: BrowserFields;
}) => React.ReactNode;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export const EVENT_URL_FIELD_NAME = 'event.url';
export const SIGNAL_RULE_NAME_FIELD_NAME = 'signal.rule.name';
export const SIGNAL_STATUS_FIELD_NAME = 'signal.status';
export const AGENT_STATUS_FIELD_NAME = 'agent.status';
export const REASON_FIELD_NAME = 'signal.reason';
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { unknownColumnRenderer } from './unknown_column_renderer';
import { zeekRowRenderer } from './zeek/zeek_row_renderer';
import { systemRowRenderers } from './system/generic_row_renderer';
import { threatMatchRowRenderer } from './cti/threat_match_row_renderer';
import { reasonColumnRenderer } from './reason_column_renderer';

// The row renderers are order dependent and will return the first renderer
// which returns true from its isInstance call. The bottom renderers which
Expand All @@ -34,6 +35,7 @@ export const defaultRowRenderers: RowRenderer[] = [
];

export const columnRenderers: ColumnRenderer[] = [
reasonColumnRenderer,
plainColumnRenderer,
emptyColumnRenderer,
unknownColumnRenderer,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you for adding tests!

* 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 { mockTimelineData } from '../../../../../common/mock';
import { defaultColumnHeaderType } from '../column_headers/default_headers';
import { REASON_FIELD_NAME } from './constants';
import { reasonColumnRenderer } from './reason_column_renderer';
import { plainColumnRenderer } from './plain_column_renderer';

import {
BrowserFields,
ColumnHeaderOptions,
RowRenderer,
RowRendererId,
} from '../../../../../../common';
import { fireEvent, render } from '@testing-library/react';
import { TestProviders } from '../../../../../../../timelines/public/mock';
import { useDraggableKeyboardWrapper as mockUseDraggableKeyboardWrapper } from '../../../../../../../timelines/public/components';
import { cloneDeep } from 'lodash';
jest.mock('./plain_column_renderer');

jest.mock('../../../../../common/lib/kibana', () => {
const originalModule = jest.requireActual('../../../../../common/lib/kibana');
return {
...originalModule,
useKibana: () => ({
services: {
timelines: {
getUseDraggableKeyboardWrapper: () => mockUseDraggableKeyboardWrapper,
},
},
}),
};
});

jest.mock('../../../../../common/components/link_to', () => {
const original = jest.requireActual('../../../../../common/components/link_to');
return {
...original,
useFormatUrl: () => ({
formatUrl: () => '',
}),
};
});

const invalidEcs = cloneDeep(mockTimelineData[0].ecs);
const validEcs = cloneDeep(mockTimelineData[28].ecs);

const field: ColumnHeaderOptions = {
id: 'test-field-id',
columnHeaderType: defaultColumnHeaderType,
};

const rowRenderers: RowRenderer[] = [
{
id: RowRendererId.alerts,
isInstance: (ecs) => ecs === validEcs,
// eslint-disable-next-line react/display-name
renderRow: () => <span data-test-subj="test-row-render" />,
},
];
const browserFields: BrowserFields = {};

const defaultProps = {
columnName: REASON_FIELD_NAME,
eventId: 'test-event-id',
field,
timelineId: 'test-timeline-id',
values: ['test-value'],
};

describe('reasonColumnRenderer', () => {
beforeEach(() => {
jest.resetAllMocks();
});

describe('isIntance', () => {
it('returns true when columnName is `signal.reason`', () => {
expect(reasonColumnRenderer.isInstance(REASON_FIELD_NAME, [])).toBeTruthy();
});
});

describe('renderColumn', () => {
it('calls `plainColumnRenderer.renderColumn` when ecsData, rowRenderers or browserFields is empty', () => {
reasonColumnRenderer.renderColumn(defaultProps);

expect(plainColumnRenderer.renderColumn).toBeCalledTimes(1);
});

it("doesn't call `plainColumnRenderer.renderColumn` when ecsData, rowRenderers or browserFields fields are not empty", () => {
reasonColumnRenderer.renderColumn({
...defaultProps,
ecsData: invalidEcs,
rowRenderers,
browserFields,
});

expect(plainColumnRenderer.renderColumn).toBeCalledTimes(0);
});

it("doesn't render popover button when getRowRenderer doesn't find a rowRenderer", () => {
const renderedColumn = reasonColumnRenderer.renderColumn({
...defaultProps,
ecsData: invalidEcs,
rowRenderers,
browserFields,
});

const wrapper = render(<TestProviders>{renderedColumn}</TestProviders>);

expect(wrapper.queryByTestId('reason-cell-button')).not.toBeInTheDocument();
});

it('render popover button when getRowRenderer finds a rowRenderer', () => {
const renderedColumn = reasonColumnRenderer.renderColumn({
...defaultProps,
ecsData: validEcs,
rowRenderers,
browserFields,
});

const wrapper = render(<TestProviders>{renderedColumn}</TestProviders>);

expect(wrapper.queryByTestId('reason-cell-button')).toBeInTheDocument();
});

it('render rowRender inside a popover when reson field button is clicked', () => {
const renderedColumn = reasonColumnRenderer.renderColumn({
...defaultProps,
ecsData: validEcs,
rowRenderers,
browserFields,
});

const wrapper = render(<TestProviders>{renderedColumn}</TestProviders>);

fireEvent.click(wrapper.getByTestId('reason-cell-button'));

expect(wrapper.queryByTestId('test-row-render')).toBeInTheDocument();
});
});
});
Loading