mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[SECURITY SOLUTION] Investigate EQL signal in timeline (#79049)
* 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 <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
e451a4dc16
commit
cf45fef4c5
38 changed files with 334 additions and 253 deletions
|
@ -41,4 +41,5 @@ export interface RuleEcs {
|
|||
updated_by?: string[];
|
||||
version?: string[];
|
||||
note?: string[];
|
||||
building_block_type?: string[];
|
||||
}
|
||||
|
|
|
@ -10,4 +10,7 @@ export interface SignalEcs {
|
|||
rule?: RuleEcs;
|
||||
original_time?: string[];
|
||||
status?: string[];
|
||||
group?: {
|
||||
id?: string[];
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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>;
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
@ -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>;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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';
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
@ -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(
|
||||
() => ({
|
||||
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
@ -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([
|
||||
|
@ -173,6 +178,7 @@ export const sendAlertToTimelineAction = async ({
|
|||
fetchPolicy: 'no-cache',
|
||||
variables: {
|
||||
id: timelineId,
|
||||
timelineType: TimelineType.template,
|
||||
},
|
||||
}),
|
||||
searchStrategyClient.search<
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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: {
|
||||
|
|
|
@ -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,
|
||||
|
@ -133,7 +144,7 @@ const AlertsUtilityBarComponent: React.FC<AlertsUtilityBarProps> = ({
|
|||
|
||||
const UtilityBarAdditionalFiltersContent = (closePopover: () => void) => (
|
||||
<UtilityBarFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<BuildingBlockContainer>
|
||||
<EuiCheckbox
|
||||
id="showBuildingBlockAlertsCheckbox"
|
||||
aria-label="showBuildingBlockAlerts"
|
||||
|
@ -146,7 +157,7 @@ const AlertsUtilityBarComponent: React.FC<AlertsUtilityBarProps> = ({
|
|||
data-test-subj="showBuildingBlockAlertsCheckbox"
|
||||
label={i18n.ADDITIONAL_FILTERS_ACTIONS_SHOW_BUILDING_BLOCK}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</BuildingBlockContainer>
|
||||
</UtilityBarFlexGroup>
|
||||
);
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
@ -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',
|
||||
|
|
|
@ -212,6 +212,12 @@
|
|||
"ofType": { "kind": "SCALAR", "name": "ID", "ofType": null }
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "timelineType",
|
||||
"description": "",
|
||||
"type": { "kind": "ENUM", "name": "TimelineType", "ofType": null },
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
|
@ -1914,6 +1920,29 @@
|
|||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "ENUM",
|
||||
"name": "TimelineType",
|
||||
"description": "",
|
||||
"fields": null,
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
"enumValues": [
|
||||
{
|
||||
"name": "default",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "template",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "TimelineResult",
|
||||
|
@ -2953,29 +2982,6 @@
|
|||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "ENUM",
|
||||
"name": "TimelineType",
|
||||
"description": "",
|
||||
"fields": null,
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
"enumValues": [
|
||||
{
|
||||
"name": "default",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "template",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "PageInfoTimeline",
|
||||
|
|
|
@ -274,6 +274,11 @@ export enum HostPolicyResponseActionStatus {
|
|||
warning = 'warning',
|
||||
}
|
||||
|
||||
export enum TimelineType {
|
||||
default = 'default',
|
||||
template = 'template',
|
||||
}
|
||||
|
||||
export enum DataProviderType {
|
||||
default = 'default',
|
||||
template = 'template',
|
||||
|
@ -301,11 +306,6 @@ export enum TimelineStatus {
|
|||
immutable = 'immutable',
|
||||
}
|
||||
|
||||
export enum TimelineType {
|
||||
default = 'default',
|
||||
template = 'template',
|
||||
}
|
||||
|
||||
export enum SortFieldTimeline {
|
||||
title = 'title',
|
||||
description = 'description',
|
||||
|
@ -1599,6 +1599,8 @@ export interface SourceQueryArgs {
|
|||
}
|
||||
export interface GetOneTimelineQueryArgs {
|
||||
id: string;
|
||||
|
||||
timelineType?: Maybe<TimelineType>;
|
||||
}
|
||||
export interface GetAllTimelineQueryArgs {
|
||||
pageInfo: PageInfoTimeline;
|
||||
|
@ -2166,6 +2168,7 @@ export namespace PersistTimelineNoteMutation {
|
|||
export namespace GetOneTimeline {
|
||||
export type Variables = {
|
||||
id: string;
|
||||
timelineType?: Maybe<TimelineType>;
|
||||
};
|
||||
|
||||
export type Query = {
|
||||
|
|
|
@ -110,5 +110,6 @@ export const getInspectResponse = <T extends FactoryQueryTypes>(
|
|||
prevResponse: InspectResponse
|
||||
): InspectResponse => ({
|
||||
dsl: response?.inspect?.dsl ?? prevResponse?.dsl ?? [],
|
||||
response: response != null ? [JSON.stringify(response, null, 2)] : prevResponse?.response,
|
||||
response:
|
||||
response != null ? [JSON.stringify(response.rawResponse, null, 2)] : prevResponse?.response,
|
||||
});
|
||||
|
|
|
@ -35,6 +35,7 @@ import { OnUpdateColumns } from '../timeline/events';
|
|||
import { TruncatableText } from '../../../common/components/truncatable_text';
|
||||
import { FieldName } from './field_name';
|
||||
import * as i18n from './translations';
|
||||
import { getAlertColumnHeader } from './helpers';
|
||||
|
||||
const TypeIcon = styled(EuiIcon)`
|
||||
margin-left: 5px;
|
||||
|
@ -127,6 +128,7 @@ export const getFieldItems = ({
|
|||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: field.name || '',
|
||||
width: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
...getAlertColumnHeader(timelineId, field.name || ''),
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -7,8 +7,10 @@
|
|||
import { EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { filter, get, pickBy } from 'lodash/fp';
|
||||
import styled from 'styled-components';
|
||||
import { TimelineId } from '../../../../common/types/timeline';
|
||||
|
||||
import { BrowserField, BrowserFields } from '../../../common/containers/source';
|
||||
import { alertsHeaders } from '../../../detections/components/alerts_table/default_config';
|
||||
import {
|
||||
DEFAULT_CATEGORY_NAME,
|
||||
defaultHeaders,
|
||||
|
@ -141,3 +143,8 @@ export const mergeBrowserFieldsWithDefaultCategory = (
|
|||
fieldIds: defaultHeaders.map((header) => header.id),
|
||||
}),
|
||||
});
|
||||
|
||||
export const getAlertColumnHeader = (timelineId: string, fieldId: string) =>
|
||||
timelineId === TimelineId.detectionsPage || timelineId === TimelineId.detectionsRulesDetailsPage
|
||||
? alertsHeaders.find((c) => c.id === fieldId) ?? {}
|
||||
: {};
|
||||
|
|
|
@ -25,21 +25,21 @@ NoteContainer.displayName = 'NoteContainer';
|
|||
interface NoteCardsCompProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
const NoteCardsCompContainer = styled(EuiPanel)`
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
`;
|
||||
NoteCardsCompContainer.displayName = 'NoteCardsCompContainer';
|
||||
|
||||
const NoteCardsComp = React.memo<NoteCardsCompProps>(({ children }) => (
|
||||
<EuiPanel
|
||||
data-test-subj="note-cards"
|
||||
hasShadow={false}
|
||||
paddingSize="none"
|
||||
style={{ border: 'none' }}
|
||||
>
|
||||
<NoteCardsCompContainer data-test-subj="note-cards" hasShadow={false} paddingSize="none">
|
||||
{children}
|
||||
</EuiPanel>
|
||||
</NoteCardsCompContainer>
|
||||
));
|
||||
NoteCardsComp.displayName = 'NoteCardsComp';
|
||||
|
||||
const NotesContainer = styled(EuiFlexGroup)`
|
||||
padding: 0 5px;
|
||||
margin-bottom: 5px;
|
||||
`;
|
||||
NotesContainer.displayName = 'NotesContainer';
|
||||
|
|
|
@ -95,9 +95,9 @@ export const EventColumnView = React.memo<Props>(
|
|||
toggleShowNotes,
|
||||
updateNote,
|
||||
}) => {
|
||||
const { eventType: timelineEventType, timelineType, status } = useShallowEqualSelector<
|
||||
TimelineModel
|
||||
>((state) => state.timeline.timelineById[timelineId]);
|
||||
const { timelineType, status } = useShallowEqualSelector<TimelineModel>(
|
||||
(state) => state.timeline.timelineById[timelineId]
|
||||
);
|
||||
|
||||
const handlePinClicked = useCallback(
|
||||
() =>
|
||||
|
@ -151,17 +151,13 @@ export const EventColumnView = React.memo<Props>(
|
|||
/>,
|
||||
]
|
||||
: []),
|
||||
...(timelineEventType !== 'raw'
|
||||
? [
|
||||
<AlertContextMenu
|
||||
key="alert-context-menu"
|
||||
ecsRowData={ecsData}
|
||||
timelineId={timelineId}
|
||||
disabled={eventType !== 'signal'}
|
||||
refetch={refetch}
|
||||
/>,
|
||||
]
|
||||
: []),
|
||||
<AlertContextMenu
|
||||
key="alert-context-menu"
|
||||
ecsRowData={ecsData}
|
||||
timelineId={timelineId}
|
||||
disabled={eventType !== 'signal'}
|
||||
refetch={refetch}
|
||||
/>,
|
||||
],
|
||||
[
|
||||
associateNote,
|
||||
|
@ -178,7 +174,6 @@ export const EventColumnView = React.memo<Props>(
|
|||
showNotes,
|
||||
status,
|
||||
timelineId,
|
||||
timelineEventType,
|
||||
timelineType,
|
||||
toggleShowNotes,
|
||||
updateNote,
|
||||
|
|
|
@ -33,7 +33,7 @@ import { EventsTrGroup, EventsTrSupplement, EventsTrSupplementContainer } from '
|
|||
import { ColumnRenderer } from '../renderers/column_renderer';
|
||||
import { getRowRenderer } from '../renderers/get_row_renderer';
|
||||
import { RowRenderer } from '../renderers/row_renderer';
|
||||
import { getEventType } from '../helpers';
|
||||
import { isEventBuildingBlockType, getEventType } from '../helpers';
|
||||
import { NoteCards } from '../../../notes/note_cards';
|
||||
import { useEventDetailsWidthContext } from '../../../../../common/components/events_viewer/event_details_width_context';
|
||||
import { EventColumnView } from './event_column_view';
|
||||
|
@ -183,6 +183,7 @@ const StatefulEventComponent: React.FC<Props> = ({
|
|||
className={STATEFUL_EVENT_CSS_CLASS_NAME}
|
||||
data-test-subj="event"
|
||||
eventType={getEventType(event.ecs)}
|
||||
isBuildingBlockType={isEventBuildingBlockType(event.ecs)}
|
||||
showLeftBorder={!isEventViewer}
|
||||
ref={divElement}
|
||||
>
|
||||
|
|
|
@ -103,6 +103,9 @@ export const getEventIdToDataMapping = (
|
|||
};
|
||||
}, {});
|
||||
|
||||
export const isEventBuildingBlockType = (event: Ecs): boolean =>
|
||||
!isEmpty(event.signal?.rule?.building_block_type);
|
||||
|
||||
/** Return eventType raw or signal */
|
||||
export const getEventType = (event: Ecs): Omit<TimelineEventsType, 'all'> => {
|
||||
if (!isEmpty(event.signal?.rule?.id)) {
|
||||
|
|
|
@ -72,15 +72,14 @@ exports[`Footer Timeline Component rendering it renders the default timeline foo
|
|||
data-test-subj="paging-control-container"
|
||||
grow={false}
|
||||
>
|
||||
<PaginationEuiFlexItem>
|
||||
<PagingControl
|
||||
activePage={0}
|
||||
data-test-subj="paging-control"
|
||||
isLoading={false}
|
||||
onPageClick={[Function]}
|
||||
totalPages={5}
|
||||
/>
|
||||
</PaginationEuiFlexItem>
|
||||
<PagingControl
|
||||
activePage={0}
|
||||
data-test-subj="paging-control"
|
||||
isLoading={false}
|
||||
onPageClick={[Function]}
|
||||
totalCount={15546}
|
||||
totalPages={7773}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
data-test-subj="last-updated-container"
|
||||
|
|
|
@ -17,7 +17,6 @@ describe('Footer Timeline Component', () => {
|
|||
const updatedAt = 1546878704036;
|
||||
const serverSideEventCount = 15546;
|
||||
const itemsCount = 2;
|
||||
const totalCount = 10;
|
||||
|
||||
describe('rendering', () => {
|
||||
test('it renders the default timeline footer', () => {
|
||||
|
@ -34,9 +33,7 @@ describe('Footer Timeline Component', () => {
|
|||
itemsPerPageOptions={[1, 5, 10, 20]}
|
||||
onChangeItemsPerPage={onChangeItemsPerPage}
|
||||
onChangePage={loadMore}
|
||||
serverSideEventCount={serverSideEventCount}
|
||||
totalCount={totalCount}
|
||||
showMorePagesIndicator
|
||||
totalCount={serverSideEventCount}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -57,9 +54,7 @@ describe('Footer Timeline Component', () => {
|
|||
itemsPerPageOptions={[1, 5, 10, 20]}
|
||||
onChangeItemsPerPage={onChangeItemsPerPage}
|
||||
onChangePage={loadMore}
|
||||
serverSideEventCount={serverSideEventCount}
|
||||
totalCount={totalCount}
|
||||
showMorePagesIndicator
|
||||
totalCount={serverSideEventCount}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -81,9 +76,7 @@ describe('Footer Timeline Component', () => {
|
|||
itemsPerPageOptions={[1, 5, 10, 20]}
|
||||
onChangeItemsPerPage={onChangeItemsPerPage}
|
||||
onChangePage={loadMore}
|
||||
serverSideEventCount={serverSideEventCount}
|
||||
totalCount={totalCount}
|
||||
showMorePagesIndicator
|
||||
totalCount={serverSideEventCount}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -95,6 +88,7 @@ describe('Footer Timeline Component', () => {
|
|||
const wrapper = shallow(
|
||||
<PagingControlComponent
|
||||
activePage={0}
|
||||
totalCount={30}
|
||||
totalPages={3}
|
||||
onPageClick={loadMore}
|
||||
isLoading={true}
|
||||
|
@ -110,6 +104,7 @@ describe('Footer Timeline Component', () => {
|
|||
const wrapper = shallow(
|
||||
<PagingControlComponent
|
||||
activePage={0}
|
||||
totalCount={30}
|
||||
totalPages={3}
|
||||
onPageClick={loadMore}
|
||||
isLoading={false}
|
||||
|
@ -133,9 +128,7 @@ describe('Footer Timeline Component', () => {
|
|||
itemsPerPageOptions={[1, 5, 10, 20]}
|
||||
onChangeItemsPerPage={onChangeItemsPerPage}
|
||||
onChangePage={loadMore}
|
||||
serverSideEventCount={serverSideEventCount}
|
||||
totalCount={totalCount}
|
||||
showMorePagesIndicator
|
||||
totalCount={serverSideEventCount}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -157,9 +150,7 @@ describe('Footer Timeline Component', () => {
|
|||
itemsPerPageOptions={[1, 5, 10, 20]}
|
||||
onChangeItemsPerPage={onChangeItemsPerPage}
|
||||
onChangePage={loadMore}
|
||||
serverSideEventCount={serverSideEventCount}
|
||||
totalCount={totalCount}
|
||||
showMorePagesIndicator
|
||||
totalCount={serverSideEventCount}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -185,9 +176,7 @@ describe('Footer Timeline Component', () => {
|
|||
itemsPerPageOptions={[1, 5, 10, 20]}
|
||||
onChangeItemsPerPage={onChangeItemsPerPage}
|
||||
onChangePage={loadMore}
|
||||
serverSideEventCount={serverSideEventCount}
|
||||
totalCount={totalCount}
|
||||
showMorePagesIndicator
|
||||
totalCount={serverSideEventCount}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -211,9 +200,7 @@ describe('Footer Timeline Component', () => {
|
|||
itemsPerPageOptions={[1, 5, 10, 20]}
|
||||
onChangeItemsPerPage={onChangeItemsPerPage}
|
||||
onChangePage={loadMore}
|
||||
serverSideEventCount={serverSideEventCount}
|
||||
totalCount={totalCount}
|
||||
showMorePagesIndicator
|
||||
totalCount={serverSideEventCount}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -239,9 +226,7 @@ describe('Footer Timeline Component', () => {
|
|||
itemsPerPageOptions={[1, 5, 10, 20]}
|
||||
onChangeItemsPerPage={onChangeItemsPerPage}
|
||||
onChangePage={loadMore}
|
||||
serverSideEventCount={serverSideEventCount}
|
||||
totalCount={totalCount}
|
||||
showMorePagesIndicator
|
||||
totalCount={serverSideEventCount}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -265,9 +250,7 @@ describe('Footer Timeline Component', () => {
|
|||
itemsPerPageOptions={[1, 5, 10, 20]}
|
||||
onChangeItemsPerPage={onChangeItemsPerPage}
|
||||
onChangePage={loadMore}
|
||||
serverSideEventCount={serverSideEventCount}
|
||||
totalCount={totalCount}
|
||||
showMorePagesIndicator
|
||||
totalCount={serverSideEventCount}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
|
|
@ -28,7 +28,6 @@ import { OnChangeItemsPerPage, OnChangePage } from '../events';
|
|||
import { LastUpdatedAt } from './last_updated';
|
||||
import * as i18n from './translations';
|
||||
import { useEventDetailsWidthContext } from '../../../../common/components/events_viewer/event_details_width_context';
|
||||
import { PaginationEuiFlexItem } from '../../../../common/components/paginated_table';
|
||||
import { useManageTimeline } from '../../manage_timeline';
|
||||
|
||||
export const isCompactFooter = (width: number): boolean => width < 600;
|
||||
|
@ -179,13 +178,23 @@ interface PagingControlProps {
|
|||
activePage: number;
|
||||
isLoading: boolean;
|
||||
onPageClick: OnChangePage;
|
||||
totalCount: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
const TimelinePaginationContainer = styled.div<{ hideLastPage: boolean }>`
|
||||
ul.euiPagination__list {
|
||||
li.euiPagination__item:last-child {
|
||||
${({ hideLastPage }) => `${hideLastPage ? 'display:none' : ''}`};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const PagingControlComponent: React.FC<PagingControlProps> = ({
|
||||
activePage,
|
||||
isLoading,
|
||||
onPageClick,
|
||||
totalCount,
|
||||
totalPages,
|
||||
}) => {
|
||||
if (isLoading) {
|
||||
|
@ -197,12 +206,14 @@ export const PagingControlComponent: React.FC<PagingControlProps> = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<EuiPagination
|
||||
data-test-subj="timeline-pagination"
|
||||
pageCount={totalPages}
|
||||
activePage={activePage}
|
||||
onPageClick={onPageClick}
|
||||
/>
|
||||
<TimelinePaginationContainer hideLastPage={totalCount > 9999}>
|
||||
<EuiPagination
|
||||
data-test-subj="timeline-pagination"
|
||||
pageCount={totalPages}
|
||||
activePage={activePage}
|
||||
onPageClick={onPageClick}
|
||||
/>
|
||||
</TimelinePaginationContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -223,8 +234,6 @@ interface FooterProps {
|
|||
itemsPerPageOptions: number[];
|
||||
onChangeItemsPerPage: OnChangeItemsPerPage;
|
||||
onChangePage: OnChangePage;
|
||||
serverSideEventCount: number;
|
||||
showMorePagesIndicator: boolean;
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
|
@ -241,8 +250,6 @@ export const FooterComponent = ({
|
|||
itemsPerPageOptions,
|
||||
onChangeItemsPerPage,
|
||||
onChangePage,
|
||||
serverSideEventCount,
|
||||
showMorePagesIndicator,
|
||||
totalCount,
|
||||
}: FooterProps) => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
@ -292,11 +299,6 @@ export const FooterComponent = ({
|
|||
totalCount,
|
||||
]);
|
||||
|
||||
const PaginationWrapper = useMemo(
|
||||
() => (showMorePagesIndicator ? PaginationEuiFlexItem : EuiFlexItem),
|
||||
[showMorePagesIndicator]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (paginationLoading && !isLoading) {
|
||||
setPaginationLoading(false);
|
||||
|
@ -347,7 +349,7 @@ export const FooterComponent = ({
|
|||
items={rowItems}
|
||||
itemsCount={itemsCount}
|
||||
onClick={onButtonClick}
|
||||
serverSideEventCount={serverSideEventCount}
|
||||
serverSideEventCount={totalCount}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
@ -373,15 +375,14 @@ export const FooterComponent = ({
|
|||
</b>
|
||||
</EuiText>
|
||||
) : (
|
||||
<PaginationWrapper>
|
||||
<PagingControl
|
||||
data-test-subj="paging-control"
|
||||
totalPages={totalPages}
|
||||
activePage={activePage}
|
||||
onPageClick={handleChangePageClick}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</PaginationWrapper>
|
||||
<PagingControl
|
||||
data-test-subj="paging-control"
|
||||
totalCount={totalCount}
|
||||
totalPages={totalPages}
|
||||
activePage={activePage}
|
||||
onPageClick={handleChangePageClick}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
|
||||
|
|
|
@ -106,14 +106,16 @@ const SearchTimelineSuperSelectComponent: React.FC<SearchTimelineSuperSelectProp
|
|||
description: t.description,
|
||||
favorite: t.favorite,
|
||||
label: t.title,
|
||||
id: t.savedObjectId,
|
||||
id: timelineType === TimelineType.template ? t.templateTimelineId : t.savedObjectId,
|
||||
key: `${t.title}-${index}`,
|
||||
title: t.title,
|
||||
checked: t.savedObjectId === timelineId ? 'on' : undefined,
|
||||
checked: [t.savedObjectId, t.templateTimelineId].includes(timelineId)
|
||||
? 'on'
|
||||
: undefined,
|
||||
} as EuiSelectableOption)
|
||||
),
|
||||
],
|
||||
[hideUntitled, timelineId]
|
||||
[hideUntitled, timelineId, timelineType]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -173,14 +173,23 @@ export const EventsTbody = styled.div.attrs(({ className = '' }) => ({
|
|||
|
||||
export const EventsTrGroup = styled.div.attrs(({ className = '' }) => ({
|
||||
className: `siemEventsTable__trGroup ${className}`,
|
||||
}))<{ className?: string; eventType: Omit<TimelineEventsType, 'all'>; showLeftBorder: boolean }>`
|
||||
}))<{
|
||||
className?: string;
|
||||
eventType: Omit<TimelineEventsType, 'all'>;
|
||||
isBuildingBlockType: boolean;
|
||||
showLeftBorder: boolean;
|
||||
}>`
|
||||
border-bottom: ${({ theme }) => theme.eui.euiBorderWidthThin} solid
|
||||
${({ theme }) => theme.eui.euiColorLightShade};
|
||||
${({ theme, eventType, showLeftBorder }) =>
|
||||
${({ theme, eventType, isBuildingBlockType, showLeftBorder }) =>
|
||||
showLeftBorder
|
||||
? `border-left: 4px solid
|
||||
${eventType === 'raw' ? theme.eui.euiColorLightShade : theme.eui.euiColorWarning}`
|
||||
: ''};
|
||||
${({ isBuildingBlockType, showLeftBorder }) =>
|
||||
isBuildingBlockType
|
||||
? `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);`
|
||||
: ''};
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.eui.euiTableHoverColor};
|
||||
|
@ -207,7 +216,13 @@ export const EventsTrSupplement = styled.div.attrs(({ className = '' }) => ({
|
|||
}))<{ className: string }>`
|
||||
font-size: ${({ theme }) => theme.eui.euiFontSizeXS};
|
||||
line-height: ${({ theme }) => theme.eui.euiLineHeight};
|
||||
padding: 0 ${({ theme }) => theme.eui.paddingSizes.xs} 0 52px;
|
||||
padding: 0 ${({ theme }) => theme.eui.paddingSizes.m};
|
||||
.euiAccordion + div {
|
||||
background-color: ${({ theme }) => theme.eui.euiColorEmptyShade};
|
||||
padding: 0 ${({ theme }) => theme.eui.paddingSizes.s};
|
||||
border: 1px solid ${({ theme }) => theme.eui.euiColorLightShade};
|
||||
border-radius: ${({ theme }) => theme.eui.paddingSizes.xs};
|
||||
}
|
||||
`;
|
||||
|
||||
export const EventsTdGroupActions = styled.div.attrs(({ className = '' }) => ({
|
||||
|
|
|
@ -301,9 +301,7 @@ export const TimelineComponent: React.FC<Props> = ({
|
|||
itemsPerPageOptions={itemsPerPageOptions}
|
||||
onChangeItemsPerPage={onChangeItemsPerPage}
|
||||
onChangePage={loadPage}
|
||||
serverSideEventCount={totalCount}
|
||||
showMorePagesIndicator={pageInfo.showMorePagesIndicator}
|
||||
totalCount={pageInfo.fakeTotalCount}
|
||||
totalCount={totalCount}
|
||||
/>
|
||||
</StyledEuiFlyoutFooter>
|
||||
)
|
||||
|
|
|
@ -93,6 +93,7 @@ export const getAllTimeline = memoizeOne(
|
|||
updated: timeline.updated,
|
||||
updatedBy: timeline.updatedBy,
|
||||
timelineType: timeline.timelineType ?? TimelineType.default,
|
||||
templateTimelineId: timeline.templateTimelineId,
|
||||
}))
|
||||
);
|
||||
|
||||
|
|
|
@ -15,13 +15,12 @@ import { inputsModel } from '../../common/store';
|
|||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { createFilter } from '../../common/containers/helpers';
|
||||
import { DocValueFields } from '../../common/containers/query_template';
|
||||
import { generateTablePaginationOptions } from '../../common/components/paginated_table/helpers';
|
||||
import { timelineActions } from '../../timelines/store/timeline';
|
||||
import { detectionsTimelineIds, skipQueryForDetectionsPage } from './helpers';
|
||||
import { getInspectResponse } from '../../helpers';
|
||||
import {
|
||||
Direction,
|
||||
PageInfoPaginated,
|
||||
PaginationInputPaginated,
|
||||
TimelineEventsQueries,
|
||||
TimelineEventsAllStrategyResponse,
|
||||
TimelineEventsAllRequestOptions,
|
||||
|
@ -37,7 +36,7 @@ export interface TimelineArgs {
|
|||
id: string;
|
||||
inspect: InspectResponse;
|
||||
loadPage: LoadPage;
|
||||
pageInfo: PageInfoPaginated;
|
||||
pageInfo: Pick<PaginationInputPaginated, 'activePage' | 'querySize'>;
|
||||
refetch: inputsModel.Refetch;
|
||||
totalCount: number;
|
||||
updatedAt: number;
|
||||
|
@ -62,6 +61,10 @@ const getTimelineEvents = (timelineEdges: TimelineEdges[]): TimelineItem[] =>
|
|||
timelineEdges.map((e: TimelineEdges) => e.node);
|
||||
|
||||
const ID = 'timelineEventsQuery';
|
||||
const initSortDefault = {
|
||||
field: '@timestamp',
|
||||
direction: Direction.asc,
|
||||
};
|
||||
|
||||
export const useTimelineEvents = ({
|
||||
docValueFields,
|
||||
|
@ -72,10 +75,7 @@ export const useTimelineEvents = ({
|
|||
filterQuery,
|
||||
startDate,
|
||||
limit,
|
||||
sort = {
|
||||
field: '@timestamp',
|
||||
direction: Direction.asc,
|
||||
},
|
||||
sort = initSortDefault,
|
||||
skip = false,
|
||||
}: UseTimelineEventsProps): [boolean, TimelineArgs] => {
|
||||
const dispatch = useDispatch();
|
||||
|
@ -87,7 +87,7 @@ export const useTimelineEvents = ({
|
|||
const [timelineRequest, setTimelineRequest] = useState<TimelineEventsAllRequestOptions | null>(
|
||||
!skip
|
||||
? {
|
||||
fields,
|
||||
fields: [],
|
||||
fieldRequested: fields,
|
||||
filterQuery: createFilter(filterQuery),
|
||||
id: ID,
|
||||
|
@ -96,7 +96,10 @@ export const useTimelineEvents = ({
|
|||
from: startDate,
|
||||
to: endDate,
|
||||
},
|
||||
pagination: generateTablePaginationOptions(activePage, limit),
|
||||
pagination: {
|
||||
activePage,
|
||||
querySize: limit,
|
||||
},
|
||||
sort,
|
||||
defaultIndex: indexNames,
|
||||
docValueFields: docValueFields ?? [],
|
||||
|
@ -130,8 +133,7 @@ export const useTimelineEvents = ({
|
|||
totalCount: -1,
|
||||
pageInfo: {
|
||||
activePage: 0,
|
||||
fakeTotalCount: 0,
|
||||
showMorePagesIndicator: false,
|
||||
querySize: 0,
|
||||
},
|
||||
events: [],
|
||||
loadPage: wrappedLoadPage,
|
||||
|
@ -205,31 +207,60 @@ export const useTimelineEvents = ({
|
|||
}
|
||||
|
||||
setTimelineRequest((prevRequest) => {
|
||||
const myRequest = {
|
||||
...(prevRequest ?? {
|
||||
fields,
|
||||
fieldRequested: fields,
|
||||
id,
|
||||
factoryQueryType: TimelineEventsQueries.all,
|
||||
}),
|
||||
const prevSearchParameters = {
|
||||
defaultIndex: prevRequest?.defaultIndex ?? [],
|
||||
filterQuery: prevRequest?.filterQuery ?? '',
|
||||
querySize: prevRequest?.pagination.querySize ?? 0,
|
||||
sort: prevRequest?.sort ?? initSortDefault,
|
||||
timerange: prevRequest?.timerange ?? {},
|
||||
};
|
||||
|
||||
const currentSearchParameters = {
|
||||
defaultIndex: indexNames,
|
||||
docValueFields: docValueFields ?? [],
|
||||
filterQuery: createFilter(filterQuery),
|
||||
id: ID,
|
||||
pagination: generateTablePaginationOptions(activePage, limit),
|
||||
querySize: limit,
|
||||
sort,
|
||||
timerange: {
|
||||
interval: '12h',
|
||||
from: startDate,
|
||||
to: endDate,
|
||||
},
|
||||
sort,
|
||||
};
|
||||
|
||||
const newActivePage = deepEqual(prevSearchParameters, currentSearchParameters)
|
||||
? activePage
|
||||
: 0;
|
||||
|
||||
const currentRequest = {
|
||||
defaultIndex: indexNames,
|
||||
docValueFields: docValueFields ?? [],
|
||||
factoryQueryType: TimelineEventsQueries.all,
|
||||
fieldRequested: fields,
|
||||
fields: [],
|
||||
filterQuery: createFilter(filterQuery),
|
||||
id,
|
||||
pagination: {
|
||||
activePage: newActivePage,
|
||||
querySize: limit,
|
||||
},
|
||||
sort,
|
||||
timerange: {
|
||||
interval: '12h',
|
||||
from: startDate,
|
||||
to: endDate,
|
||||
},
|
||||
};
|
||||
|
||||
if (activePage !== newActivePage) {
|
||||
setActivePage(newActivePage);
|
||||
}
|
||||
|
||||
if (
|
||||
!skip &&
|
||||
!skipQueryForDetectionsPage(id, indexNames) &&
|
||||
!deepEqual(prevRequest, myRequest)
|
||||
!deepEqual(prevRequest, currentRequest)
|
||||
) {
|
||||
return myRequest;
|
||||
return currentRequest;
|
||||
}
|
||||
return prevRequest;
|
||||
});
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
import gql from 'graphql-tag';
|
||||
|
||||
export const oneTimelineQuery = gql`
|
||||
query GetOneTimeline($id: ID!) {
|
||||
getOneTimeline(id: $id) {
|
||||
query GetOneTimeline($id: ID!, $timelineType: TimelineType) {
|
||||
getOneTimeline(id: $id, timelineType: $timelineType) {
|
||||
savedObjectId
|
||||
columns {
|
||||
aggregatable
|
||||
|
|
|
@ -44,7 +44,7 @@ export const createTimelineResolvers = (
|
|||
} => ({
|
||||
Query: {
|
||||
async getOneTimeline(root, args, { req }) {
|
||||
return libs.timeline.getTimeline(req, args.id);
|
||||
return libs.timeline.getTimeline(req, args.id, args.timelineType);
|
||||
},
|
||||
async getAllTimeline(root, args, { req }) {
|
||||
return libs.timeline.getAllTimeline(
|
||||
|
|
|
@ -317,7 +317,7 @@ export const timelineSchema = gql`
|
|||
#########################
|
||||
|
||||
extend type Query {
|
||||
getOneTimeline(id: ID!): TimelineResult!
|
||||
getOneTimeline(id: ID!, timelineType: TimelineType): TimelineResult!
|
||||
getAllTimeline(pageInfo: PageInfoTimeline!, search: String, sort: SortTimeline, onlyUserFavorite: Boolean, timelineType: TimelineType, status: TimelineStatus): ResponseTimelines!
|
||||
}
|
||||
|
||||
|
|
|
@ -276,6 +276,11 @@ export enum HostPolicyResponseActionStatus {
|
|||
warning = 'warning',
|
||||
}
|
||||
|
||||
export enum TimelineType {
|
||||
default = 'default',
|
||||
template = 'template',
|
||||
}
|
||||
|
||||
export enum DataProviderType {
|
||||
default = 'default',
|
||||
template = 'template',
|
||||
|
@ -303,11 +308,6 @@ export enum TimelineStatus {
|
|||
immutable = 'immutable',
|
||||
}
|
||||
|
||||
export enum TimelineType {
|
||||
default = 'default',
|
||||
template = 'template',
|
||||
}
|
||||
|
||||
export enum SortFieldTimeline {
|
||||
title = 'title',
|
||||
description = 'description',
|
||||
|
@ -1601,6 +1601,8 @@ export interface SourceQueryArgs {
|
|||
}
|
||||
export interface GetOneTimelineQueryArgs {
|
||||
id: string;
|
||||
|
||||
timelineType?: Maybe<TimelineType>;
|
||||
}
|
||||
export interface GetAllTimelineQueryArgs {
|
||||
pageInfo: PageInfoTimeline;
|
||||
|
@ -1838,6 +1840,8 @@ export namespace QueryResolvers {
|
|||
> = Resolver<R, Parent, TContext, GetOneTimelineArgs>;
|
||||
export interface GetOneTimelineArgs {
|
||||
id: string;
|
||||
|
||||
timelineType?: Maybe<TimelineType>;
|
||||
}
|
||||
|
||||
export type GetAllTimelineResolver<
|
||||
|
|
|
@ -58,7 +58,11 @@ export interface ResponseTemplateTimeline {
|
|||
}
|
||||
|
||||
export interface Timeline {
|
||||
getTimeline: (request: FrameworkRequest, timelineId: string) => Promise<TimelineSavedObject>;
|
||||
getTimeline: (
|
||||
request: FrameworkRequest,
|
||||
timelineId: string,
|
||||
timelineType?: TimelineTypeLiteralWithNull
|
||||
) => Promise<TimelineSavedObject>;
|
||||
|
||||
getAllTimeline: (
|
||||
request: FrameworkRequest,
|
||||
|
@ -95,9 +99,27 @@ export interface Timeline {
|
|||
|
||||
export const getTimeline = async (
|
||||
request: FrameworkRequest,
|
||||
timelineId: string
|
||||
timelineId: string,
|
||||
timelineType: TimelineTypeLiteralWithNull = TimelineType.default
|
||||
): Promise<TimelineSavedObject> => {
|
||||
return getSavedTimeline(request, timelineId);
|
||||
let timelineIdToUse = timelineId;
|
||||
try {
|
||||
if (timelineType === TimelineType.template) {
|
||||
const options = {
|
||||
type: timelineSavedObjectType,
|
||||
perPage: 1,
|
||||
page: 1,
|
||||
filter: `siem-ui-timeline.attributes.templateTimelineId: ${timelineId}`,
|
||||
};
|
||||
const result = await getAllSavedTimeline(request, options);
|
||||
if (result.totalCount === 1) {
|
||||
timelineIdToUse = result.timeline[0].savedObjectId;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// TO DO, we need to bring the logger here
|
||||
}
|
||||
return getSavedTimeline(request, timelineIdToUse);
|
||||
};
|
||||
|
||||
export const getTimelineByTemplateTimelineId = async (
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
export const TIMELINE_EVENTS_FIELDS = [
|
||||
'@timestamp',
|
||||
'signal.status',
|
||||
'signal.group.id',
|
||||
'signal.original_time',
|
||||
'signal.rule.filters',
|
||||
'signal.rule.from',
|
||||
|
@ -136,6 +137,7 @@ export const TIMELINE_EVENTS_FIELDS = [
|
|||
'signal.rule.note',
|
||||
'signal.rule.threshold',
|
||||
'signal.rule.exceptions_list',
|
||||
'signal.rule.building_block_type',
|
||||
'suricata.eve.proto',
|
||||
'suricata.eve.flow_id',
|
||||
'suricata.eve.alert.signature',
|
||||
|
|
|
@ -11,8 +11,7 @@ import { toArray } from '../../../../helpers/to_array';
|
|||
export const formatTimelineData = (
|
||||
dataFields: readonly string[],
|
||||
ecsFields: readonly string[],
|
||||
hit: EventHit,
|
||||
fieldMap: Readonly<Record<string, string>>
|
||||
hit: EventHit
|
||||
) =>
|
||||
uniq([...ecsFields, ...dataFields]).reduce<TimelineEdges>(
|
||||
(flattenedFields, fieldName) => {
|
||||
|
@ -25,14 +24,7 @@ export const formatTimelineData = (
|
|||
flattenedFields.cursor.value = hit.sort[0];
|
||||
flattenedFields.cursor.tiebreaker = hit.sort[1];
|
||||
}
|
||||
return mergeTimelineFieldsWithHit(
|
||||
fieldName,
|
||||
flattenedFields,
|
||||
fieldMap,
|
||||
hit,
|
||||
dataFields,
|
||||
ecsFields
|
||||
);
|
||||
return mergeTimelineFieldsWithHit(fieldName, flattenedFields, hit, dataFields, ecsFields);
|
||||
},
|
||||
{
|
||||
node: { ecs: { _id: '' }, data: [], _id: '', _index: '' },
|
||||
|
@ -48,13 +40,12 @@ const specialFields = ['_id', '_index', '_type', '_score'];
|
|||
const mergeTimelineFieldsWithHit = <T>(
|
||||
fieldName: string,
|
||||
flattenedFields: T,
|
||||
fieldMap: Readonly<Record<string, string>>,
|
||||
hit: { _source: {} },
|
||||
dataFields: readonly string[],
|
||||
ecsFields: readonly string[]
|
||||
) => {
|
||||
if (fieldMap[fieldName] != null || dataFields.includes(fieldName)) {
|
||||
const esField = dataFields.includes(fieldName) ? fieldName : fieldMap[fieldName];
|
||||
if (fieldName != null || dataFields.includes(fieldName)) {
|
||||
const esField = fieldName;
|
||||
if (has(esField, hit._source) || specialFields.includes(esField)) {
|
||||
const objectWithProperty = {
|
||||
node: {
|
||||
|
|
|
@ -14,10 +14,9 @@ import {
|
|||
TimelineEventsAllRequestOptions,
|
||||
TimelineEdges,
|
||||
} from '../../../../../../common/search_strategy/timeline';
|
||||
import { inspectStringifyObject, reduceFields } from '../../../../../utils/build_query';
|
||||
import { inspectStringifyObject } from '../../../../../utils/build_query';
|
||||
import { SecuritySolutionTimelineFactory } from '../../types';
|
||||
import { buildTimelineEventsAllQuery } from './query.events_all.dsl';
|
||||
import { eventFieldsMap } from '../../../../../lib/ecs_fields';
|
||||
import { TIMELINE_EVENTS_FIELDS } from './constants';
|
||||
import { formatTimelineData } from './helpers';
|
||||
|
||||
|
@ -27,10 +26,7 @@ export const timelineEventsAll: SecuritySolutionTimelineFactory<TimelineEventsQu
|
|||
throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`);
|
||||
}
|
||||
const { fieldRequested, ...queryOptions } = cloneDeep(options);
|
||||
queryOptions.fields = uniq([
|
||||
...fieldRequested,
|
||||
...reduceFields(TIMELINE_EVENTS_FIELDS, eventFieldsMap),
|
||||
]);
|
||||
queryOptions.fields = uniq([...fieldRequested, ...TIMELINE_EVENTS_FIELDS]);
|
||||
return buildTimelineEventsAllQuery(queryOptions);
|
||||
},
|
||||
parse: async (
|
||||
|
@ -38,23 +34,17 @@ export const timelineEventsAll: SecuritySolutionTimelineFactory<TimelineEventsQu
|
|||
response: IEsSearchResponse<unknown>
|
||||
): Promise<TimelineEventsAllStrategyResponse> => {
|
||||
const { fieldRequested, ...queryOptions } = cloneDeep(options);
|
||||
queryOptions.fields = uniq([
|
||||
...fieldRequested,
|
||||
...reduceFields(TIMELINE_EVENTS_FIELDS, eventFieldsMap),
|
||||
]);
|
||||
const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination;
|
||||
|
||||
queryOptions.fields = uniq([...fieldRequested, ...TIMELINE_EVENTS_FIELDS]);
|
||||
const { activePage, querySize } = options.pagination;
|
||||
const totalCount = getOr(0, 'hits.total.value', response.rawResponse);
|
||||
const hits = response.rawResponse.hits.hits;
|
||||
const edges: TimelineEdges[] = hits.splice(cursorStart, querySize - cursorStart).map((hit) =>
|
||||
const edges: TimelineEdges[] = hits.map((hit) =>
|
||||
// @ts-expect-error
|
||||
formatTimelineData(options.fieldRequested, TIMELINE_EVENTS_FIELDS, hit, eventFieldsMap)
|
||||
formatTimelineData(options.fieldRequested, TIMELINE_EVENTS_FIELDS, hit)
|
||||
);
|
||||
const inspect = {
|
||||
dsl: [inspectStringifyObject(buildTimelineEventsAllQuery(queryOptions))],
|
||||
};
|
||||
const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount;
|
||||
const showMorePagesIndicator = totalCount > fakeTotalCount;
|
||||
|
||||
return {
|
||||
...response,
|
||||
|
@ -63,8 +53,7 @@ export const timelineEventsAll: SecuritySolutionTimelineFactory<TimelineEventsQu
|
|||
totalCount,
|
||||
pageInfo: {
|
||||
activePage: activePage ?? 0,
|
||||
fakeTotalCount,
|
||||
showMorePagesIndicator,
|
||||
querySize,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
|
@ -12,11 +12,11 @@ import {
|
|||
TimelineEventsAllRequestOptions,
|
||||
} from '../../../../../../common/search_strategy';
|
||||
import { createQueryFilterClauses } from '../../../../../utils/build_query';
|
||||
import { TIMELINE_EVENTS_FIELDS } from './constants';
|
||||
|
||||
export const buildTimelineEventsAllQuery = ({
|
||||
defaultIndex,
|
||||
docValueFields,
|
||||
fields,
|
||||
filterQuery,
|
||||
pagination: { activePage, querySize },
|
||||
sort,
|
||||
|
@ -68,7 +68,7 @@ export const buildTimelineEventsAllQuery = ({
|
|||
size: querySize,
|
||||
track_total_hits: true,
|
||||
sort: getSortField(sort),
|
||||
_source: TIMELINE_EVENTS_FIELDS,
|
||||
_source: fields,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue