Skip to content

Commit

Permalink
[Security Solution][Resolver] Word-break long titles in related event… (
Browse files Browse the repository at this point in the history
#75926)

* [Security Solution][Resolver] Word-break long titles in related event description lists

* word-break long titles at non-word boundaries

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
  • Loading branch information
bkimmel and elasticmachine committed Aug 27, 2020
1 parent c08bf7f commit 4294232
Show file tree
Hide file tree
Showing 12 changed files with 225 additions and 122 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as selectors from './selectors';
import { DataState } from '../../types';
import { DataAction } from './action';
import { ResolverChildNode, ResolverTree } from '../../../../common/endpoint/types';
import * as eventModel from '../../../../common/endpoint/models/event';

/**
* Test the data reducer and selector.
Expand Down Expand Up @@ -175,6 +176,24 @@ describe('Resolver Data Middleware', () => {
eventStatsForFirstChildNode.byCategory[categoryToOverCount] - 1
);
});
it('should return the correct related event detail metadata for a given related event', () => {
const relatedEventsByCategory = selectors.relatedEventsByCategory(store.getState());
const someRelatedEventForTheFirstChild = relatedEventsByCategory(firstChildNodeInTree.id)(
categoryToOverCount
)[0];
const relatedEventID = eventModel.eventId(someRelatedEventForTheFirstChild)!;
const relatedDisplayInfo = selectors.relatedEventDisplayInfoByEntityAndSelfID(
store.getState()
)(firstChildNodeInTree.id, relatedEventID);
const [, countOfSameType, , sectionData] = relatedDisplayInfo;
const hostEntries = sectionData.filter((section) => {
return section.sectionTitle === 'host';
})[0].entries;
expect(hostEntries).toContainEqual({ title: 'os.platform', description: 'Windows' });
expect(countOfSameType).toBe(
eventStatsForFirstChildNode.byCategory[categoryToOverCount] - 1
);
});
it('should indicate the limit has been exceeded because the number of related events received for the category is less than what the stats count said it would be', () => {
const selectedRelatedInfo = selectors.relatedEventInfoByEntityId(store.getState());
const shouldShowLimit = selectedRelatedInfo(firstChildNodeInTree.id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
IndexedProcessNode,
AABB,
VisibleEntites,
SectionData,
} from '../../types';
import {
isGraphableProcess,
Expand All @@ -29,11 +30,14 @@ import {
ResolverNodeStats,
ResolverRelatedEvents,
SafeResolverEvent,
EndpointEvent,
LegacyEndpointEvent,
} from '../../../../common/endpoint/types';
import * as resolverTreeModel from '../../models/resolver_tree';
import * as isometricTaxiLayoutModel from '../../models/indexed_process_tree/isometric_taxi_layout';
import * as eventModel from '../../../../common/endpoint/models/event';
import * as vector2 from '../../models/vector2';
import { formatDate } from '../../view/panels/panel_content_utilities';

/**
* If there is currently a request.
Expand Down Expand Up @@ -173,6 +177,100 @@ export function relatedEventsByEntityId(data: DataState): Map<string, ResolverRe
return data.relatedEvents;
}

/**
* A helper function to turn objects into EuiDescriptionList entries.
* This reflects the strategy of more or less "dumping" metadata for related processes
* in description lists with little/no 'prettification'. This has the obvious drawback of
* data perhaps appearing inscrutable/daunting, but the benefit of presenting these fields
* to the user "as they occur" in ECS, which may help them with e.g. EQL queries.
*
* Given an object like: {a:{b: 1}, c: 'd'} it will yield title/description entries like so:
* {title: "a.b", description: "1"}, {title: "c", description: "d"}
*
* @param {object} obj The object to turn into `<dt><dd>` entries
*/
const objectToDescriptionListEntries = function* (
obj: object,
prefix = ''
): Generator<{ title: string; description: string }> {
const nextPrefix = prefix.length ? `${prefix}.` : '';
for (const [metaKey, metaValue] of Object.entries(obj)) {
if (typeof metaValue === 'number' || typeof metaValue === 'string') {
yield { title: nextPrefix + metaKey, description: `${metaValue}` };
} else if (metaValue instanceof Array) {
yield {
title: nextPrefix + metaKey,
description: metaValue
.filter((arrayEntry) => {
return typeof arrayEntry === 'number' || typeof arrayEntry === 'string';
})
.join(','),
};
} else if (typeof metaValue === 'object') {
yield* objectToDescriptionListEntries(metaValue, nextPrefix + metaKey);
}
}
};

/**
* Returns a function that returns the information needed to display related event details based on
* the related event's entityID and its own ID.
*/
export const relatedEventDisplayInfoByEntityAndSelfID: (
state: DataState
) => (
entityId: string,
relatedEventId: string | number
) => [
EndpointEvent | LegacyEndpointEvent | undefined,
number,
string | undefined,
SectionData,
string
] = createSelector(relatedEventsByEntityId, function relatedEventDetails(
/* eslint-disable no-shadow */
relatedEventsByEntityId
/* eslint-enable no-shadow */
) {
return defaultMemoize((entityId: string, relatedEventId: string | number) => {
const relatedEventsForThisProcess = relatedEventsByEntityId.get(entityId);
if (!relatedEventsForThisProcess) {
return [undefined, 0, undefined, [], ''];
}
const specificEvent = relatedEventsForThisProcess.events.find(
(evt) => eventModel.eventId(evt) === relatedEventId
);
// For breadcrumbs:
const specificCategory = specificEvent && eventModel.primaryEventCategory(specificEvent);
const countOfCategory = relatedEventsForThisProcess.events.reduce((sumtotal, evt) => {
return eventModel.primaryEventCategory(evt) === specificCategory ? sumtotal + 1 : sumtotal;
}, 0);

// Assuming these details (agent, ecs, process) aren't as helpful, can revisit
const { agent, ecs, process, ...relevantData } = specificEvent as ResolverEvent & {
// Type this with various unknown keys so that ts will let us delete those keys
ecs: unknown;
process: unknown;
};

let displayDate = '';
const sectionData: SectionData = Object.entries(relevantData)
.map(([sectionTitle, val]) => {
if (sectionTitle === '@timestamp') {
displayDate = formatDate(val);
return { sectionTitle: '', entries: [] };
}
if (typeof val !== 'object') {
return { sectionTitle, entries: [{ title: sectionTitle, description: `${val}` }] };
}
return { sectionTitle, entries: [...objectToDescriptionListEntries(val)] };
})
.filter((v) => v.sectionTitle !== '' && v.entries.length);

return [specificEvent, countOfCategory, specificCategory, sectionData, displayDate];
});
});

/**
* Returns a function that returns a function (when supplied with an entity id for a node)
* that returns related events for a node that match an event.category (when supplied with the category)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,15 @@ export const relatedEventsByEntityId = composeSelectors(
dataSelectors.relatedEventsByEntityId
);

/**
* Returns a function that returns the information needed to display related event details based on
* the related event's entityID and its own ID.
*/
export const relatedEventDisplayInfoByEntityAndSelfId = composeSelectors(
dataStateSelector,
dataSelectors.relatedEventDisplayInfoByEntityAndSelfID
);

/**
* Returns a function that returns a function (when supplied with an entity id for a node)
* that returns related events for a node that match an event.category (when supplied with the category)
Expand Down
16 changes: 16 additions & 0 deletions x-pack/plugins/security_solution/public/resolver/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,22 @@ export interface IndexedProcessNode extends BBox {
position: Vector2;
}

/**
* A type describing the shape of section titles and entries for description lists
*/
export type SectionData = Array<{
sectionTitle: string;
entries: Array<{ title: string; description: string }>;
}>;

/**
* The two query parameters we read/write on to control which view the table presents:
*/
export interface CrumbInfo {
crumbId: string;
crumbEvent: string;
}

/**
* A type containing all things to actually be rendered to the DOM.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import React, { memo, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiBasicTableColumn, EuiButtonEmpty, EuiSpacer, EuiInMemoryTable } from '@elastic/eui';
import { FormattedMessage } from 'react-intl';
import { CrumbInfo, StyledBreadcrumbs } from './panel_content_utilities';
import { StyledBreadcrumbs } from './panel_content_utilities';

import * as event from '../../../../common/endpoint/models/event';
import { ResolverEvent, ResolverNodeStats } from '../../../../common/endpoint/types';
import { CrumbInfo } from '../../types';

/**
* This view gives counts for all the related events of a process grouped by related event type.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
import { i18n } from '@kbn/i18n';
import { EuiSpacer, EuiText, EuiButtonEmpty } from '@elastic/eui';
import React, { memo, useMemo } from 'react';
import { CrumbInfo, StyledBreadcrumbs } from './panel_content_utilities';
import { StyledBreadcrumbs } from './panel_content_utilities';
import { CrumbInfo } from '../../types';

/**
* Display an error in the panel when something goes wrong and give the user a way to "retreat" back to a default state.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,6 @@ const BetaHeader = styled(`header`)`
margin-bottom: 1em;
`;

/**
* The two query parameters we read/write on to control which view the table presents:
*/
export interface CrumbInfo {
crumbId: string;
crumbEvent: string;
}

const ThemedBreadcrumbs = styled(EuiBreadcrumbs)<{ background: string; text: string }>`
&.euiBreadcrumbs {
background-color: ${(props) => props.background};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { FormattedMessage } from 'react-intl';
import { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list';
import * as selectors from '../../store/selectors';
import * as event from '../../../../common/endpoint/models/event';
import { CrumbInfo, formatDate, StyledBreadcrumbs } from './panel_content_utilities';
import { formatDate, StyledBreadcrumbs } from './panel_content_utilities';
import {
processPath,
processPid,
Expand All @@ -31,6 +31,7 @@ import {
import { CubeForProcess } from './cube_for_process';
import { ResolverEvent } from '../../../../common/endpoint/types';
import { useResolverTheme } from '../assets';
import { CrumbInfo } from '../../types';

const StyledDescriptionList = styled(EuiDescriptionList)`
&.euiDescriptionList.euiDescriptionList--column dt.euiDescriptionList__title.desc-title {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,13 @@ import { EuiTitle, EuiSpacer, EuiText, EuiButtonEmpty, EuiHorizontalRule } from
import { useSelector } from 'react-redux';
import { FormattedMessage } from 'react-intl';
import styled from 'styled-components';
import {
CrumbInfo,
formatDate,
StyledBreadcrumbs,
BoldCode,
StyledTime,
} from './panel_content_utilities';
import { formatDate, StyledBreadcrumbs, BoldCode, StyledTime } from './panel_content_utilities';
import * as event from '../../../../common/endpoint/models/event';
import { ResolverEvent, ResolverNodeStats } from '../../../../common/endpoint/types';
import * as selectors from '../../store/selectors';
import { useResolverDispatch } from '../use_resolver_dispatch';
import { RelatedEventLimitWarning } from '../limit_warnings';
import { CrumbInfo } from '../../types';

/**
* This view presents a list of related events of a given type for a given process.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ import { useSelector } from 'react-redux';
import styled from 'styled-components';
import * as event from '../../../../common/endpoint/models/event';
import * as selectors from '../../store/selectors';
import { CrumbInfo, formatter, StyledBreadcrumbs } from './panel_content_utilities';
import { formatter, StyledBreadcrumbs } from './panel_content_utilities';
import { useResolverDispatch } from '../use_resolver_dispatch';
import { SideEffectContext } from '../side_effect_context';
import { CubeForProcess } from './cube_for_process';
import { SafeResolverEvent } from '../../../../common/endpoint/types';
import { LimitWarning } from '../limit_warnings';
import { CrumbInfo } from '../../types';

const StyledLimitWarning = styled(LimitWarning)`
flex-flow: row wrap;
Expand Down
Loading

0 comments on commit 4294232

Please sign in to comment.