Skip to content

Commit

Permalink
Add Entity data table to security events (#21091)
Browse files Browse the repository at this point in the history
* Add Entity data table to security events

* Add event definitions

* Refactoring of expand row content

* fix alert filter

* Add event definition filter

* fix tsc errors

* fix tests

* add feature flag

* fix linter errors

* fix linter errors

* fix linter errors

* Fixing formatting.

---------

Co-authored-by: Dennis Oelkers <[email protected]>
  • Loading branch information
maxiadlovskii and dennisoelkers authored Dec 11, 2024
1 parent dab4dbe commit a18ee23
Show file tree
Hide file tree
Showing 10 changed files with 222 additions and 115 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,6 @@ data_warehouse_search=off

# Enable preview of input setup wizard
setup_mode=off

# Show security events in paginated entity data table
show_security_events_in_pedt=off
37 changes: 22 additions & 15 deletions graylog2-web-interface/src/components/events/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,7 @@ import EventDefinitionPriorityEnum from 'logic/alerts/EventDefinitionPriorityEnu

export const EVENTS_ENTITY_TABLE_ID = 'events';

export const detailsAttributes: Array<Attribute> = [
{
id: 'id',
title: 'ID',
type: 'STRING',
sortable: true,
},
export const commonEventAttributes: Array<Attribute> = [
{
id: 'priority',
title: 'Priority',
Expand Down Expand Up @@ -58,6 +52,21 @@ export const detailsAttributes: Array<Attribute> = [
type: 'STRING',
sortable: true,
},
{
id: 'key',
title: 'Key',
type: 'STRING',
sortable: true,
searchable: false,
},
{
id: 'group_by_fields',
title: 'Group-By Fields',
sortable: false,
},
];
export const detailsAttributes: Array<Attribute> = [
...commonEventAttributes,
{
id: 'remediation_steps',
title: 'Remediation Steps',
Expand All @@ -71,8 +80,8 @@ export const detailsAttributes: Array<Attribute> = [
filterable: true,
},
{
id: 'key',
title: 'Key',
id: 'id',
title: 'ID',
type: 'STRING',
sortable: true,
searchable: true,
Expand All @@ -84,13 +93,8 @@ export const detailsAttributes: Array<Attribute> = [
type: 'STRING',
sortable: false,
},
{
id: 'group_by_fields',
title: 'Group-By Fields',
sortable: false,
},
];
export const additionalAttributes: Array<Attribute> = [
export const eventsTableSpecificAttributes: Array<Attribute> = [
{
id: 'message',
title: 'Description',
Expand All @@ -106,6 +110,9 @@ export const additionalAttributes: Array<Attribute> = [
filterable: true,
filter_options: [{ value: 'false', title: 'Event' }, { value: 'true', title: 'Alert' }],
},
];
export const additionalAttributes: Array<Attribute> = [
...eventsTableSpecificAttributes,
...detailsAttributes,
];

Expand Down
21 changes: 6 additions & 15 deletions graylog2-web-interface/src/components/events/ExpandedSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,13 @@
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import React, { useMemo } from 'react';
import React from 'react';

import useTableLayout from 'components/common/EntityDataTable/hooks/useTableLayout';
import { useTableFetchContext } from 'components/common/PaginatedEntityTable';
import type { Attribute } from 'stores/PaginationTypes';
import type useTableLayout from 'components/common/EntityDataTable/hooks/useTableLayout';
import type { Event, EventsAdditionalData } from 'components/events/events/types';
import useMetaDataContext from 'components/common/EntityDataTable/hooks/useMetaDataContext';
import EventDetailsTable from 'components/events/events/EventDetailsTable';
import GeneralEventDetailsTable from 'components/events/events/GeneralEventDetailsTable';
import useNonDisplayedAttributes from 'components/events/events/hooks/useNonDisplayedAttributes';

type Props = {
defaultLayout: Parameters<typeof useTableLayout>[0],
Expand All @@ -30,20 +29,12 @@ type Props = {

const ExpandedSection = ({ defaultLayout, event }: Props) => {
const { meta } = useMetaDataContext<EventsAdditionalData>();
const { layoutConfig: { displayedAttributes }, isInitialLoading } = useTableLayout(defaultLayout);
const { attributes } = useTableFetchContext();

const nonDisplayedAttributes = useMemo(() => {
if (isInitialLoading) return [];

const displayedAttributesSet = new Set(displayedAttributes);

return attributes.filter(({ id }) => !displayedAttributesSet.has(id)).map(({ id, title }: Attribute) => ({ id, title }));
}, [attributes, displayedAttributes, isInitialLoading]);
const nonDisplayedAttributes = useNonDisplayedAttributes(defaultLayout);

if (!nonDisplayedAttributes.length) return <em>No further details</em>;

return <EventDetailsTable attributesList={nonDisplayedAttributes} event={event} meta={meta} />;
return <GeneralEventDetailsTable attributesList={nonDisplayedAttributes} event={event} meta={meta} />;
};

export default ExpandedSection;
Original file line number Diff line number Diff line change
Expand Up @@ -20,38 +20,22 @@ import { useMemo } from 'react';
import styled from 'styled-components';
import isEmpty from 'lodash/isEmpty';

import { isPermitted } from 'util/PermissionsMixin';
import type { ColumnRenderers } from 'components/common/EntityDataTable';
import EventTypeLabel from 'components/events/events/EventTypeLabel';
import { Link } from 'components/common/router';
import Routes from 'routing/Routes';
import type { Event, EventsAdditionalData } from 'components/events/events/types';
import PriorityName from 'components/events/events/PriorityName';
import usePluginEntities from 'hooks/usePluginEntities';
import EventFields from 'components/events/events/EventFields';
import { MarkdownPreview } from 'components/common/MarkdownEditor';
import useExpandedSections from 'components/common/EntityDataTable/hooks/useExpandedSections';
import { Timestamp } from 'components/common';
import useCurrentUser from 'hooks/useCurrentUser';
import type { ColumnRenderersByAttribute, EntityBase } from 'components/common/EntityDataTable/types';
import EventDefinitionLink from 'components/events/events/EventDefinitionLink';

const EventDefinitionRenderer = ({ eventDefinitionId, meta }: { eventDefinitionId: string, meta: EventsAdditionalData }) => {
const { permissions } = useCurrentUser();
const { context: eventsContext } = meta;
const eventDefinitionContext = eventsContext?.event_definitions?.[eventDefinitionId];

if (!eventDefinitionContext) {
return <em>{eventDefinitionId}</em>;
}

if (isPermitted(permissions, `eventdefinitions:edit:${eventDefinitionContext.id}`)) {
return (
<Link to={Routes.ALERTS.DEFINITIONS.edit(eventDefinitionContext.id)}>
{eventDefinitionContext.title}
</Link>
);
}
const title = meta?.context?.event_definitions?.[eventDefinitionId]?.title;

return <>{eventDefinitionContext.title}</>;
return <EventDefinitionLink id={eventDefinitionId} title={title} />;
};

const EventDefinitionTypeRenderer = ({ type }: { type: string }) => {
Expand Down Expand Up @@ -122,45 +106,48 @@ const TimeRangeRenderer = ({ eventData }: { eventData: Event}) => (eventData.tim
<em>No time range</em>
));

export const getGeneralEventAttributeRenderers = <T extends EntityBase, M = unknown>(): ColumnRenderersByAttribute<T, M> => ({
message: {
minWidth: 300,
width: 0.5,
renderCell: (_message: string, event) => <MessageRenderer message={_message} eventId={event.id} />,
},
key: {
renderCell: (_key: string) => <span>{_key || <em>No Key set for this Event.</em>}</span>,
staticWidth: 200,
},
id: {
staticWidth: 300,
},
alert: {
renderCell: (_alert: boolean) => <EventTypeLabel isAlert={_alert} />,
staticWidth: 100,
},
priority: {
renderCell: (_priority: number) => <PriorityName priority={_priority} />,
staticWidth: 100,
},
event_definition_type: {
renderCell: (_type: string) => <EventDefinitionTypeRenderer type={_type} />,
staticWidth: 200,
},
group_by_fields: {
renderCell: (groupByFields: Record<string, string>) => <GroupByFieldsRenderer groupByFields={groupByFields} />,
staticWidth: 400,
},
});
const customColumnRenderers = (): ColumnRenderers<Event> => ({
attributes: {
message: {
minWidth: 300,
width: 0.5,
renderCell: (_message: string, event) => <MessageRenderer message={_message} eventId={event.id} />,
},
key: {
renderCell: (_key: string) => <span>{_key || <em>No Key set for this Event.</em>}</span>,
staticWidth: 200,
},
id: {
staticWidth: 300,
},
alert: {
renderCell: (_alert: boolean) => <EventTypeLabel isAlert={_alert} />,
staticWidth: 100,
},
...getGeneralEventAttributeRenderers<Event>(),
event_definition_id: {
minWidth: 300,
width: 0.3,
renderCell: (_eventDefinitionId: string, _, __, meta: EventsAdditionalData) => <EventDefinitionRenderer meta={meta} eventDefinitionId={_eventDefinitionId} />,
},
priority: {
renderCell: (_priority: number) => <PriorityName priority={_priority} />,
staticWidth: 100,
},
event_definition_type: {
renderCell: (_type: string) => <EventDefinitionTypeRenderer type={_type} />,
staticWidth: 200,
},
fields: {
renderCell: (_fields: Record<string, string>) => <FieldsRenderer fields={_fields} />,
staticWidth: 400,
},
group_by_fields: {
renderCell: (groupByFields: Record<string, string>) => <GroupByFieldsRenderer groupByFields={groupByFields} />,
staticWidth: 400,
},
remediation_steps: {
renderCell: (_, event: Event, __, meta: EventsAdditionalData) => <RemediationStepRenderer meta={meta} eventDefinitionId={event.event_definition_id} />,
width: 0.3,
Expand All @@ -172,6 +159,6 @@ const customColumnRenderers = (): ColumnRenderers<Event> => ({
},
});

const useColumnRenderers = () => useMemo(customColumnRenderers, []);
const useColumnRenderers = () => useMemo<ColumnRenderers<Event>>(customColumnRenderers, []);

export default useColumnRenderers;
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import React from 'react';

import useCurrentUser from 'hooks/useCurrentUser';
import { isPermitted } from 'util/PermissionsMixin';
import { Link } from 'components/common/router';
import Routes from 'routing/Routes';

const EventDefinitionLink = ({ title, id }: { title: string | undefined, id: string }) => {
const { permissions } = useCurrentUser();

if (!title) {
return <em>{id}</em>;
}

if (isPermitted(permissions, `eventdefinitions:edit:${id}`)) {
return (
<Link to={Routes.ALERTS.DEFINITIONS.edit(id)}>
{title}
</Link>
);
}

return <>{title}</>;
};

export default EventDefinitionLink;
Original file line number Diff line number Diff line change
Expand Up @@ -19,38 +19,39 @@ import { styled } from 'styled-components';

import { Table } from 'components/bootstrap';
import type { Event, EventsAdditionalData } from 'components/events/events/types';
import useColumnRenderers from 'components/events/events/ColumnRenderers';
import type { ColumnRenderersByAttribute, EntityBase } from 'components/common/EntityDataTable/types';
import DefaultColumnRenderers from 'components/common/EntityDataTable/DefaultColumnRenderers';
import type { Attribute } from 'stores/PaginationTypes';

const TD = styled.td`
white-space: nowrap;
`;

type Props = {
type Props<T extends EntityBase, M = EventsAdditionalData> = {
attributesList: Array<{ id: string, title: string}>,
event: Event,
meta: EventsAdditionalData,
event: T,
meta?: M | {},
attributesRenderers: ColumnRenderersByAttribute<T, M>
}

const EventDetailsTable = ({ event, attributesList, meta }: Props) => {
const { attributes: attributesRenderers } = useColumnRenderers();
const EventDetailsTable = <E extends EntityBase = Event>({ event, attributesList, meta = {}, attributesRenderers }: Props<E>) => (
<Table condensed striped>
<tbody>
{attributesList.map((attribute: Attribute) => {
const defaultTypeRenderer = DefaultColumnRenderers.types?.[attribute?.type]?.renderCell;
const typeRenderer = attributesRenderers?.types?.[attribute?.type]?.renderCell;
const renderCell = attributesRenderers[attribute.id]?.renderCell ?? typeRenderer ?? defaultTypeRenderer;
const value = event[attribute.id];

return (
<Table condensed striped>
<tbody>
{attributesList.map((attribute) => {
const renderCell = attributesRenderers[attribute.id]?.renderCell;
const value = event[attribute.id];

return (
<tr key={attribute.id}>
<TD><b>{attribute.title}</b></TD>
<td>{renderCell ? renderCell(value, event, attribute, meta) : value}</td>
</tr>
);
})}
</tbody>
</Table>
);
};
return (
<tr key={attribute.id}>
<TD><b>{attribute.title}</b></TD>
<td>{renderCell ? renderCell(value, event, attribute, meta) : value}</td>
</tr>
);
})}
</tbody>
</Table>
);

export default EventDetailsTable;
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import React from 'react';

import type { Event, EventsAdditionalData } from 'components/events/events/types';
import useColumnRenderers from 'components/events/events/ColumnRenderers';
import EventDetailsTable from 'components/events/events/EventDetailsTable';

type Props = {
attributesList: Array<{ id: string, title: string}>,
event: Event,
meta: EventsAdditionalData,
}

const GeneralEventDetailsTable = ({ event, attributesList, meta }: Props) => {
const { attributes: attributesRenderers } = useColumnRenderers();

return (
<EventDetailsTable<Event> event={event} meta={meta} attributesRenderers={attributesRenderers} attributesList={attributesList} />
);
};

export default GeneralEventDetailsTable;
Loading

0 comments on commit a18ee23

Please sign in to comment.