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][Resolver] Word-break long titles in related event… #75926

Merged
merged 20 commits into from
Aug 27, 2020
Merged
Show file tree
Hide file tree
Changes from 18 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 @@ -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 @@ -11,7 +11,6 @@ import { useSelector } from 'react-redux';
import { FormattedMessage } from 'react-intl';
import styled from 'styled-components';
import {
CrumbInfo,
formatDate,
StyledBreadcrumbs,
BoldCode,
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