Skip to content

Commit

Permalink
[SECURITY SOLUTION] Investigate EQL signal in timeline (#79049) (#79606)
Browse files Browse the repository at this point in the history
* fix template timeline for rule

* fix moving column with linkfield by giving back the browserfield

* leftover from investigate timeline with template from rule

* add visualization for eql sequences in timeline + allow eql investigate to timeline through signal.group.id

* bug fix of column in eventviewer

* review I

* review II

* fix bug - Columns dynamically added to timeline indicate no data

* fix pagination to work as attempted by elastic search

* no tweak on pagination timeline

* fix snapshot

* reset activePage to 0 when changing indexNames

* remove last page when we are not sure if it is really the last page

* update activePage when resetting it by searchParameter

* review bug on the last commit

Co-authored-by: Kibana Machine <[email protected]>

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
XavierM and kibanamachine authored Oct 6, 2020
1 parent f627b14 commit 684128b
Show file tree
Hide file tree
Showing 38 changed files with 333 additions and 252 deletions.
1 change: 1 addition & 0 deletions x-pack/plugins/security_solution/common/ecs/rule/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,5 @@ export interface RuleEcs {
updated_by?: string[];
version?: string[];
note?: string[];
building_block_type?: string[];
}
3 changes: 3 additions & 0 deletions x-pack/plugins/security_solution/common/ecs/signal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ export interface SignalEcs {
rule?: RuleEcs;
original_time?: string[];
status?: string[];
group?: {
id?: string[];
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common';
import { Ecs } from '../../../../ecs';
import { CursorType, Inspect, Maybe, PageInfoPaginated } from '../../../common';
import { CursorType, Inspect, Maybe, PaginationInputPaginated } from '../../../common';
import { TimelineRequestOptionsPaginated } from '../..';

export interface TimelineEdges {
Expand All @@ -29,7 +29,7 @@ export interface TimelineNonEcsData {
export interface TimelineEventsAllStrategyResponse extends IEsSearchResponse {
edges: TimelineEdges[];
totalCount: number;
pageInfo: PageInfoPaginated;
pageInfo: Pick<PaginationInputPaginated, 'activePage' | 'querySize'>;
inspect?: Maybe<Inspect>;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,7 @@ import {
TimelineEventsLastEventTimeRequestOptions,
TimelineEventsLastEventTimeStrategyResponse,
} from './events';
import {
DocValueFields,
PaginationInput,
PaginationInputPaginated,
TimerangeInput,
SortField,
} from '../common';
import { DocValueFields, PaginationInputPaginated, TimerangeInput, SortField } from '../common';

export * from './events';

Expand All @@ -34,14 +28,9 @@ export interface TimelineRequestBasicOptions extends IEsSearchRequest {
factoryQueryType?: TimelineFactoryQueryTypes;
}

export interface TimelineRequestOptions<Field = string> extends TimelineRequestBasicOptions {
pagination: PaginationInput;
sort: SortField<Field>;
}

export interface TimelineRequestOptionsPaginated<Field = string>
extends TimelineRequestBasicOptions {
pagination: PaginationInputPaginated;
pagination: Pick<PaginationInputPaginated, 'activePage' | 'querySize'>;
sort: SortField<Field>;
}

Expand Down
6 changes: 5 additions & 1 deletion x-pack/plugins/security_solution/public/app/home/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ const HomePageComponent: React.FC<HomePageProps> = ({ children }) => {
);
const [showTimeline] = useShowTimeline();

const { browserFields, indexPattern, indicesExist } = useSourcererScope();
const { browserFields, indexPattern, indicesExist } = useSourcererScope(
subPluginId.current === DETECTIONS_SUB_PLUGIN_ID
? SourcererScopeName.detections
: SourcererScopeName.default
);
// side effect: this will attempt to upgrade the endpoint package if it is not up to date
// this will run when a user navigates to the Security Solution app and when they navigate between
// tabs in the app. This is useful for keeping the endpoint package as up to date as possible until
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,7 @@ export const DragDropContextWrapperComponent = React.memo<Props & PropsFromRedux
}
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[dataProviders, activeTimelineDataProviders, browserFields]
[activeTimelineDataProviders, browserFields, dataProviders, dispatch, onAddedToTimeline]
);
return (
<DragDropContext onDragEnd={onDragEnd} onBeforeCapture={onBeforeCapture} sensors={sensors}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { DropResult } from 'react-beautiful-dnd';
import { Dispatch } from 'redux';
import { ActionCreator } from 'typescript-fsa';

import { alertsHeaders } from '../../../detections/components/alerts_table/default_config';
import { BrowserField, BrowserFields, getAllFieldsByName } from '../../containers/source';
import { dragAndDropActions } from '../../store/actions';
import { IdToDataProvider } from '../../store/drag_and_drop/model';
Expand All @@ -17,6 +18,7 @@ import { timelineActions } from '../../../timelines/store/timeline';
import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants';
import { addContentToTimeline } from '../../../timelines/components/timeline/data_providers/helpers';
import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider';
import { TimelineId } from '../../../../common/types/timeline';

export const draggableIdPrefix = 'draggableId';

Expand Down Expand Up @@ -197,6 +199,10 @@ export const addFieldToTimelineColumns = ({
const fieldId = getFieldIdFromDraggable(result);
const allColumns = getAllFieldsByName(browserFields);
const column = allColumns[fieldId];
const initColumnHeader =
timelineId === TimelineId.detectionsPage || timelineId === TimelineId.detectionsRulesDetailsPage
? alertsHeaders.find((c) => c.id === fieldId) ?? {}
: {};

if (column != null) {
dispatch(
Expand All @@ -211,6 +217,7 @@ export const addFieldToTimelineColumns = ({
type: column.type,
aggregatable: column.aggregatable,
width: DEFAULT_COLUMN_MIN_WIDTH,
...initColumnHeader,
},
id: timelineId,
index: result.destination != null ? result.destination.index : 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,6 @@ const ProviderContainerComponent = styled.div<ProviderContainerProps>`
.${STATEFUL_EVENT_CSS_CLASS_NAME}:hover &,
tr:hover & {
background-color: ${({ theme }) => theme.eui.euiColorLightShade};
&::before {
background-image: linear-gradient(
135deg,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
import { isEmpty, union } from 'lodash/fp';
import { isEmpty } from 'lodash/fp';
import React, { useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
import deepEqual from 'fast-deep-equal';
Expand Down Expand Up @@ -190,14 +190,10 @@ const EventsViewerComponent: React.FC<Props> = ({
[isLoadingIndexPattern, combinedQueries, start, end]
);

const fields = useMemo(
() =>
union(
columnsHeader.map((c) => c.id),
queryFields ?? []
),
[columnsHeader, queryFields]
);
const fields = useMemo(() => [...columnsHeader.map((c) => c.id), ...(queryFields ?? [])], [
columnsHeader,
queryFields,
]);

const sortField = useMemo(
() => ({
Expand Down Expand Up @@ -311,9 +307,7 @@ const EventsViewerComponent: React.FC<Props> = ({
itemsPerPageOptions={itemsPerPageOptions}
onChangeItemsPerPage={onChangeItemsPerPage}
onChangePage={loadPage}
serverSideEventCount={totalCountMinusDeleted}
showMorePagesIndicator={pageInfo.showMorePagesIndicator}
totalCount={pageInfo.fakeTotalCount}
totalCount={totalCountMinusDeleted}
/>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@ export const getThresholdAggregationDataProvider = (
];
};

export const isEqlRule = (ecsData: Ecs) =>
ecsData.signal?.rule?.type?.length && ecsData.signal?.rule?.type[0] === 'eql';

export const isThresholdRule = (ecsData: Ecs) =>
ecsData.signal?.rule?.type?.length && ecsData.signal?.rule?.type[0] === 'threshold';

export const sendAlertToTimelineAction = async ({
apolloClient,
createTimeline,
Expand All @@ -158,13 +164,12 @@ export const sendAlertToTimelineAction = async ({
updateTimelineIsLoading,
searchStrategyClient,
}: SendAlertToTimelineActionProps) => {
let openAlertInBasicTimeline = true;
const noteContent = ecsData.signal?.rule?.note != null ? ecsData.signal?.rule?.note[0] : '';
const timelineId =
ecsData.signal?.rule?.timeline_id != null ? ecsData.signal?.rule?.timeline_id[0] : '';
const { to, from } = determineToAndFrom({ ecsData });

if (timelineId !== '' && apolloClient != null) {
if (!isEmpty(timelineId) && apolloClient != null) {
try {
updateTimelineIsLoading({ id: TimelineId.active, isLoading: true });
const [responseTimeline, eventDataResp] = await Promise.all([
Expand All @@ -173,6 +178,7 @@ export const sendAlertToTimelineAction = async ({
fetchPolicy: 'no-cache',
variables: {
id: timelineId,
timelineType: TimelineType.template,
},
}),
searchStrategyClient.search<
Expand All @@ -195,7 +201,6 @@ export const sendAlertToTimelineAction = async ({
const eventData: TimelineEventsDetailsItem[] = getOr([], 'data', eventDataResp);
if (!isEmpty(resultingTimeline)) {
const timelineTemplate: TimelineResult = omitTypenameInTimeline(resultingTimeline);
openAlertInBasicTimeline = false;
const { timeline, notes } = formatTimelineResultToModel(
timelineTemplate,
true,
Expand Down Expand Up @@ -250,16 +255,11 @@ export const sendAlertToTimelineAction = async ({
});
}
} catch {
openAlertInBasicTimeline = true;
updateTimelineIsLoading({ id: TimelineId.active, isLoading: false });
}
}

if (
ecsData.signal?.rule?.type?.length &&
ecsData.signal?.rule?.type[0] === 'threshold' &&
openAlertInBasicTimeline
) {
if (isThresholdRule(ecsData)) {
return createTimeline({
from,
notes: null,
Expand Down Expand Up @@ -312,26 +312,44 @@ export const sendAlertToTimelineAction = async ({
ruleNote: noteContent,
});
} else {
let dataProviders = [
{
and: [],
id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-alert-id-${ecsData._id}`,
name: ecsData._id,
enabled: true,
excluded: false,
kqlQuery: '',
queryMatch: {
field: '_id',
value: ecsData._id,
operator: ':' as const,
},
},
];
if (isEqlRule(ecsData)) {
const signalGroupId = ecsData.signal?.group?.id?.length
? ecsData.signal?.group?.id[0]
: 'unknown-signal-group-id';
dataProviders = [
{
...dataProviders[0],
id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-alert-id-${signalGroupId}`,
queryMatch: {
field: 'signal.group.id',
value: signalGroupId,
operator: ':' as const,
},
},
];
}

return createTimeline({
from,
notes: null,
timeline: {
...timelineDefaults,
dataProviders: [
{
and: [],
id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-alert-id-${ecsData._id}`,
name: ecsData._id,
enabled: true,
excluded: false,
kqlQuery: '',
queryMatch: {
field: '_id',
value: ecsData._id,
operator: ':',
},
},
],
dataProviders,
id: TimelineId.active,
indexNames: [],
dateRange: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,17 @@ const UtilityBarFlexGroup = styled(EuiFlexGroup)`
min-width: 175px;
`;

const BuildingBlockContainer = styled(EuiFlexItem)`
background: repeating-linear-gradient(
127deg,
rgba(245, 167, 0, 0.2),
rgba(245, 167, 0, 0.2) 1px,
rgba(245, 167, 0, 0.05) 2px,
rgba(245, 167, 0, 0.05) 10px
);
padding: ${({ theme }) => `${theme.eui.paddingSizes.xs}`};
`;

const AlertsUtilityBarComponent: React.FC<AlertsUtilityBarProps> = ({
canUserCRUD,
hasIndexWrite,
Expand Down Expand Up @@ -133,7 +144,7 @@ const AlertsUtilityBarComponent: React.FC<AlertsUtilityBarProps> = ({

const UtilityBarAdditionalFiltersContent = (closePopover: () => void) => (
<UtilityBarFlexGroup direction="column">
<EuiFlexItem>
<BuildingBlockContainer>
<EuiCheckbox
id="showBuildingBlockAlertsCheckbox"
aria-label="showBuildingBlockAlerts"
Expand All @@ -146,7 +157,7 @@ const AlertsUtilityBarComponent: React.FC<AlertsUtilityBarProps> = ({
data-test-subj="showBuildingBlockAlertsCheckbox"
label={i18n.ADDITIONAL_FILTERS_ACTIONS_SHOW_BUILDING_BLOCK}
/>
</EuiFlexItem>
</BuildingBlockContainer>
</UtilityBarFlexGroup>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,9 @@ export const alertsDefaultModel: SubsetTimelineModel = {
export const requiredFieldsForActions = [
'@timestamp',
'signal.status',
'signal.group.id',
'signal.original_time',
'signal.rule.building_block_type',
'signal.rule.filters',
'signal.rule.from',
'signal.rule.language',
Expand All @@ -177,7 +179,6 @@ export const requiredFieldsForActions = [
'signal.rule.type',
'signal.original_event.kind',
'signal.original_event.module',

// Endpoint exception fields
'file.path',
'file.Ext.code_signature.subject_name',
Expand Down
Loading

0 comments on commit 684128b

Please sign in to comment.