[Security Solution] Allow to configure Event Renderers settings (#69693) (#71368)

This commit is contained in:
patrykkopycinski 2020-07-10 16:54:00 +02:00 committed by GitHub
parent 1924f78471
commit 666af5d332
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
82 changed files with 2548 additions and 1078 deletions

View file

@ -4,12 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable @typescript-eslint/no-empty-interface */
/* eslint-disable @typescript-eslint/camelcase, @typescript-eslint/no-empty-interface */
import * as runtimeTypes from 'io-ts';
import { SavedObjectsClient } from 'kibana/server';
import { unionWithNullType } from '../../utility_types';
import { stringEnum, unionWithNullType } from '../../utility_types';
import { NoteSavedObject, NoteSavedObjectToReturnRuntimeType } from './note';
import { PinnedEventToReturnSavedObjectRuntimeType, PinnedEventSavedObject } from './pinned_event';
@ -164,6 +164,24 @@ export type TimelineStatusLiteralWithNull = runtimeTypes.TypeOf<
typeof TimelineStatusLiteralWithNullRt
>;
export enum RowRendererId {
auditd = 'auditd',
auditd_file = 'auditd_file',
netflow = 'netflow',
plain = 'plain',
suricata = 'suricata',
system = 'system',
system_dns = 'system_dns',
system_endgame_process = 'system_endgame_process',
system_file = 'system_file',
system_fim = 'system_fim',
system_security_event = 'system_security_event',
system_socket = 'system_socket',
zeek = 'zeek',
}
export const RowRendererIdRuntimeType = stringEnum(RowRendererId, 'RowRendererId');
/**
* Timeline template type
*/
@ -211,6 +229,7 @@ export const SavedTimelineRuntimeType = runtimeTypes.partial({
dataProviders: unionWithNullType(runtimeTypes.array(SavedDataProviderRuntimeType)),
description: unionWithNullType(runtimeTypes.string),
eventType: unionWithNullType(runtimeTypes.string),
excludedRowRendererIds: unionWithNullType(runtimeTypes.array(RowRendererIdRuntimeType)),
favorite: unionWithNullType(runtimeTypes.array(SavedFavoriteRuntimeType)),
filters: unionWithNullType(runtimeTypes.array(SavedFilterRuntimeType)),
kqlMode: unionWithNullType(runtimeTypes.string),

View file

@ -15,3 +15,14 @@ export interface DescriptionList {
export const unionWithNullType = <T extends runtimeTypes.Mixed>(type: T) =>
runtimeTypes.union([type, runtimeTypes.null]);
export const stringEnum = <T>(enumObj: T, enumName = 'enum') =>
new runtimeTypes.Type<T[keyof T], string>(
enumName,
(u): u is T[keyof T] => Object.values(enumObj).includes(u),
(u, c) =>
Object.values(enumObj).includes(u)
? runtimeTypes.success(u as T[keyof T])
: runtimeTypes.failure(u, c),
(a) => (a as unknown) as string
);

View file

@ -7,7 +7,7 @@
export const CLOSE_MODAL = '[data-test-subj="modal-inspect-close"]';
export const EVENTS_VIEWER_FIELDS_BUTTON =
'[data-test-subj="events-viewer-panel"] [data-test-subj="show-field-browser-gear"]';
'[data-test-subj="events-viewer-panel"] [data-test-subj="show-field-browser"]';
export const EVENTS_VIEWER_PANEL = '[data-test-subj="events-viewer-panel"]';

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { RowRendererId } from '../../../../common/types/timeline';
import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers';
import {
DEFAULT_COLUMN_MIN_WIDTH,
@ -69,5 +70,5 @@ export const alertsHeaders: ColumnHeaderOptions[] = [
export const alertsDefaultModel: SubsetTimelineModel = {
...timelineDefaults,
columns: alertsHeaders,
showRowRenderers: false,
excludedRowRendererIds: Object.values(RowRendererId),
};

View file

@ -15,13 +15,13 @@ import {
} from 'react-beautiful-dnd';
import { useDispatch } from 'react-redux';
import styled from 'styled-components';
import deepEqual from 'fast-deep-equal';
import { dragAndDropActions } from '../../store/drag_and_drop';
import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider';
import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../../../timelines/components/row_renderers_browser/constants';
import { TruncatableText } from '../truncatable_text';
import { WithHoverActions } from '../with_hover_actions';
import { DraggableWrapperHoverContent, useGetTimelineId } from './draggable_wrapper_hover_content';
import { getDraggableId, getDroppableId } from './helpers';
import { ProviderContainer } from './provider_container';
@ -49,13 +49,27 @@ class DragDropErrorBoundary extends React.PureComponent {
}
}
const Wrapper = styled.div`
interface WrapperProps {
disabled: boolean;
}
const Wrapper = styled.div<WrapperProps>`
display: inline-block;
max-width: 100%;
[data-rbd-placeholder-context-id] {
display: none !important;
}
${({ disabled }) =>
disabled &&
`
[data-rbd-draggable-id]:hover,
.euiBadge:hover,
.euiBadge__text:hover {
cursor: default;
}
`}
`;
Wrapper.displayName = 'Wrapper';
@ -74,6 +88,7 @@ type RenderFunctionProp = (
interface Props {
dataProvider: DataProvider;
disabled?: boolean;
inline?: boolean;
render: RenderFunctionProp;
timelineId?: string;
@ -100,162 +115,169 @@ export const getStyle = (
};
};
export const DraggableWrapper = React.memo<Props>(
({ dataProvider, onFilterAdded, render, timelineId, truncate }) => {
const draggableRef = useRef<HTMLDivElement | null>(null);
const [closePopOverTrigger, setClosePopOverTrigger] = useState(false);
const [showTopN, setShowTopN] = useState<boolean>(false);
const [goGetTimelineId, setGoGetTimelineId] = useState(false);
const timelineIdFind = useGetTimelineId(draggableRef, goGetTimelineId);
const [providerRegistered, setProviderRegistered] = useState(false);
const DraggableWrapperComponent: React.FC<Props> = ({
dataProvider,
onFilterAdded,
render,
timelineId,
truncate,
}) => {
const draggableRef = useRef<HTMLDivElement | null>(null);
const [closePopOverTrigger, setClosePopOverTrigger] = useState(false);
const [showTopN, setShowTopN] = useState<boolean>(false);
const [goGetTimelineId, setGoGetTimelineId] = useState(false);
const timelineIdFind = useGetTimelineId(draggableRef, goGetTimelineId);
const [providerRegistered, setProviderRegistered] = useState(false);
const isDisabled = dataProvider.id.includes(`-${ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID}-`);
const dispatch = useDispatch();
const dispatch = useDispatch();
const handleClosePopOverTrigger = useCallback(
() => setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger),
[]
);
const handleClosePopOverTrigger = useCallback(
() => setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger),
[]
);
const toggleTopN = useCallback(() => {
setShowTopN((prevShowTopN) => {
const newShowTopN = !prevShowTopN;
if (newShowTopN === false) {
handleClosePopOverTrigger();
}
return newShowTopN;
});
}, [handleClosePopOverTrigger]);
const registerProvider = useCallback(() => {
if (!providerRegistered) {
dispatch(dragAndDropActions.registerProvider({ provider: dataProvider }));
setProviderRegistered(true);
const toggleTopN = useCallback(() => {
setShowTopN((prevShowTopN) => {
const newShowTopN = !prevShowTopN;
if (newShowTopN === false) {
handleClosePopOverTrigger();
}
}, [dispatch, providerRegistered, dataProvider]);
return newShowTopN;
});
}, [handleClosePopOverTrigger]);
const unRegisterProvider = useCallback(
() => dispatch(dragAndDropActions.unRegisterProvider({ id: dataProvider.id })),
[dispatch, dataProvider]
);
const registerProvider = useCallback(() => {
if (!isDisabled) {
dispatch(dragAndDropActions.registerProvider({ provider: dataProvider }));
setProviderRegistered(true);
}
}, [isDisabled, dispatch, dataProvider]);
useEffect(
() => () => {
unRegisterProvider();
},
[unRegisterProvider]
);
const unRegisterProvider = useCallback(
() =>
providerRegistered &&
dispatch(dragAndDropActions.unRegisterProvider({ id: dataProvider.id })),
[providerRegistered, dispatch, dataProvider.id]
);
const hoverContent = useMemo(
() => (
<DraggableWrapperHoverContent
closePopOver={handleClosePopOverTrigger}
draggableId={getDraggableId(dataProvider.id)}
field={dataProvider.queryMatch.field}
goGetTimelineId={setGoGetTimelineId}
onFilterAdded={onFilterAdded}
showTopN={showTopN}
timelineId={timelineId ?? timelineIdFind}
toggleTopN={toggleTopN}
value={
typeof dataProvider.queryMatch.value !== 'number'
? dataProvider.queryMatch.value
: `${dataProvider.queryMatch.value}`
}
/>
),
[
dataProvider,
handleClosePopOverTrigger,
onFilterAdded,
showTopN,
timelineId,
timelineIdFind,
toggleTopN,
]
);
useEffect(
() => () => {
unRegisterProvider();
},
[unRegisterProvider]
);
const renderContent = useCallback(
() => (
<Wrapper data-test-subj="draggableWrapperDiv">
<DragDropErrorBoundary>
<Droppable
isDropDisabled={true}
droppableId={getDroppableId(dataProvider.id)}
renderClone={(provided, snapshot) => (
<ConditionalPortal registerProvider={registerProvider}>
<div
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getStyle(provided.draggableProps.style, snapshot)}
ref={provided.innerRef}
data-test-subj="providerContainer"
>
<ProviderContentWrapper
data-test-subj={`draggable-content-${dataProvider.queryMatch.field}`}
>
{render(dataProvider, provided, snapshot)}
</ProviderContentWrapper>
</div>
</ConditionalPortal>
)}
>
{(droppableProvided) => (
<div ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
<Draggable
draggableId={getDraggableId(dataProvider.id)}
index={0}
key={getDraggableId(dataProvider.id)}
>
{(provided, snapshot) => (
<ProviderContainer
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={(e: HTMLDivElement) => {
provided.innerRef(e);
draggableRef.current = e;
}}
data-test-subj="providerContainer"
isDragging={snapshot.isDragging}
registerProvider={registerProvider}
>
{truncate && !snapshot.isDragging ? (
<TruncatableText data-test-subj="draggable-truncatable-content">
{render(dataProvider, provided, snapshot)}
</TruncatableText>
) : (
<ProviderContentWrapper
data-test-subj={`draggable-content-${dataProvider.queryMatch.field}`}
>
{render(dataProvider, provided, snapshot)}
</ProviderContentWrapper>
)}
</ProviderContainer>
)}
</Draggable>
{droppableProvided.placeholder}
</div>
)}
</Droppable>
</DragDropErrorBoundary>
</Wrapper>
),
[dataProvider, render, registerProvider, truncate]
);
return (
<WithHoverActions
alwaysShow={showTopN}
closePopOverTrigger={closePopOverTrigger}
hoverContent={hoverContent}
render={renderContent}
const hoverContent = useMemo(
() => (
<DraggableWrapperHoverContent
closePopOver={handleClosePopOverTrigger}
draggableId={getDraggableId(dataProvider.id)}
field={dataProvider.queryMatch.field}
goGetTimelineId={setGoGetTimelineId}
onFilterAdded={onFilterAdded}
showTopN={showTopN}
timelineId={timelineId ?? timelineIdFind}
toggleTopN={toggleTopN}
value={
typeof dataProvider.queryMatch.value !== 'number'
? dataProvider.queryMatch.value
: `${dataProvider.queryMatch.value}`
}
/>
);
},
(prevProps, nextProps) =>
deepEqual(prevProps.dataProvider, nextProps.dataProvider) &&
prevProps.render !== nextProps.render &&
prevProps.truncate === nextProps.truncate
);
),
[
dataProvider,
handleClosePopOverTrigger,
onFilterAdded,
showTopN,
timelineId,
timelineIdFind,
toggleTopN,
]
);
const renderContent = useCallback(
() => (
<Wrapper data-test-subj="draggableWrapperDiv" disabled={isDisabled}>
<DragDropErrorBoundary>
<Droppable
isDropDisabled={true}
droppableId={getDroppableId(dataProvider.id)}
renderClone={(provided, snapshot) => (
<ConditionalPortal registerProvider={registerProvider}>
<div
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getStyle(provided.draggableProps.style, snapshot)}
ref={provided.innerRef}
data-test-subj="providerContainer"
>
<ProviderContentWrapper
data-test-subj={`draggable-content-${dataProvider.queryMatch.field}`}
>
{render(dataProvider, provided, snapshot)}
</ProviderContentWrapper>
</div>
</ConditionalPortal>
)}
>
{(droppableProvided) => (
<div ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
<Draggable
draggableId={getDraggableId(dataProvider.id)}
index={0}
key={getDraggableId(dataProvider.id)}
isDragDisabled={isDisabled}
>
{(provided, snapshot) => (
<ProviderContainer
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={(e: HTMLDivElement) => {
provided.innerRef(e);
draggableRef.current = e;
}}
data-test-subj="providerContainer"
isDragging={snapshot.isDragging}
registerProvider={registerProvider}
>
{truncate && !snapshot.isDragging ? (
<TruncatableText data-test-subj="draggable-truncatable-content">
{render(dataProvider, provided, snapshot)}
</TruncatableText>
) : (
<ProviderContentWrapper
data-test-subj={`draggable-content-${dataProvider.queryMatch.field}`}
>
{render(dataProvider, provided, snapshot)}
</ProviderContentWrapper>
)}
</ProviderContainer>
)}
</Draggable>
{droppableProvided.placeholder}
</div>
)}
</Droppable>
</DragDropErrorBoundary>
</Wrapper>
),
[dataProvider, registerProvider, render, isDisabled, truncate]
);
if (isDisabled) return <>{renderContent()}</>;
return (
<WithHoverActions
alwaysShow={showTopN}
closePopOverTrigger={closePopOverTrigger}
hoverContent={hoverContent}
render={renderContent}
/>
);
};
export const DraggableWrapper = React.memo(DraggableWrapperComponent);
DraggableWrapper.displayName = 'DraggableWrapper';

View file

@ -5,13 +5,16 @@
*/
import { EuiBadge, EuiToolTip, IconType } from '@elastic/eui';
import React from 'react';
import React, { useCallback, useMemo } from 'react';
import styled from 'styled-components';
import { DragEffects, DraggableWrapper } from '../drag_and_drop/draggable_wrapper';
import { escapeDataProviderId } from '../drag_and_drop/helpers';
import { getEmptyStringTag } from '../empty_value';
import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider';
import {
DataProvider,
IS_OPERATOR,
} from '../../../timelines/components/timeline/data_providers/data_provider';
import { Provider } from '../../../timelines/components/timeline/data_providers/provider';
export interface DefaultDraggableType {
@ -84,36 +87,48 @@ Content.displayName = 'Content';
* @param queryValue - defaults to `value`, this query overrides the `queryMatch.value` used by the `DataProvider` that represents the data
*/
export const DefaultDraggable = React.memo<DefaultDraggableType>(
({ id, field, value, name, children, timelineId, tooltipContent, queryValue }) =>
value != null ? (
({ id, field, value, name, children, timelineId, tooltipContent, queryValue }) => {
const dataProviderProp: DataProvider = useMemo(
() => ({
and: [],
enabled: true,
id: escapeDataProviderId(id),
name: name ? name : value ?? '',
excluded: false,
kqlQuery: '',
queryMatch: {
field,
value: queryValue ? queryValue : value ?? '',
operator: IS_OPERATOR,
},
}),
[field, id, name, queryValue, value]
);
const renderCallback = useCallback(
(dataProvider, _, snapshot) =>
snapshot.isDragging ? (
<DragEffects>
<Provider dataProvider={dataProvider} />
</DragEffects>
) : (
<Content field={field} tooltipContent={tooltipContent} value={value}>
{children}
</Content>
),
[children, field, tooltipContent, value]
);
if (value == null) return null;
return (
<DraggableWrapper
dataProvider={{
and: [],
enabled: true,
id: escapeDataProviderId(id),
name: name ? name : value,
excluded: false,
kqlQuery: '',
queryMatch: {
field,
value: queryValue ? queryValue : value,
operator: IS_OPERATOR,
},
}}
render={(dataProvider, _, snapshot) =>
snapshot.isDragging ? (
<DragEffects>
<Provider dataProvider={dataProvider} />
</DragEffects>
) : (
<Content field={field} tooltipContent={tooltipContent} value={value}>
{children}
</Content>
)
}
dataProvider={dataProviderProp}
render={renderCallback}
timelineId={timelineId}
/>
) : null
);
}
);
DefaultDraggable.displayName = 'DefaultDraggable';
@ -146,33 +161,34 @@ export type BadgeDraggableType = Omit<DefaultDraggableType, 'id'> & {
* prevent a tooltip from being displayed, or pass arbitrary content
* @param queryValue - defaults to `value`, this query overrides the `queryMatch.value` used by the `DataProvider` that represents the data
*/
export const DraggableBadge = React.memo<BadgeDraggableType>(
({
contextId,
eventId,
field,
value,
iconType,
name,
color = 'hollow',
children,
tooltipContent,
queryValue,
}) =>
value != null ? (
<DefaultDraggable
id={`draggable-badge-default-draggable-${contextId}-${eventId}-${field}-${value}`}
field={field}
name={name}
value={value}
tooltipContent={tooltipContent}
queryValue={queryValue}
>
<Badge iconType={iconType} color={color} title="">
{children ? children : value !== '' ? value : getEmptyStringTag()}
</Badge>
</DefaultDraggable>
) : null
);
const DraggableBadgeComponent: React.FC<BadgeDraggableType> = ({
contextId,
eventId,
field,
value,
iconType,
name,
color = 'hollow',
children,
tooltipContent,
queryValue,
}) =>
value != null ? (
<DefaultDraggable
id={`draggable-badge-default-draggable-${contextId}-${eventId}-${field}-${value}`}
field={field}
name={name}
value={value}
tooltipContent={tooltipContent}
queryValue={queryValue}
>
<Badge iconType={iconType} color={color} title="">
{children ? children : value !== '' ? value : getEmptyStringTag()}
</Badge>
</DefaultDraggable>
) : null;
DraggableBadgeComponent.displayName = 'DraggableBadgeComponent';
export const DraggableBadge = React.memo(DraggableBadgeComponent);
DraggableBadge.displayName = 'DraggableBadge';

View file

@ -77,7 +77,7 @@ describe('EventsViewer', () => {
await wait();
wrapper.update();
expect(wrapper.find(`[data-test-subj="show-field-browser-gear"]`).first().exists()).toBe(true);
expect(wrapper.find(`[data-test-subj="show-field-browser"]`).first().exists()).toBe(true);
});
test('it renders the footer containing the Load More button', async () => {

View file

@ -45,6 +45,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
defaultIndices,
deleteEventQuery,
end,
excludedRowRendererIds,
filters,
headerFilterGroup,
id,
@ -57,7 +58,6 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
removeColumn,
start,
showCheckboxes,
showRowRenderers,
sort,
updateItemsPerPage,
upsertColumn,
@ -69,7 +69,14 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
useEffect(() => {
if (createTimeline != null) {
createTimeline({ id, columns, sort, itemsPerPage, showCheckboxes, showRowRenderers });
createTimeline({
id,
columns,
excludedRowRendererIds,
sort,
itemsPerPage,
showCheckboxes,
});
}
return () => {
deleteEventQuery({ id, inputId: 'global' });
@ -125,7 +132,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
onChangeItemsPerPage={onChangeItemsPerPage}
query={query}
start={start}
sort={sort!}
sort={sort}
toggleColumn={toggleColumn}
utilityBar={utilityBar}
/>
@ -145,18 +152,19 @@ const makeMapStateToProps = () => {
columns,
dataProviders,
deletedEventIds,
excludedRowRendererIds,
itemsPerPage,
itemsPerPageOptions,
kqlMode,
sort,
showCheckboxes,
showRowRenderers,
} = events;
return {
columns,
dataProviders,
deletedEventIds,
excludedRowRendererIds,
filters: getGlobalFiltersQuerySelector(state),
id,
isLive: input.policy.kind === 'interval',
@ -166,7 +174,6 @@ const makeMapStateToProps = () => {
query: getGlobalQuerySelector(state),
sort,
showCheckboxes,
showRowRenderers,
};
};
return mapStateToProps;
@ -192,6 +199,7 @@ export const StatefulEventsViewer = connector(
deepEqual(prevProps.columns, nextProps.columns) &&
deepEqual(prevProps.defaultIndices, nextProps.defaultIndices) &&
deepEqual(prevProps.dataProviders, nextProps.dataProviders) &&
deepEqual(prevProps.excludedRowRendererIds, nextProps.excludedRowRendererIds) &&
prevProps.deletedEventIds === nextProps.deletedEventIds &&
prevProps.end === nextProps.end &&
deepEqual(prevProps.filters, nextProps.filters) &&
@ -204,7 +212,6 @@ export const StatefulEventsViewer = connector(
prevProps.start === nextProps.start &&
deepEqual(prevProps.pageFilters, nextProps.pageFilters) &&
prevProps.showCheckboxes === nextProps.showCheckboxes &&
prevProps.showRowRenderers === nextProps.showRowRenderers &&
prevProps.start === nextProps.start &&
prevProps.utilityBar === nextProps.utilityBar
)

View file

@ -195,6 +195,7 @@ export const mockGlobalState: State = {
dataProviders: [],
description: '',
eventIdToNoteIds: {},
excludedRowRendererIds: [],
highlightedDropAndProviderId: '',
historyIds: [],
isFavorite: false,
@ -215,7 +216,6 @@ export const mockGlobalState: State = {
},
selectedEventIds: {},
show: false,
showRowRenderers: true,
showCheckboxes: false,
pinnedEventIds: {},
pinnedEventsSaveObject: {},

View file

@ -418,8 +418,8 @@ export const mockTimelineData: TimelineItem[] = [
data: [
{ field: '@timestamp', value: ['2019-03-07T05:06:51.000Z'] },
{ field: 'host.name', value: ['zeek-franfurt'] },
{ field: 'source.ip', value: ['185.176.26.101'] },
{ field: 'destination.ip', value: ['207.154.238.205'] },
{ field: 'source.ip', value: ['192.168.26.101'] },
{ field: 'destination.ip', value: ['192.168.238.205'] },
],
ecs: {
_id: '14',
@ -466,8 +466,8 @@ export const mockTimelineData: TimelineItem[] = [
data: [
{ field: '@timestamp', value: ['2019-03-07T00:51:28.000Z'] },
{ field: 'host.name', value: ['suricata-zeek-singapore'] },
{ field: 'source.ip', value: ['206.189.35.240'] },
{ field: 'destination.ip', value: ['67.207.67.3'] },
{ field: 'source.ip', value: ['192.168.35.240'] },
{ field: 'destination.ip', value: ['192.168.67.3'] },
],
ecs: {
_id: '15',
@ -520,8 +520,8 @@ export const mockTimelineData: TimelineItem[] = [
data: [
{ field: '@timestamp', value: ['2019-03-05T07:00:20.000Z'] },
{ field: 'host.name', value: ['suricata-zeek-singapore'] },
{ field: 'source.ip', value: ['206.189.35.240'] },
{ field: 'destination.ip', value: ['192.241.164.26'] },
{ field: 'source.ip', value: ['192.168.35.240'] },
{ field: 'destination.ip', value: ['192.168.164.26'] },
],
ecs: {
_id: '16',
@ -572,7 +572,7 @@ export const mockTimelineData: TimelineItem[] = [
data: [
{ field: '@timestamp', value: ['2019-02-28T22:36:28.000Z'] },
{ field: 'host.name', value: ['zeek-franfurt'] },
{ field: 'source.ip', value: ['8.42.77.171'] },
{ field: 'source.ip', value: ['192.168.77.171'] },
],
ecs: {
_id: '17',
@ -621,8 +621,8 @@ export const mockTimelineData: TimelineItem[] = [
data: [
{ field: '@timestamp', value: ['2019-02-22T21:12:13.000Z'] },
{ field: 'host.name', value: ['zeek-sensor-amsterdam'] },
{ field: 'source.ip', value: ['188.166.66.184'] },
{ field: 'destination.ip', value: ['91.189.95.15'] },
{ field: 'source.ip', value: ['192.168.66.184'] },
{ field: 'destination.ip', value: ['192.168.95.15'] },
],
ecs: {
_id: '18',
@ -767,7 +767,7 @@ export const mockTimelineData: TimelineItem[] = [
{ field: '@timestamp', value: ['2019-03-14T22:30:25.527Z'] },
{ field: 'event.category', value: ['user-login'] },
{ field: 'host.name', value: ['zeek-london'] },
{ field: 'source.ip', value: ['8.42.77.171'] },
{ field: 'source.ip', value: ['192.168.77.171'] },
{ field: 'user.name', value: ['root'] },
],
ecs: {
@ -1101,7 +1101,7 @@ export const mockTimelineData: TimelineItem[] = [
{ field: 'event.action', value: ['connected-to'] },
{ field: 'event.category', value: ['audit-rule'] },
{ field: 'host.name', value: ['zeek-london'] },
{ field: 'destination.ip', value: ['93.184.216.34'] },
{ field: 'destination.ip', value: ['192.168.216.34'] },
{ field: 'user.name', value: ['alice'] },
],
ecs: {
@ -1121,7 +1121,7 @@ export const mockTimelineData: TimelineItem[] = [
data: null,
summary: {
actor: { primary: ['alice'], secondary: ['alice'] },
object: { primary: ['93.184.216.34'], secondary: ['80'], type: ['socket'] },
object: { primary: ['192.168.216.34'], secondary: ['80'], type: ['socket'] },
how: ['/usr/bin/wget'],
message_type: null,
sequence: null,
@ -1133,7 +1133,7 @@ export const mockTimelineData: TimelineItem[] = [
ip: ['46.101.3.136', '10.16.0.5', 'fe80::4066:42ff:fe19:b3b9'],
},
source: null,
destination: { ip: ['93.184.216.34'], port: [80] },
destination: { ip: ['192.168.216.34'], port: [80] },
geo: null,
suricata: null,
network: null,
@ -1174,7 +1174,7 @@ export const mockTimelineData: TimelineItem[] = [
},
auditd: {
result: ['success'],
session: ['unset'],
session: ['242'],
data: null,
summary: {
actor: { primary: ['unset'], secondary: ['root'] },

View file

@ -2098,6 +2098,7 @@ export const mockTimelineModel: TimelineModel = {
description: 'This is a sample rule description',
eventIdToNoteIds: {},
eventType: 'all',
excludedRowRendererIds: [],
filters: [
{
$state: {
@ -2137,7 +2138,6 @@ export const mockTimelineModel: TimelineModel = {
selectedEventIds: {},
show: false,
showCheckboxes: false,
showRowRenderers: true,
sort: {
columnId: '@timestamp',
sortDirection: Direction.desc,
@ -2217,6 +2217,7 @@ export const defaultTimelineProps: CreateTimelineProps = {
description: '',
eventIdToNoteIds: {},
eventType: 'all',
excludedRowRendererIds: [],
filters: [],
highlightedDropAndProviderId: '',
historyIds: [],
@ -2241,7 +2242,6 @@ export const defaultTimelineProps: CreateTimelineProps = {
selectedEventIds: {},
show: false,
showCheckboxes: false,
showRowRenderers: true,
sort: { columnId: '@timestamp', sortDirection: Direction.desc },
status: TimelineStatus.draft,
title: '',

View file

@ -158,6 +158,7 @@ describe('alert actions', () => {
description: 'This is a sample rule description',
eventIdToNoteIds: {},
eventType: 'all',
excludedRowRendererIds: [],
filters: [
{
$state: {
@ -210,7 +211,6 @@ describe('alert actions', () => {
selectedEventIds: {},
show: true,
showCheckboxes: false,
showRowRenderers: true,
sort: {
columnId: '@timestamp',
sortDirection: 'desc',

View file

@ -39,6 +39,10 @@ interface AlertsUtilityBarProps {
updateAlertsStatus: UpdateAlertsStatus;
}
const UtilityBarFlexGroup = styled(EuiFlexGroup)`
min-width: 175px;
`;
const AlertsUtilityBarComponent: React.FC<AlertsUtilityBarProps> = ({
canUserCRUD,
hasIndexWrite,
@ -69,10 +73,6 @@ const AlertsUtilityBarComponent: React.FC<AlertsUtilityBarProps> = ({
defaultNumberFormat
);
const UtilityBarFlexGroup = styled(EuiFlexGroup)`
min-width: 175px;
`;
const UtilityBarPopoverContent = (closePopover: () => void) => (
<UtilityBarFlexGroup direction="column">
{currentFilter !== FILTER_OPEN && (

View file

@ -11,6 +11,7 @@ import ApolloClient from 'apollo-client';
import { Dispatch } from 'redux';
import { EuiText } from '@elastic/eui';
import { RowRendererId } from '../../../../common/types/timeline';
import { Status } from '../../../../common/detection_engine/schemas/common/schemas';
import { Filter } from '../../../../../../../src/plugins/data/common/es_query';
import {
@ -162,7 +163,7 @@ export const alertsDefaultModel: SubsetTimelineModel = {
...timelineDefaults,
columns: alertsHeaders,
showCheckboxes: true,
showRowRenderers: false,
excludedRowRendererIds: Object.values(RowRendererId),
};
export const requiredFieldsForActions = [

View file

@ -10,7 +10,7 @@ import { TimelineIdLiteral, TimelineId } from '../../common/types/timeline';
import { AlertsRoutes } from './routes';
import { SecuritySubPlugin } from '../app/types';
const ALERTS_TIMELINE_IDS: TimelineIdLiteral[] = [
const DETECTIONS_TIMELINE_IDS: TimelineIdLiteral[] = [
TimelineId.detectionsRulesDetailsPage,
TimelineId.detectionsPage,
];
@ -22,7 +22,7 @@ export class Detections {
return {
SubPluginRoutes: AlertsRoutes,
storageTimelines: {
timelineById: getTimelinesInStorageByIds(storage, ALERTS_TIMELINE_IDS),
timelineById: getTimelinesInStorageByIds(storage, DETECTIONS_TIMELINE_IDS),
},
};
}

View file

@ -9641,6 +9641,22 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "excludedRowRendererIds",
"description": "",
"args": [],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "ENUM", "name": "RowRendererId", "ofType": null }
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "favorite",
"description": "",
@ -10146,6 +10162,75 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "RowRendererId",
"description": "",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{ "name": "auditd", "description": "", "isDeprecated": false, "deprecationReason": null },
{
"name": "auditd_file",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "netflow",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{ "name": "plain", "description": "", "isDeprecated": false, "deprecationReason": null },
{
"name": "suricata",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{ "name": "system", "description": "", "isDeprecated": false, "deprecationReason": null },
{
"name": "system_dns",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "system_endgame_process",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "system_file",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "system_fim",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "system_security_event",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "system_socket",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{ "name": "zeek", "description": "", "isDeprecated": false, "deprecationReason": null }
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "FavoriteTimelineResult",
@ -11061,6 +11146,20 @@
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": null
},
{
"name": "excludedRowRendererIds",
"description": "",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "ENUM", "name": "RowRendererId", "ofType": null }
}
},
"defaultValue": null
},
{
"name": "filters",
"description": "",

View file

@ -124,6 +124,8 @@ export interface TimelineInput {
eventType?: Maybe<string>;
excludedRowRendererIds?: Maybe<RowRendererId[]>;
filters?: Maybe<FilterTimelineInput[]>;
kqlMode?: Maybe<string>;
@ -349,6 +351,22 @@ export enum DataProviderType {
template = 'template',
}
export enum RowRendererId {
auditd = 'auditd',
auditd_file = 'auditd_file',
netflow = 'netflow',
plain = 'plain',
suricata = 'suricata',
system = 'system',
system_dns = 'system_dns',
system_endgame_process = 'system_endgame_process',
system_file = 'system_file',
system_fim = 'system_fim',
system_security_event = 'system_security_event',
system_socket = 'system_socket',
zeek = 'zeek',
}
export enum TimelineStatus {
active = 'active',
draft = 'draft',
@ -1961,6 +1979,8 @@ export interface TimelineResult {
eventType?: Maybe<string>;
excludedRowRendererIds?: Maybe<RowRendererId[]>;
favorite?: Maybe<FavoriteTimelineResult[]>;
filters?: Maybe<FilterTimelineResult[]>;
@ -4385,6 +4405,8 @@ export namespace GetAllTimeline {
eventIdToNoteIds: Maybe<EventIdToNoteIds[]>;
excludedRowRendererIds: Maybe<RowRendererId[]>;
notes: Maybe<Notes[]>;
noteIds: Maybe<string[]>;
@ -5454,6 +5476,8 @@ export namespace GetOneTimeline {
eventIdToNoteIds: Maybe<EventIdToNoteIds[]>;
excludedRowRendererIds: Maybe<RowRendererId[]>;
favorite: Maybe<Favorite[]>;
filters: Maybe<Filters[]>;

View file

@ -35,6 +35,10 @@ Percent.displayName = 'Percent';
const SourceDestinationArrowsContainer = styled(EuiFlexGroup)`
margin: 0 2px;
.euiToolTipAnchor {
white-space: nowrap;
}
`;
SourceDestinationArrowsContainer.displayName = 'SourceDestinationArrowsContainer';

View file

@ -6,11 +6,9 @@
import { mount } from 'enzyme';
import React from 'react';
import { ActionCreator } from 'typescript-fsa';
import { mockBrowserFields } from '../../../common/containers/source/mock';
import { TestProviders } from '../../../common/mock';
import { ColumnHeaderOptions } from '../../store/timeline/model';
import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from './helpers';
@ -29,17 +27,6 @@ afterAll(() => {
console.warn = originalWarn;
});
const removeColumnMock = (jest.fn() as unknown) as ActionCreator<{
id: string;
columnId: string;
}>;
const upsertColumnMock = (jest.fn() as unknown) as ActionCreator<{
column: ColumnHeaderOptions;
id: string;
index: number;
}>;
describe('StatefulFieldsBrowser', () => {
const timelineId = 'test';
@ -54,13 +41,11 @@ describe('StatefulFieldsBrowser', () => {
timelineId={timelineId}
toggleColumn={jest.fn()}
width={FIELD_BROWSER_WIDTH}
removeColumn={removeColumnMock}
upsertColumn={upsertColumnMock}
/>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="show-field-browser"]').first().text()).toEqual('Columns');
expect(wrapper.find('[data-test-subj="show-field-browser"]').exists()).toBe(true);
});
describe('toggleShow', () => {
@ -75,8 +60,6 @@ describe('StatefulFieldsBrowser', () => {
timelineId={timelineId}
toggleColumn={jest.fn()}
width={FIELD_BROWSER_WIDTH}
removeColumn={removeColumnMock}
upsertColumn={upsertColumnMock}
/>
</TestProviders>
);
@ -95,8 +78,6 @@ describe('StatefulFieldsBrowser', () => {
timelineId={timelineId}
toggleColumn={jest.fn()}
width={FIELD_BROWSER_WIDTH}
removeColumn={removeColumnMock}
upsertColumn={upsertColumnMock}
/>
</TestProviders>
);
@ -122,8 +103,6 @@ describe('StatefulFieldsBrowser', () => {
timelineId={timelineId}
toggleColumn={jest.fn()}
width={FIELD_BROWSER_WIDTH}
removeColumn={removeColumnMock}
upsertColumn={upsertColumnMock}
/>
</TestProviders>
);
@ -149,8 +128,6 @@ describe('StatefulFieldsBrowser', () => {
timelineId={timelineId}
toggleColumn={jest.fn()}
width={FIELD_BROWSER_WIDTH}
removeColumn={removeColumnMock}
upsertColumn={upsertColumnMock}
/>
</TestProviders>
);
@ -186,39 +163,14 @@ describe('StatefulFieldsBrowser', () => {
timelineId={timelineId}
toggleColumn={jest.fn()}
width={FIELD_BROWSER_WIDTH}
removeColumn={removeColumnMock}
upsertColumn={upsertColumnMock}
/>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="show-field-browser-gear"]').first().exists()).toBe(true);
expect(wrapper.find('[data-test-subj="show-field-browser"]').first().exists()).toBe(true);
});
test('it does NOT render the Fields Browser button as a settings gear when the isEventViewer prop is false', () => {
const isEventViewer = false;
const wrapper = mount(
<TestProviders>
<StatefulFieldsBrowserComponent
browserFields={mockBrowserFields}
columnHeaders={[]}
height={FIELD_BROWSER_HEIGHT}
isEventViewer={isEventViewer}
onUpdateColumns={jest.fn()}
timelineId={timelineId}
toggleColumn={jest.fn()}
width={FIELD_BROWSER_WIDTH}
removeColumn={removeColumnMock}
upsertColumn={upsertColumnMock}
/>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="show-field-browser-gear"]').first().exists()).toBe(false);
});
test('it does NOT render the default Fields Browser button when the isEventViewer prop is true', () => {
test('it renders the Fields Browser button as a settings gear when the isEventViewer prop is false', () => {
const isEventViewer = true;
const wrapper = mount(
@ -232,12 +184,10 @@ describe('StatefulFieldsBrowser', () => {
timelineId={timelineId}
toggleColumn={jest.fn()}
width={FIELD_BROWSER_WIDTH}
removeColumn={removeColumnMock}
upsertColumn={upsertColumnMock}
/>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="show-field-browser"]').first().exists()).toBe(false);
expect(wrapper.find('[data-test-subj="show-field-browser"]').first().exists()).toBe(true);
});
});

View file

@ -4,14 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiButtonEmpty, EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import { noop } from 'lodash/fp';
import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import styled from 'styled-components';
import { BrowserFields } from '../../../common/containers/source';
import { timelineActions } from '../../store/timeline';
import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import { DEFAULT_CATEGORY_NAME } from '../timeline/body/column_headers/default_headers';
import { FieldsBrowser } from './field_browser';
@ -34,181 +32,156 @@ FieldsBrowserButtonContainer.displayName = 'FieldsBrowserButtonContainer';
/**
* Manages the state of the field browser
*/
export const StatefulFieldsBrowserComponent = React.memo<FieldBrowserProps & PropsFromRedux>(
({
columnHeaders,
browserFields,
height,
isEventViewer = false,
onFieldSelected,
onUpdateColumns,
timelineId,
toggleColumn,
width,
}) => {
/** tracks the latest timeout id from `setTimeout`*/
const inputTimeoutId = useRef(0);
export const StatefulFieldsBrowserComponent: React.FC<FieldBrowserProps> = ({
columnHeaders,
browserFields,
height,
isEventViewer = false,
onFieldSelected,
onUpdateColumns,
timelineId,
toggleColumn,
width,
}) => {
/** tracks the latest timeout id from `setTimeout`*/
const inputTimeoutId = useRef(0);
/** all field names shown in the field browser must contain this string (when specified) */
const [filterInput, setFilterInput] = useState('');
/** all fields in this collection have field names that match the filterInput */
const [filteredBrowserFields, setFilteredBrowserFields] = useState<BrowserFields | null>(null);
/** when true, show a spinner in the input to indicate the field browser is searching for matching field names */
const [isSearching, setIsSearching] = useState(false);
/** this category will be displayed in the right-hand pane of the field browser */
const [selectedCategoryId, setSelectedCategoryId] = useState(DEFAULT_CATEGORY_NAME);
/** show the field browser */
const [show, setShow] = useState(false);
useEffect(() => {
return () => {
if (inputTimeoutId.current !== 0) {
// ⚠️ mutation: cancel any remaining timers and zero-out the timer id:
clearTimeout(inputTimeoutId.current);
inputTimeoutId.current = 0;
}
};
}, []);
/** all field names shown in the field browser must contain this string (when specified) */
const [filterInput, setFilterInput] = useState('');
/** all fields in this collection have field names that match the filterInput */
const [filteredBrowserFields, setFilteredBrowserFields] = useState<BrowserFields | null>(null);
/** when true, show a spinner in the input to indicate the field browser is searching for matching field names */
const [isSearching, setIsSearching] = useState(false);
/** this category will be displayed in the right-hand pane of the field browser */
const [selectedCategoryId, setSelectedCategoryId] = useState(DEFAULT_CATEGORY_NAME);
/** show the field browser */
const [show, setShow] = useState(false);
useEffect(() => {
return () => {
if (inputTimeoutId.current !== 0) {
// ⚠️ mutation: cancel any remaining timers and zero-out the timer id:
clearTimeout(inputTimeoutId.current);
inputTimeoutId.current = 0;
}
};
}, []);
/** Shows / hides the field browser */
const toggleShow = useCallback(() => {
setShow(!show);
}, [show]);
/** Shows / hides the field browser */
const toggleShow = useCallback(() => {
setShow(!show);
}, [show]);
/** Invoked when the user types in the filter input */
const updateFilter = useCallback(
(newFilterInput: string) => {
setFilterInput(newFilterInput);
setIsSearching(true);
if (inputTimeoutId.current !== 0) {
clearTimeout(inputTimeoutId.current); // ⚠️ mutation: cancel any previous timers
}
// ⚠️ mutation: schedule a new timer that will apply the filter when it fires:
inputTimeoutId.current = window.setTimeout(() => {
const newFilteredBrowserFields = filterBrowserFieldsByFieldName({
browserFields: mergeBrowserFieldsWithDefaultCategory(browserFields),
substring: newFilterInput,
});
setFilteredBrowserFields(newFilteredBrowserFields);
setIsSearching(false);
/** Invoked when the user types in the filter input */
const updateFilter = useCallback(
(newFilterInput: string) => {
setFilterInput(newFilterInput);
setIsSearching(true);
if (inputTimeoutId.current !== 0) {
clearTimeout(inputTimeoutId.current); // ⚠️ mutation: cancel any previous timers
}
// ⚠️ mutation: schedule a new timer that will apply the filter when it fires:
inputTimeoutId.current = window.setTimeout(() => {
const newFilteredBrowserFields = filterBrowserFieldsByFieldName({
browserFields: mergeBrowserFieldsWithDefaultCategory(browserFields),
substring: newFilterInput,
});
setFilteredBrowserFields(newFilteredBrowserFields);
setIsSearching(false);
const newSelectedCategoryId =
newFilterInput === '' || Object.keys(newFilteredBrowserFields).length === 0
? DEFAULT_CATEGORY_NAME
: Object.keys(newFilteredBrowserFields)
.sort()
.reduce<string>(
(selected, category) =>
newFilteredBrowserFields[category].fields != null &&
newFilteredBrowserFields[selected].fields != null &&
Object.keys(newFilteredBrowserFields[category].fields!).length >
Object.keys(newFilteredBrowserFields[selected].fields!).length
? category
: selected,
Object.keys(newFilteredBrowserFields)[0]
);
setSelectedCategoryId(newSelectedCategoryId);
}, INPUT_TIMEOUT);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[browserFields, filterInput, inputTimeoutId.current]
);
const newSelectedCategoryId =
newFilterInput === '' || Object.keys(newFilteredBrowserFields).length === 0
? DEFAULT_CATEGORY_NAME
: Object.keys(newFilteredBrowserFields)
.sort()
.reduce<string>(
(selected, category) =>
newFilteredBrowserFields[category].fields != null &&
newFilteredBrowserFields[selected].fields != null &&
Object.keys(newFilteredBrowserFields[category].fields!).length >
Object.keys(newFilteredBrowserFields[selected].fields!).length
? category
: selected,
Object.keys(newFilteredBrowserFields)[0]
);
setSelectedCategoryId(newSelectedCategoryId);
}, INPUT_TIMEOUT);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[browserFields, filterInput, inputTimeoutId.current]
);
/**
* Invoked when the user clicks a category name in the left-hand side of
* the field browser
*/
const updateSelectedCategoryId = useCallback((categoryId: string) => {
setSelectedCategoryId(categoryId);
}, []);
/**
* Invoked when the user clicks a category name in the left-hand side of
* the field browser
*/
const updateSelectedCategoryId = useCallback((categoryId: string) => {
setSelectedCategoryId(categoryId);
}, []);
/**
* Invoked when the user clicks on the context menu to view a category's
* columns in the timeline, this function dispatches the action that
* causes the timeline display those columns.
*/
const updateColumnsAndSelectCategoryId = useCallback((columns: ColumnHeaderOptions[]) => {
onUpdateColumns(columns); // show the category columns in the timeline
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
/**
* Invoked when the user clicks on the context menu to view a category's
* columns in the timeline, this function dispatches the action that
* causes the timeline display those columns.
*/
const updateColumnsAndSelectCategoryId = useCallback((columns: ColumnHeaderOptions[]) => {
onUpdateColumns(columns); // show the category columns in the timeline
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
/** Invoked when the field browser should be hidden */
const hideFieldBrowser = useCallback(() => {
setFilterInput('');
setFilterInput('');
setFilteredBrowserFields(null);
setIsSearching(false);
setSelectedCategoryId(DEFAULT_CATEGORY_NAME);
setShow(false);
}, []);
// only merge in the default category if the field browser is visible
const browserFieldsWithDefaultCategory = useMemo(
() => (show ? mergeBrowserFieldsWithDefaultCategory(browserFields) : {}),
[show, browserFields]
);
/** Invoked when the field browser should be hidden */
const hideFieldBrowser = useCallback(() => {
setFilterInput('');
setFilterInput('');
setFilteredBrowserFields(null);
setIsSearching(false);
setSelectedCategoryId(DEFAULT_CATEGORY_NAME);
setShow(false);
}, []);
// only merge in the default category if the field browser is visible
const browserFieldsWithDefaultCategory = useMemo(
() => (show ? mergeBrowserFieldsWithDefaultCategory(browserFields) : {}),
[show, browserFields]
);
return (
<>
<FieldsBrowserButtonContainer data-test-subj="fields-browser-button-container">
<EuiToolTip content={i18n.CUSTOMIZE_COLUMNS}>
{isEventViewer ? (
<EuiButtonIcon
aria-label={i18n.CUSTOMIZE_COLUMNS}
className={fieldsButtonClassName}
data-test-subj="show-field-browser-gear"
iconType="list"
onClick={toggleShow}
/>
) : (
<EuiButtonEmpty
className={fieldsButtonClassName}
data-test-subj="show-field-browser"
iconType="list"
onClick={toggleShow}
size="xs"
>
{i18n.FIELDS}
</EuiButtonEmpty>
)}
</EuiToolTip>
return (
<FieldsBrowserButtonContainer data-test-subj="fields-browser-button-container">
<EuiToolTip content={i18n.CUSTOMIZE_COLUMNS}>
<EuiButtonIcon
aria-label={i18n.CUSTOMIZE_COLUMNS}
className={fieldsButtonClassName}
data-test-subj="show-field-browser"
iconType="list"
onClick={toggleShow}
>
{i18n.FIELDS}
</EuiButtonIcon>
</EuiToolTip>
{show && (
<FieldsBrowser
browserFields={browserFieldsWithDefaultCategory}
columnHeaders={columnHeaders}
filteredBrowserFields={
filteredBrowserFields != null
? filteredBrowserFields
: browserFieldsWithDefaultCategory
}
height={height}
isEventViewer={isEventViewer}
isSearching={isSearching}
onCategorySelected={updateSelectedCategoryId}
onFieldSelected={onFieldSelected}
onHideFieldBrowser={hideFieldBrowser}
onOutsideClick={show ? hideFieldBrowser : noop}
onSearchInputChange={updateFilter}
onUpdateColumns={updateColumnsAndSelectCategoryId}
searchInput={filterInput}
selectedCategoryId={selectedCategoryId}
timelineId={timelineId}
toggleColumn={toggleColumn}
width={width}
/>
)}
</FieldsBrowserButtonContainer>
</>
);
}
);
const mapDispatchToProps = {
removeColumn: timelineActions.removeColumn,
upsertColumn: timelineActions.upsertColumn,
{show && (
<FieldsBrowser
browserFields={browserFieldsWithDefaultCategory}
columnHeaders={columnHeaders}
filteredBrowserFields={
filteredBrowserFields != null ? filteredBrowserFields : browserFieldsWithDefaultCategory
}
height={height}
isEventViewer={isEventViewer}
isSearching={isSearching}
onCategorySelected={updateSelectedCategoryId}
onFieldSelected={onFieldSelected}
onHideFieldBrowser={hideFieldBrowser}
onOutsideClick={show ? hideFieldBrowser : noop}
onSearchInputChange={updateFilter}
onUpdateColumns={updateColumnsAndSelectCategoryId}
searchInput={filterInput}
selectedCategoryId={selectedCategoryId}
timelineId={timelineId}
toggleColumn={toggleColumn}
width={width}
/>
)}
</FieldsBrowserButtonContainer>
);
};
const connector = connect(null, mapDispatchToProps);
type PropsFromRedux = ConnectedProps<typeof connector>;
export const StatefulFieldsBrowser = connector(React.memo(StatefulFieldsBrowserComponent));
export const StatefulFieldsBrowser = React.memo(StatefulFieldsBrowserComponent);

View file

@ -28,6 +28,7 @@ interface FlyoutPaneComponentProps {
const EuiFlyoutContainer = styled.div`
.timeline-flyout {
z-index: 4001;
min-width: 150px;
width: auto;
}

View file

@ -270,6 +270,7 @@ describe('helpers', () => {
deletedEventIds: [],
eventIdToNoteIds: {},
eventType: 'all',
excludedRowRendererIds: [],
filters: [],
highlightedDropAndProviderId: '',
historyIds: [],
@ -294,7 +295,6 @@ describe('helpers', () => {
selectedEventIds: {},
show: false,
showCheckboxes: false,
showRowRenderers: true,
sort: {
columnId: '@timestamp',
sortDirection: 'desc',
@ -368,6 +368,7 @@ describe('helpers', () => {
deletedEventIds: [],
eventIdToNoteIds: {},
eventType: 'all',
excludedRowRendererIds: [],
filters: [],
highlightedDropAndProviderId: '',
historyIds: [],
@ -392,7 +393,6 @@ describe('helpers', () => {
selectedEventIds: {},
show: false,
showCheckboxes: false,
showRowRenderers: true,
sort: {
columnId: '@timestamp',
sortDirection: 'desc',
@ -502,6 +502,7 @@ describe('helpers', () => {
deletedEventIds: [],
eventIdToNoteIds: {},
eventType: 'all',
excludedRowRendererIds: [],
filters: [],
highlightedDropAndProviderId: '',
historyIds: [],
@ -532,7 +533,6 @@ describe('helpers', () => {
selectedEventIds: {},
show: false,
showCheckboxes: false,
showRowRenderers: true,
sort: {
columnId: '@timestamp',
sortDirection: 'desc',
@ -628,6 +628,7 @@ describe('helpers', () => {
deletedEventIds: [],
eventIdToNoteIds: {},
eventType: 'all',
excludedRowRendererIds: [],
filters: [
{
$state: {
@ -701,7 +702,6 @@ describe('helpers', () => {
selectedEventIds: {},
show: false,
showCheckboxes: false,
showRowRenderers: true,
sort: {
columnId: '@timestamp',
sortDirection: 'desc',

View file

@ -13,6 +13,7 @@ import {
TimelineTypeLiteralWithNull,
TimelineStatus,
TemplateTimelineTypeLiteral,
RowRendererId,
} from '../../../../common/types/timeline';
/** The users who added a timeline to favorites */
@ -46,6 +47,7 @@ export interface OpenTimelineResult {
created?: number | null;
description?: string | null;
eventIdToNoteIds?: Readonly<Record<string, string[]>> | null;
excludedRowRendererIds?: RowRendererId[] | null;
favorite?: FavoriteTimelineResult[] | null;
noteIds?: string[] | null;
notes?: TimelineResultNote[] | null;

View file

@ -0,0 +1,199 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiLink } from '@elastic/eui';
import React from 'react';
import { ExternalLinkIcon } from '../../../../common/components/external_link_icon';
import { RowRendererId } from '../../../../../common/types/timeline';
import {
AuditdExample,
AuditdFileExample,
NetflowExample,
SuricataExample,
SystemExample,
SystemDnsExample,
SystemEndgameProcessExample,
SystemFileExample,
SystemFimExample,
SystemSecurityEventExample,
SystemSocketExample,
ZeekExample,
} from '../examples';
import * as i18n from './translations';
const Link = ({ children, url }: { children: React.ReactNode; url: string }) => (
<EuiLink
href={url}
target="_blank"
rel="noopener nofollow noreferrer"
data-test-subj="externalLink"
>
{children}
<ExternalLinkIcon data-test-subj="externalLinkIcon" />
</EuiLink>
);
export interface RowRendererOption {
id: RowRendererId;
name: string;
description: React.ReactNode;
searchableDescription: string;
example: React.ReactNode;
}
export const renderers: RowRendererOption[] = [
{
id: RowRendererId.auditd,
name: i18n.AUDITD_NAME,
description: (
<span>
<Link url="https://www.elastic.co/guide/en/beats/auditbeat/current/auditbeat-module-auditd.html">
{i18n.AUDITD_NAME}
</Link>{' '}
{i18n.AUDITD_DESCRIPTION_PART1}
</span>
),
example: AuditdExample,
searchableDescription: `${i18n.AUDITD_NAME} ${i18n.AUDITD_DESCRIPTION_PART1}`,
},
{
id: RowRendererId.auditd_file,
name: i18n.AUDITD_FILE_NAME,
description: (
<span>
<Link url="https://www.elastic.co/guide/en/beats/auditbeat/current/auditbeat-module-auditd.html">
{i18n.AUDITD_NAME}
</Link>{' '}
{i18n.AUDITD_FILE_DESCRIPTION_PART1}
</span>
),
example: AuditdFileExample,
searchableDescription: `${i18n.AUDITD_FILE_NAME} ${i18n.AUDITD_FILE_DESCRIPTION_PART1}`,
},
{
id: RowRendererId.system_security_event,
name: i18n.AUTHENTICATION_NAME,
description: (
<div>
<p>{i18n.AUTHENTICATION_DESCRIPTION_PART1}</p>
<br />
<p>{i18n.AUTHENTICATION_DESCRIPTION_PART2}</p>
</div>
),
example: SystemSecurityEventExample,
searchableDescription: `${i18n.AUTHENTICATION_DESCRIPTION_PART1} ${i18n.AUTHENTICATION_DESCRIPTION_PART2}`,
},
{
id: RowRendererId.system_dns,
name: i18n.DNS_NAME,
description: i18n.DNS_DESCRIPTION_PART1,
example: SystemDnsExample,
searchableDescription: i18n.DNS_DESCRIPTION_PART1,
},
{
id: RowRendererId.netflow,
name: i18n.FLOW_NAME,
description: (
<div>
<p>{i18n.FLOW_DESCRIPTION_PART1}</p>
<br />
<p>{i18n.FLOW_DESCRIPTION_PART2}</p>
</div>
),
example: NetflowExample,
searchableDescription: `${i18n.FLOW_DESCRIPTION_PART1} ${i18n.FLOW_DESCRIPTION_PART2}`,
},
{
id: RowRendererId.system,
name: i18n.SYSTEM_NAME,
description: (
<div>
<p>
{i18n.SYSTEM_DESCRIPTION_PART1}{' '}
<Link url="https://www.elastic.co/guide/en/beats/auditbeat/current/auditbeat-module-system.html">
{i18n.SYSTEM_NAME}
</Link>{' '}
{i18n.SYSTEM_DESCRIPTION_PART2}
</p>
<br />
<p>{i18n.SYSTEM_DESCRIPTION_PART3}</p>
</div>
),
example: SystemExample,
searchableDescription: `${i18n.SYSTEM_DESCRIPTION_PART1} ${i18n.SYSTEM_NAME} ${i18n.SYSTEM_DESCRIPTION_PART2} ${i18n.SYSTEM_DESCRIPTION_PART3}`,
},
{
id: RowRendererId.system_endgame_process,
name: i18n.PROCESS,
description: (
<div>
<p>{i18n.PROCESS_DESCRIPTION_PART1}</p>
<br />
<p>{i18n.PROCESS_DESCRIPTION_PART2}</p>
</div>
),
example: SystemEndgameProcessExample,
searchableDescription: `${i18n.PROCESS_DESCRIPTION_PART1} ${i18n.PROCESS_DESCRIPTION_PART2}`,
},
{
id: RowRendererId.system_fim,
name: i18n.FIM_NAME,
description: i18n.FIM_DESCRIPTION_PART1,
example: SystemFimExample,
searchableDescription: i18n.FIM_DESCRIPTION_PART1,
},
{
id: RowRendererId.system_file,
name: i18n.FILE_NAME,
description: i18n.FILE_DESCRIPTION_PART1,
example: SystemFileExample,
searchableDescription: i18n.FILE_DESCRIPTION_PART1,
},
{
id: RowRendererId.system_socket,
name: i18n.SOCKET_NAME,
description: (
<div>
<p>{i18n.SOCKET_DESCRIPTION_PART1}</p>
<br />
<p>{i18n.SOCKET_DESCRIPTION_PART2}</p>
</div>
),
example: SystemSocketExample,
searchableDescription: `${i18n.SOCKET_DESCRIPTION_PART1} ${i18n.SOCKET_DESCRIPTION_PART2}`,
},
{
id: RowRendererId.suricata,
name: 'Suricata',
description: (
<p>
{i18n.SURICATA_DESCRIPTION_PART1}{' '}
<Link url="https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-module-suricata.html">
{i18n.SURICATA_NAME}
</Link>{' '}
{i18n.SURICATA_DESCRIPTION_PART2}
</p>
),
example: SuricataExample,
searchableDescription: `${i18n.SURICATA_DESCRIPTION_PART1} ${i18n.SURICATA_NAME} ${i18n.SURICATA_DESCRIPTION_PART2}`,
},
{
id: RowRendererId.zeek,
name: i18n.ZEEK_NAME,
description: (
<p>
{i18n.ZEEK_DESCRIPTION_PART1}{' '}
<Link url="https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-module-zeek.html">
{i18n.ZEEK_NAME}
</Link>{' '}
{i18n.ZEEK_DESCRIPTION_PART2}
</p>
),
example: ZeekExample,
searchableDescription: `${i18n.ZEEK_DESCRIPTION_PART1} ${i18n.ZEEK_NAME} ${i18n.ZEEK_DESCRIPTION_PART2}`,
},
];

View file

@ -0,0 +1,215 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
export const AUDITD_NAME = i18n.translate('xpack.securitySolution.eventRenderers.auditdName', {
defaultMessage: 'Auditd',
});
export const AUDITD_DESCRIPTION_PART1 = i18n.translate(
'xpack.securitySolution.eventRenderers.auditdDescriptionPart1',
{
defaultMessage: 'audit events convey security-relevant logs from the Linux Audit Framework.',
}
);
export const AUDITD_FILE_NAME = i18n.translate(
'xpack.securitySolution.eventRenderers.auditdFileName',
{
defaultMessage: 'Auditd File',
}
);
export const AUDITD_FILE_DESCRIPTION_PART1 = i18n.translate(
'xpack.securitySolution.eventRenderers.auditdFileDescriptionPart1',
{
defaultMessage:
'File events show users (and system accounts) performing CRUD operations on files via specific processes.',
}
);
export const AUTHENTICATION_NAME = i18n.translate(
'xpack.securitySolution.eventRenderers.authenticationName',
{
defaultMessage: 'Authentication',
}
);
export const AUTHENTICATION_DESCRIPTION_PART1 = i18n.translate(
'xpack.securitySolution.eventRenderers.authenticationDescriptionPart1',
{
defaultMessage:
'Authentication events show users (and system accounts) successfully or unsuccessfully logging into hosts.',
}
);
export const AUTHENTICATION_DESCRIPTION_PART2 = i18n.translate(
'xpack.securitySolution.eventRenderers.authenticationDescriptionPart2',
{
defaultMessage:
'Some authentication events may include additional details when users authenticate on behalf of other users.',
}
);
export const DNS_NAME = i18n.translate('xpack.securitySolution.eventRenderers.dnsName', {
defaultMessage: 'Domain Name System (DNS)',
});
export const DNS_DESCRIPTION_PART1 = i18n.translate(
'xpack.securitySolution.eventRenderers.dnsDescriptionPart1',
{
defaultMessage:
'Domain Name System (DNS) events show users (and system accounts) making requests via specific processes to translate from host names to IP addresses.',
}
);
export const FILE_NAME = i18n.translate('xpack.securitySolution.eventRenderers.fileName', {
defaultMessage: 'File',
});
export const FILE_DESCRIPTION_PART1 = i18n.translate(
'xpack.securitySolution.eventRenderers.fileDescriptionPart1',
{
defaultMessage:
'File events show users (and system accounts) performing CRUD operations on files via specific processes.',
}
);
export const FIM_NAME = i18n.translate('xpack.securitySolution.eventRenderers.fimName', {
defaultMessage: 'File Integrity Module (FIM)',
});
export const FIM_DESCRIPTION_PART1 = i18n.translate(
'xpack.securitySolution.eventRenderers.fimDescriptionPart1',
{
defaultMessage:
'File Integrity Module (FIM) events show users (and system accounts) performing CRUD operations on files via specific processes.',
}
);
export const FLOW_NAME = i18n.translate('xpack.securitySolution.eventRenderers.flowName', {
defaultMessage: 'Flow',
});
export const FLOW_DESCRIPTION_PART1 = i18n.translate(
'xpack.securitySolution.eventRenderers.flowDescriptionPart1',
{
defaultMessage:
"The Flow renderer visualizes the flow of data between a source and destination. It's applicable to many types of events.",
}
);
export const FLOW_DESCRIPTION_PART2 = i18n.translate(
'xpack.securitySolution.eventRenderers.flowDescriptionPart2',
{
defaultMessage:
'The hosts, ports, protocol, direction, duration, amount transferred, process, geographic location, and other details are visualized when available.',
}
);
export const PROCESS = i18n.translate('xpack.securitySolution.eventRenderers.processName', {
defaultMessage: 'Process',
});
export const PROCESS_DESCRIPTION_PART1 = i18n.translate(
'xpack.securitySolution.eventRenderers.processDescriptionPart1',
{
defaultMessage:
'Process events show users (and system accounts) starting and stopping processes.',
}
);
export const PROCESS_DESCRIPTION_PART2 = i18n.translate(
'xpack.securitySolution.eventRenderers.processDescriptionPart2',
{
defaultMessage:
'Details including the command line arguments, parent process, and if applicable, file hashes are displayed when available.',
}
);
export const SOCKET_NAME = i18n.translate('xpack.securitySolution.eventRenderers.socketName', {
defaultMessage: 'Socket (Network)',
});
export const SOCKET_DESCRIPTION_PART1 = i18n.translate(
'xpack.securitySolution.eventRenderers.socketDescriptionPart1',
{
defaultMessage:
'Socket (Network) events show processes listening, accepting, and closing connections.',
}
);
export const SOCKET_DESCRIPTION_PART2 = i18n.translate(
'xpack.securitySolution.eventRenderers.socketDescriptionPart2',
{
defaultMessage:
'Details including the protocol, ports, and a community ID for correlating all network events related to a single flow are displayed when available.',
}
);
export const SURICATA_NAME = i18n.translate('xpack.securitySolution.eventRenderers.suricataName', {
defaultMessage: 'Suricata',
});
export const SURICATA_DESCRIPTION_PART1 = i18n.translate(
'xpack.securitySolution.eventRenderers.suricataDescriptionPart1',
{
defaultMessage: 'Summarizes',
}
);
export const SURICATA_DESCRIPTION_PART2 = i18n.translate(
'xpack.securitySolution.eventRenderers.suricataDescriptionPart2',
{
defaultMessage:
'intrusion detection (IDS), inline intrusion prevention (IPS), and network security monitoring (NSM) events',
}
);
export const SYSTEM_NAME = i18n.translate('xpack.securitySolution.eventRenderers.systemName', {
defaultMessage: 'System',
});
export const SYSTEM_DESCRIPTION_PART1 = i18n.translate(
'xpack.securitySolution.eventRenderers.systemDescriptionPart1',
{
defaultMessage: 'The Auditbeat',
}
);
export const SYSTEM_DESCRIPTION_PART2 = i18n.translate(
'xpack.securitySolution.eventRenderers.systemDescriptionPart2',
{
defaultMessage: 'module collects various security related information about a system.',
}
);
export const SYSTEM_DESCRIPTION_PART3 = i18n.translate(
'xpack.securitySolution.eventRenderers.systemDescriptionPart3',
{
defaultMessage:
'All datasets send both periodic state information (e.g. all currently running processes) and real-time changes (e.g. when a new process starts or stops).',
}
);
export const ZEEK_NAME = i18n.translate('xpack.securitySolution.eventRenderers.zeekName', {
defaultMessage: 'Zeek (formerly Bro)',
});
export const ZEEK_DESCRIPTION_PART1 = i18n.translate(
'xpack.securitySolution.eventRenderers.zeekDescriptionPart1',
{
defaultMessage: 'Summarizes events from the',
}
);
export const ZEEK_DESCRIPTION_PART2 = i18n.translate(
'xpack.securitySolution.eventRenderers.zeekDescriptionPart2',
{
defaultMessage: 'Network Security Monitoring (NSM) tool',
}
);

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export const ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID = 'row-renderer-example';

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { mockTimelineData } from '../../../../common/mock/mock_timeline_data';
import { createGenericAuditRowRenderer } from '../../timeline/body/renderers/auditd/generic_row_renderer';
import { CONNECTED_USING } from '../../timeline/body/renderers/auditd/translations';
import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants';
const AuditdExampleComponent: React.FC = () => {
const auditdRowRenderer = createGenericAuditRowRenderer({
actionName: 'connected-to',
text: CONNECTED_USING,
});
return (
<>
{auditdRowRenderer.renderRow({
browserFields: {},
data: mockTimelineData[26].ecs,
timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
})}
</>
);
};
export const AuditdExample = React.memo(AuditdExampleComponent);

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { mockTimelineData } from '../../../../common/mock/mock_timeline_data';
import { createGenericFileRowRenderer } from '../../timeline/body/renderers/auditd/generic_row_renderer';
import { OPENED_FILE, USING } from '../../timeline/body/renderers/auditd/translations';
import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants';
const AuditdFileExampleComponent: React.FC = () => {
const auditdFileRowRenderer = createGenericFileRowRenderer({
actionName: 'opened-file',
text: `${OPENED_FILE} ${USING}`,
});
return (
<>
{auditdFileRowRenderer.renderRow({
browserFields: {},
data: mockTimelineData[27].ecs,
timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
})}
</>
);
};
export const AuditdFileExample = React.memo(AuditdFileExampleComponent);

View file

@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './auditd';
export * from './auditd_file';
export * from './netflow';
export * from './suricata';
export * from './system';
export * from './system_dns';
export * from './system_endgame_process';
export * from './system_file';
export * from './system_fim';
export * from './system_security_event';
export * from './system_socket';
export * from './zeek';

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { getMockNetflowData } from '../../../../common/mock/netflow';
import { netflowRowRenderer } from '../../timeline/body/renderers/netflow/netflow_row_renderer';
import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants';
const NetflowExampleComponent: React.FC = () => (
<>
{netflowRowRenderer.renderRow({
browserFields: {},
data: getMockNetflowData(),
timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
})}
</>
);
export const NetflowExample = React.memo(NetflowExampleComponent);

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { mockTimelineData } from '../../../../common/mock/mock_timeline_data';
import { suricataRowRenderer } from '../../timeline/body/renderers/suricata/suricata_row_renderer';
import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants';
const SuricataExampleComponent: React.FC = () => (
<>
{suricataRowRenderer.renderRow({
browserFields: {},
data: mockTimelineData[2].ecs,
timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
})}
</>
);
export const SuricataExample = React.memo(SuricataExampleComponent);

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { TERMINATED_PROCESS } from '../../timeline/body/renderers/system/translations';
import { createGenericSystemRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer';
import { mockEndgameTerminationEvent } from '../../../../common/mock/mock_endgame_ecs_data';
import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants';
const SystemExampleComponent: React.FC = () => {
const systemRowRenderer = createGenericSystemRowRenderer({
actionName: 'termination_event',
text: TERMINATED_PROCESS,
});
return (
<>
{systemRowRenderer.renderRow({
browserFields: {},
data: mockEndgameTerminationEvent,
timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
})}
</>
);
};
export const SystemExample = React.memo(SystemExampleComponent);

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { createDnsRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer';
import { mockEndgameDnsRequest } from '../../../../common/mock/mock_endgame_ecs_data';
import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants';
const SystemDnsExampleComponent: React.FC = () => {
const systemDnsRowRenderer = createDnsRowRenderer();
return (
<>
{systemDnsRowRenderer.renderRow({
browserFields: {},
data: mockEndgameDnsRequest,
timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
})}
</>
);
};
export const SystemDnsExample = React.memo(SystemDnsExampleComponent);

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { createEndgameProcessRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer';
import { mockEndgameCreationEvent } from '../../../../common/mock/mock_endgame_ecs_data';
import { PROCESS_STARTED } from '../../timeline/body/renderers/system/translations';
import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants';
const SystemEndgameProcessExampleComponent: React.FC = () => {
const systemEndgameProcessRowRenderer = createEndgameProcessRowRenderer({
actionName: 'creation_event',
text: PROCESS_STARTED,
});
return (
<>
{systemEndgameProcessRowRenderer.renderRow({
browserFields: {},
data: mockEndgameCreationEvent,
timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
})}
</>
);
};
export const SystemEndgameProcessExample = React.memo(SystemEndgameProcessExampleComponent);

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { mockEndgameFileDeleteEvent } from '../../../../common/mock/mock_endgame_ecs_data';
import { createGenericFileRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer';
import { DELETED_FILE } from '../../timeline/body/renderers/system/translations';
import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants';
const SystemFileExampleComponent: React.FC = () => {
const systemFileRowRenderer = createGenericFileRowRenderer({
actionName: 'file_delete_event',
text: DELETED_FILE,
});
return (
<>
{systemFileRowRenderer.renderRow({
browserFields: {},
data: mockEndgameFileDeleteEvent,
timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
})}
</>
);
};
export const SystemFileExample = React.memo(SystemFileExampleComponent);

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { mockEndgameFileCreateEvent } from '../../../../common/mock/mock_endgame_ecs_data';
import { createFimRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer';
import { CREATED_FILE } from '../../timeline/body/renderers/system/translations';
import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants';
const SystemFimExampleComponent: React.FC = () => {
const systemFimRowRenderer = createFimRowRenderer({
actionName: 'file_create_event',
text: CREATED_FILE,
});
return (
<>
{systemFimRowRenderer.renderRow({
browserFields: {},
data: mockEndgameFileCreateEvent,
timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
})}
</>
);
};
export const SystemFimExample = React.memo(SystemFimExampleComponent);

View file

@ -0,0 +1,28 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { createSecurityEventRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer';
import { mockEndgameUserLogon } from '../../../../common/mock/mock_endgame_ecs_data';
import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants';
const SystemSecurityEventExampleComponent: React.FC = () => {
const systemSecurityEventRowRenderer = createSecurityEventRowRenderer({
actionName: 'user_logon',
});
return (
<>
{systemSecurityEventRowRenderer.renderRow({
browserFields: {},
data: mockEndgameUserLogon,
timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
})}
</>
);
};
export const SystemSecurityEventExample = React.memo(SystemSecurityEventExampleComponent);

View file

@ -0,0 +1,29 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { ACCEPTED_A_CONNECTION_VIA } from '../../timeline/body/renderers/system/translations';
import { createSocketRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer';
import { mockEndgameIpv4ConnectionAcceptEvent } from '../../../../common/mock/mock_endgame_ecs_data';
import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants';
const SystemSocketExampleComponent: React.FC = () => {
const systemSocketRowRenderer = createSocketRowRenderer({
actionName: 'ipv4_connection_accept_event',
text: ACCEPTED_A_CONNECTION_VIA,
});
return (
<>
{systemSocketRowRenderer.renderRow({
browserFields: {},
data: mockEndgameIpv4ConnectionAcceptEvent,
timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
})}
</>
);
};
export const SystemSocketExample = React.memo(SystemSocketExampleComponent);

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { mockTimelineData } from '../../../../common/mock/mock_timeline_data';
import { zeekRowRenderer } from '../../timeline/body/renderers/zeek/zeek_row_renderer';
import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants';
const ZeekExampleComponent: React.FC = () => (
<>
{zeekRowRenderer.renderRow({
browserFields: {},
data: mockTimelineData[13].ecs,
timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
})}
</>
);
export const ZeekExample = React.memo(ZeekExampleComponent);

View file

@ -0,0 +1,182 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
EuiButtonEmpty,
EuiButtonIcon,
EuiText,
EuiToolTip,
EuiOverlayMask,
EuiModal,
EuiModalHeader,
EuiModalHeaderTitle,
EuiModalBody,
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiInMemoryTable,
} from '@elastic/eui';
import React, { useState, useCallback, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import { State } from '../../../common/store';
import { renderers } from './catalog';
import { setExcludedRowRendererIds as dispatchSetExcludedRowRendererIds } from '../../../timelines/store/timeline/actions';
import { RowRenderersBrowser } from './row_renderers_browser';
import * as i18n from './translations';
const StyledEuiModal = styled(EuiModal)`
margin: 0 auto;
max-width: 95vw;
min-height: 95vh;
> .euiModal__flex {
max-height: 95vh;
}
`;
const StyledEuiModalBody = styled(EuiModalBody)`
.euiModalBody__overflow {
display: flex;
align-items: stretch;
overflow: hidden;
> div {
display: flex;
flex-direction: column;
flex: 1;
> div:first-child {
flex: 0;
}
.euiBasicTable {
flex: 1;
overflow: auto;
}
}
}
`;
const StyledEuiOverlayMask = styled(EuiOverlayMask)`
z-index: 8001;
padding-bottom: 0;
> div {
width: 100%;
}
`;
interface StatefulRowRenderersBrowserProps {
timelineId: string;
}
const StatefulRowRenderersBrowserComponent: React.FC<StatefulRowRenderersBrowserProps> = ({
timelineId,
}) => {
const tableRef = useRef<EuiInMemoryTable<{}>>();
const dispatch = useDispatch();
const excludedRowRendererIds = useSelector(
(state: State) => state.timeline.timelineById[timelineId]?.excludedRowRendererIds || []
);
const [show, setShow] = useState(false);
const setExcludedRowRendererIds = useCallback(
(payload) =>
dispatch(
dispatchSetExcludedRowRendererIds({
id: timelineId,
excludedRowRendererIds: payload,
})
),
[dispatch, timelineId]
);
const toggleShow = useCallback(() => setShow(!show), [show]);
const hideFieldBrowser = useCallback(() => setShow(false), []);
const handleDisableAll = useCallback(() => {
// eslint-disable-next-line no-unused-expressions
tableRef?.current?.setSelection([]);
}, [tableRef]);
const handleEnableAll = useCallback(() => {
// eslint-disable-next-line no-unused-expressions
tableRef?.current?.setSelection(renderers);
}, [tableRef]);
return (
<>
<EuiToolTip content={i18n.CUSTOMIZE_EVENT_RENDERERS_TITLE}>
<EuiButtonIcon
aria-label={i18n.CUSTOMIZE_EVENT_RENDERERS_TITLE}
data-test-subj="show-row-renderers-gear"
iconType="gear"
onClick={toggleShow}
>
{i18n.EVENT_RENDERERS_TITLE}
</EuiButtonIcon>
</EuiToolTip>
{show && (
<StyledEuiOverlayMask>
<StyledEuiModal onClose={hideFieldBrowser}>
<EuiModalHeader>
<EuiFlexGroup
alignItems="center"
justifyContent="spaceBetween"
direction="row"
gutterSize="none"
>
<EuiFlexItem grow={false}>
<EuiModalHeaderTitle>{i18n.CUSTOMIZE_EVENT_RENDERERS_TITLE}</EuiModalHeaderTitle>
<EuiText size="s">{i18n.CUSTOMIZE_EVENT_RENDERERS_DESCRIPTION}</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="s"
data-test-subj="disable-all"
onClick={handleDisableAll}
>
{i18n.DISABLE_ALL}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
fill
size="s"
data-test-subj="enable-all"
onClick={handleEnableAll}
>
{i18n.ENABLE_ALL}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiModalHeader>
<StyledEuiModalBody>
<RowRenderersBrowser
ref={tableRef}
excludedRowRendererIds={excludedRowRendererIds}
setExcludedRowRendererIds={setExcludedRowRendererIds}
/>
</StyledEuiModalBody>
</StyledEuiModal>
</StyledEuiOverlayMask>
)}
</>
);
};
export const StatefulRowRenderersBrowser = React.memo(StatefulRowRenderersBrowserComponent);

View file

@ -0,0 +1,179 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexItem, EuiInMemoryTable } from '@elastic/eui';
import React, { useMemo, useCallback } from 'react';
import { xor, xorBy } from 'lodash/fp';
import styled from 'styled-components';
import { RowRendererId } from '../../../../common/types/timeline';
import { renderers, RowRendererOption } from './catalog';
interface RowRenderersBrowserProps {
excludedRowRendererIds: RowRendererId[];
setExcludedRowRendererIds: (excludedRowRendererIds: RowRendererId[]) => void;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const StyledEuiInMemoryTable = styled(EuiInMemoryTable as any)`
.euiTable {
tr > *:last-child {
display: none;
}
.euiTableHeaderCellCheckbox > .euiTableCellContent {
display: none; // we don't want to display checkbox in the table
}
}
`;
const StyledEuiFlexItem = styled(EuiFlexItem)`
overflow: auto;
> div {
padding: 0;
> div {
margin: 0;
}
}
`;
const ExampleWrapperComponent = (Example?: React.ElementType) => {
if (!Example) return;
return (
<StyledEuiFlexItem grow={1}>
<Example />
</StyledEuiFlexItem>
);
};
const search = {
box: {
incremental: true,
schema: true,
},
};
/**
* Since `searchableDescription` contains raw text to power the Search bar,
* this "noop" function ensures it's not actually rendered
*/
const renderSearchableDescriptionNoop = () => <></>;
const initialSorting = {
sort: {
field: 'name',
direction: 'asc',
},
};
const StyledNameButton = styled.button`
text-align: left;
`;
const RowRenderersBrowserComponent = React.forwardRef(
({ excludedRowRendererIds = [], setExcludedRowRendererIds }: RowRenderersBrowserProps, ref) => {
const notExcludedRowRenderers = useMemo(() => {
if (excludedRowRendererIds.length === Object.keys(RowRendererId).length) return [];
return renderers.filter((renderer) => !excludedRowRendererIds.includes(renderer.id));
}, [excludedRowRendererIds]);
const handleNameClick = useCallback(
(item: RowRendererOption) => () => {
const newSelection = xor([item], notExcludedRowRenderers);
// @ts-ignore
ref?.current?.setSelection(newSelection); // eslint-disable-line no-unused-expressions
},
[notExcludedRowRenderers, ref]
);
const nameColumnRenderCallback = useCallback(
(value, item) => (
<StyledNameButton className="kbn-resetFocusState" onClick={handleNameClick(item)}>
{value}
</StyledNameButton>
),
[handleNameClick]
);
const columns = useMemo(
() => [
{
field: 'name',
name: 'Name',
sortable: true,
width: '10%',
render: nameColumnRenderCallback,
},
{
field: 'description',
name: 'Description',
width: '25%',
render: (description: React.ReactNode) => description,
},
{
field: 'example',
name: 'Example',
width: '65%',
render: ExampleWrapperComponent,
},
{
field: 'searchableDescription',
name: 'Searchable Description',
sortable: false,
width: '0px',
render: renderSearchableDescriptionNoop,
},
],
[nameColumnRenderCallback]
);
const handleSelectable = useCallback(() => true, []);
const handleSelectionChange = useCallback(
(selection: RowRendererOption[]) => {
if (!selection || !selection.length)
return setExcludedRowRendererIds(Object.values(RowRendererId));
const excludedRowRenderers = xorBy('id', renderers, selection);
setExcludedRowRendererIds(excludedRowRenderers.map((rowRenderer) => rowRenderer.id));
},
[setExcludedRowRendererIds]
);
const selectionValue = useMemo(
() => ({
selectable: handleSelectable,
onSelectionChange: handleSelectionChange,
initialSelected: notExcludedRowRenderers,
}),
[handleSelectable, handleSelectionChange, notExcludedRowRenderers]
);
return (
<StyledEuiInMemoryTable
ref={ref}
items={renderers}
itemId="id"
columns={columns}
search={search}
sorting={initialSorting}
isSelectable={true}
selection={selectionValue}
/>
);
}
);
RowRenderersBrowserComponent.displayName = 'RowRenderersBrowserComponent';
export const RowRenderersBrowser = React.memo(RowRenderersBrowserComponent);
RowRenderersBrowser.displayName = 'RowRenderersBrowser';

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
export const EVENT_RENDERERS_TITLE = i18n.translate(
'xpack.securitySolution.customizeEventRenderers.eventRenderersTitle',
{
defaultMessage: 'Event Renderers',
}
);
export const CUSTOMIZE_EVENT_RENDERERS_TITLE = i18n.translate(
'xpack.securitySolution.customizeEventRenderers.customizeEventRenderersTitle',
{
defaultMessage: 'Customize Event Renderers',
}
);
export const CUSTOMIZE_EVENT_RENDERERS_DESCRIPTION = i18n.translate(
'xpack.securitySolution.customizeEventRenderers.customizeEventRenderersDescription',
{
defaultMessage:
'Event Renderers automatically convey the most relevant details in an event to reveal its story',
}
);
export const ENABLE_ALL = i18n.translate(
'xpack.securitySolution.customizeEventRenderers.enableAllRenderersButtonLabel',
{
defaultMessage: 'Enable all',
}
);
export const DISABLE_ALL = i18n.translate(
'xpack.securitySolution.customizeEventRenderers.disableAllRenderersButtonLabel',
{
defaultMessage: 'Disable all',
}
);

View file

@ -119,9 +119,9 @@ export const Actions = React.memo<Props>(
<EventsTd>
<EventsTdContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}>
{loading && <EventsLoading />}
{!loading && (
{loading ? (
<EventsLoading />
) : (
<EuiButtonIcon
aria-label={expanded ? i18n.COLLAPSE : i18n.EXPAND}
data-test-subj="expand-event"

View file

@ -8,479 +8,481 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = `
<styled.div
actionsColumnWidth={76}
data-test-subj="actions-container"
isEventViewer={false}
>
<styled.div>
<styled.div
textAlign="center"
width={24}
>
<Connect(Component)
browserFields={
<Memo(StatefulFieldsBrowserComponent)
browserFields={
Object {
"agent": Object {
"fields": Object {
"agent.ephemeral_id": Object {
"aggregatable": true,
"category": "agent",
"description": "Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but \`agent.id\` does not.",
"example": "8a4f500f",
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "agent.ephemeral_id",
"searchable": true,
"type": "string",
},
"agent.hostname": Object {
"aggregatable": true,
"category": "agent",
"description": null,
"example": null,
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "agent.hostname",
"searchable": true,
"type": "string",
},
"agent.id": Object {
"aggregatable": true,
"category": "agent",
"description": "Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.",
"example": "8a4f500d",
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "agent.id",
"searchable": true,
"type": "string",
},
"agent.name": Object {
"aggregatable": true,
"category": "agent",
"description": "Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.",
"example": "foo",
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "agent.name",
"searchable": true,
"type": "string",
},
},
},
"auditd": Object {
"fields": Object {
"auditd.data.a0": Object {
"aggregatable": true,
"category": "auditd",
"description": null,
"example": null,
"format": "",
"indexes": Array [
"auditbeat",
],
"name": "auditd.data.a0",
"searchable": true,
"type": "string",
},
"auditd.data.a1": Object {
"aggregatable": true,
"category": "auditd",
"description": null,
"example": null,
"format": "",
"indexes": Array [
"auditbeat",
],
"name": "auditd.data.a1",
"searchable": true,
"type": "string",
},
"auditd.data.a2": Object {
"aggregatable": true,
"category": "auditd",
"description": null,
"example": null,
"format": "",
"indexes": Array [
"auditbeat",
],
"name": "auditd.data.a2",
"searchable": true,
"type": "string",
},
},
},
"base": Object {
"fields": Object {
"@timestamp": Object {
"aggregatable": true,
"category": "base",
"description": "Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.",
"example": "2016-05-23T08:05:34.853Z",
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "@timestamp",
"searchable": true,
"type": "date",
},
},
},
"client": Object {
"fields": Object {
"client.address": Object {
"aggregatable": true,
"category": "client",
"description": "Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the \`.address\` field. Then it should be duplicated to \`.ip\` or \`.domain\`, depending on which one it is.",
"example": null,
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "client.address",
"searchable": true,
"type": "string",
},
"client.bytes": Object {
"aggregatable": true,
"category": "client",
"description": "Bytes sent from the client to the server.",
"example": "184",
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "client.bytes",
"searchable": true,
"type": "number",
},
"client.domain": Object {
"aggregatable": true,
"category": "client",
"description": "Client domain.",
"example": null,
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "client.domain",
"searchable": true,
"type": "string",
},
"client.geo.country_iso_code": Object {
"aggregatable": true,
"category": "client",
"description": "Country ISO code.",
"example": "CA",
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "client.geo.country_iso_code",
"searchable": true,
"type": "string",
},
},
},
"cloud": Object {
"fields": Object {
"cloud.account.id": Object {
"aggregatable": true,
"category": "cloud",
"description": "The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.",
"example": "666777888999",
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "cloud.account.id",
"searchable": true,
"type": "string",
},
"cloud.availability_zone": Object {
"aggregatable": true,
"category": "cloud",
"description": "Availability zone in which this host is running.",
"example": "us-east-1c",
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "cloud.availability_zone",
"searchable": true,
"type": "string",
},
},
},
"container": Object {
"fields": Object {
"container.id": Object {
"aggregatable": true,
"category": "container",
"description": "Unique container id.",
"example": null,
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "container.id",
"searchable": true,
"type": "string",
},
"container.image.name": Object {
"aggregatable": true,
"category": "container",
"description": "Name of the image the container was built on.",
"example": null,
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "container.image.name",
"searchable": true,
"type": "string",
},
"container.image.tag": Object {
"aggregatable": true,
"category": "container",
"description": "Container image tag.",
"example": null,
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "container.image.tag",
"searchable": true,
"type": "string",
},
},
},
"destination": Object {
"fields": Object {
"destination.address": Object {
"aggregatable": true,
"category": "destination",
"description": "Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the \`.address\` field. Then it should be duplicated to \`.ip\` or \`.domain\`, depending on which one it is.",
"example": null,
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "destination.address",
"searchable": true,
"type": "string",
},
"destination.bytes": Object {
"aggregatable": true,
"category": "destination",
"description": "Bytes sent from the destination to the source.",
"example": "184",
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "destination.bytes",
"searchable": true,
"type": "number",
},
"destination.domain": Object {
"aggregatable": true,
"category": "destination",
"description": "Destination domain.",
"example": null,
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "destination.domain",
"searchable": true,
"type": "string",
},
"destination.ip": Object {
"aggregatable": true,
"category": "destination",
"description": "IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.",
"example": "",
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "destination.ip",
"searchable": true,
"type": "ip",
},
"destination.port": Object {
"aggregatable": true,
"category": "destination",
"description": "Port of the destination.",
"example": "",
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "destination.port",
"searchable": true,
"type": "long",
},
},
},
"event": Object {
"fields": Object {
"event.end": Object {
"aggregatable": true,
"category": "event",
"description": "event.end contains the date when the event ended or when the activity was last observed.",
"example": null,
"format": "",
"indexes": Array [
"apm-*-transaction*",
"auditbeat-*",
"endgame-*",
"filebeat-*",
"packetbeat-*",
"winlogbeat-*",
"logs-*",
],
"name": "event.end",
"searchable": true,
"type": "date",
},
},
},
"source": Object {
"fields": Object {
"source.ip": Object {
"aggregatable": true,
"category": "source",
"description": "IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.",
"example": "",
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "source.ip",
"searchable": true,
"type": "ip",
},
"source.port": Object {
"aggregatable": true,
"category": "source",
"description": "Port of the source.",
"example": "",
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "source.port",
"searchable": true,
"type": "long",
},
},
},
}
}
columnHeaders={
Array [
Object {
"agent": Object {
"fields": Object {
"agent.ephemeral_id": Object {
"aggregatable": true,
"category": "agent",
"description": "Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but \`agent.id\` does not.",
"example": "8a4f500f",
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "agent.ephemeral_id",
"searchable": true,
"type": "string",
},
"agent.hostname": Object {
"aggregatable": true,
"category": "agent",
"description": null,
"example": null,
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "agent.hostname",
"searchable": true,
"type": "string",
},
"agent.id": Object {
"aggregatable": true,
"category": "agent",
"description": "Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.",
"example": "8a4f500d",
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "agent.id",
"searchable": true,
"type": "string",
},
"agent.name": Object {
"aggregatable": true,
"category": "agent",
"description": "Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.",
"example": "foo",
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "agent.name",
"searchable": true,
"type": "string",
},
},
},
"auditd": Object {
"fields": Object {
"auditd.data.a0": Object {
"aggregatable": true,
"category": "auditd",
"description": null,
"example": null,
"format": "",
"indexes": Array [
"auditbeat",
],
"name": "auditd.data.a0",
"searchable": true,
"type": "string",
},
"auditd.data.a1": Object {
"aggregatable": true,
"category": "auditd",
"description": null,
"example": null,
"format": "",
"indexes": Array [
"auditbeat",
],
"name": "auditd.data.a1",
"searchable": true,
"type": "string",
},
"auditd.data.a2": Object {
"aggregatable": true,
"category": "auditd",
"description": null,
"example": null,
"format": "",
"indexes": Array [
"auditbeat",
],
"name": "auditd.data.a2",
"searchable": true,
"type": "string",
},
},
},
"base": Object {
"fields": Object {
"@timestamp": Object {
"aggregatable": true,
"category": "base",
"description": "Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.",
"example": "2016-05-23T08:05:34.853Z",
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "@timestamp",
"searchable": true,
"type": "date",
},
},
},
"client": Object {
"fields": Object {
"client.address": Object {
"aggregatable": true,
"category": "client",
"description": "Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the \`.address\` field. Then it should be duplicated to \`.ip\` or \`.domain\`, depending on which one it is.",
"example": null,
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "client.address",
"searchable": true,
"type": "string",
},
"client.bytes": Object {
"aggregatable": true,
"category": "client",
"description": "Bytes sent from the client to the server.",
"example": "184",
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "client.bytes",
"searchable": true,
"type": "number",
},
"client.domain": Object {
"aggregatable": true,
"category": "client",
"description": "Client domain.",
"example": null,
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "client.domain",
"searchable": true,
"type": "string",
},
"client.geo.country_iso_code": Object {
"aggregatable": true,
"category": "client",
"description": "Country ISO code.",
"example": "CA",
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "client.geo.country_iso_code",
"searchable": true,
"type": "string",
},
},
},
"cloud": Object {
"fields": Object {
"cloud.account.id": Object {
"aggregatable": true,
"category": "cloud",
"description": "The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.",
"example": "666777888999",
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "cloud.account.id",
"searchable": true,
"type": "string",
},
"cloud.availability_zone": Object {
"aggregatable": true,
"category": "cloud",
"description": "Availability zone in which this host is running.",
"example": "us-east-1c",
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "cloud.availability_zone",
"searchable": true,
"type": "string",
},
},
},
"container": Object {
"fields": Object {
"container.id": Object {
"aggregatable": true,
"category": "container",
"description": "Unique container id.",
"example": null,
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "container.id",
"searchable": true,
"type": "string",
},
"container.image.name": Object {
"aggregatable": true,
"category": "container",
"description": "Name of the image the container was built on.",
"example": null,
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "container.image.name",
"searchable": true,
"type": "string",
},
"container.image.tag": Object {
"aggregatable": true,
"category": "container",
"description": "Container image tag.",
"example": null,
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "container.image.tag",
"searchable": true,
"type": "string",
},
},
},
"destination": Object {
"fields": Object {
"destination.address": Object {
"aggregatable": true,
"category": "destination",
"description": "Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the \`.address\` field. Then it should be duplicated to \`.ip\` or \`.domain\`, depending on which one it is.",
"example": null,
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "destination.address",
"searchable": true,
"type": "string",
},
"destination.bytes": Object {
"aggregatable": true,
"category": "destination",
"description": "Bytes sent from the destination to the source.",
"example": "184",
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "destination.bytes",
"searchable": true,
"type": "number",
},
"destination.domain": Object {
"aggregatable": true,
"category": "destination",
"description": "Destination domain.",
"example": null,
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "destination.domain",
"searchable": true,
"type": "string",
},
"destination.ip": Object {
"aggregatable": true,
"category": "destination",
"description": "IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.",
"example": "",
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "destination.ip",
"searchable": true,
"type": "ip",
},
"destination.port": Object {
"aggregatable": true,
"category": "destination",
"description": "Port of the destination.",
"example": "",
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "destination.port",
"searchable": true,
"type": "long",
},
},
},
"event": Object {
"fields": Object {
"event.end": Object {
"aggregatable": true,
"category": "event",
"description": "event.end contains the date when the event ended or when the activity was last observed.",
"example": null,
"format": "",
"indexes": Array [
"apm-*-transaction*",
"auditbeat-*",
"endgame-*",
"filebeat-*",
"packetbeat-*",
"winlogbeat-*",
"logs-*",
],
"name": "event.end",
"searchable": true,
"type": "date",
},
},
},
"source": Object {
"fields": Object {
"source.ip": Object {
"aggregatable": true,
"category": "source",
"description": "IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.",
"example": "",
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "source.ip",
"searchable": true,
"type": "ip",
},
"source.port": Object {
"aggregatable": true,
"category": "source",
"description": "Port of the source.",
"example": "",
"format": "",
"indexes": Array [
"auditbeat",
"filebeat",
"packetbeat",
],
"name": "source.port",
"searchable": true,
"type": "long",
},
},
},
}
}
columnHeaders={
Array [
Object {
"columnHeaderType": "not-filtered",
"id": "@timestamp",
"width": 190,
},
Object {
"columnHeaderType": "not-filtered",
"id": "message",
"width": 180,
},
Object {
"columnHeaderType": "not-filtered",
"id": "event.category",
"width": 180,
},
Object {
"columnHeaderType": "not-filtered",
"id": "event.action",
"width": 180,
},
Object {
"columnHeaderType": "not-filtered",
"id": "host.name",
"width": 180,
},
Object {
"columnHeaderType": "not-filtered",
"id": "source.ip",
"width": 180,
},
Object {
"columnHeaderType": "not-filtered",
"id": "destination.ip",
"width": 180,
},
Object {
"columnHeaderType": "not-filtered",
"id": "user.name",
"width": 180,
},
]
}
data-test-subj="field-browser"
height={300}
isEventViewer={false}
onUpdateColumns={[MockFunction]}
timelineId="test"
toggleColumn={[MockFunction]}
width={900}
/>
</styled.div>
"columnHeaderType": "not-filtered",
"id": "@timestamp",
"width": 190,
},
Object {
"columnHeaderType": "not-filtered",
"id": "message",
"width": 180,
},
Object {
"columnHeaderType": "not-filtered",
"id": "event.category",
"width": 180,
},
Object {
"columnHeaderType": "not-filtered",
"id": "event.action",
"width": 180,
},
Object {
"columnHeaderType": "not-filtered",
"id": "host.name",
"width": 180,
},
Object {
"columnHeaderType": "not-filtered",
"id": "source.ip",
"width": 180,
},
Object {
"columnHeaderType": "not-filtered",
"id": "destination.ip",
"width": 180,
},
Object {
"columnHeaderType": "not-filtered",
"id": "user.name",
"width": 180,
},
]
}
data-test-subj="field-browser"
height={300}
isEventViewer={false}
onUpdateColumns={[MockFunction]}
timelineId="test"
toggleColumn={[MockFunction]}
width={900}
/>
</styled.div>
<styled.div>
<Memo(StatefulRowRenderersBrowserComponent)
data-test-subj="row-renderers-browser"
timelineId="test"
/>
</styled.div>
</styled.div>
<Connect(Droppable)

View file

@ -10,7 +10,7 @@ import {
DEFAULT_DATE_COLUMN_MIN_WIDTH,
DEFAULT_ACTIONS_COLUMN_WIDTH,
SHOW_CHECK_BOXES_COLUMN_WIDTH,
EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH,
MINIMUM_ACTIONS_COLUMN_WIDTH,
} from '../constants';
describe('helpers', () => {
@ -36,12 +36,12 @@ describe('helpers', () => {
});
test('returns the events viewer actions column width when isEventViewer is true', () => {
expect(getActionsColumnWidth(true)).toEqual(EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH);
expect(getActionsColumnWidth(true)).toEqual(MINIMUM_ACTIONS_COLUMN_WIDTH);
});
test('returns the events viewer actions column width + checkbox width when isEventViewer is true and showCheckboxes is true', () => {
expect(getActionsColumnWidth(true, true)).toEqual(
EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH + SHOW_CHECK_BOXES_COLUMN_WIDTH
MINIMUM_ACTIONS_COLUMN_WIDTH + SHOW_CHECK_BOXES_COLUMN_WIDTH
);
});
});

View file

@ -14,6 +14,7 @@ import {
SHOW_CHECK_BOXES_COLUMN_WIDTH,
EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH,
DEFAULT_ACTIONS_COLUMN_WIDTH,
MINIMUM_ACTIONS_COLUMN_WIDTH,
} from '../constants';
/** Enriches the column headers with field details from the specified browserFields */
@ -42,7 +43,14 @@ export const getActionsColumnWidth = (
isEventViewer: boolean,
showCheckboxes = false,
additionalActionWidth = 0
): number =>
(showCheckboxes ? SHOW_CHECK_BOXES_COLUMN_WIDTH : 0) +
(isEventViewer ? EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH : DEFAULT_ACTIONS_COLUMN_WIDTH) +
additionalActionWidth;
): number => {
const checkboxesWidth = showCheckboxes ? SHOW_CHECK_BOXES_COLUMN_WIDTH : 0;
const actionsColumnWidth =
checkboxesWidth +
(isEventViewer ? EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH : DEFAULT_ACTIONS_COLUMN_WIDTH) +
additionalActionWidth;
return actionsColumnWidth > MINIMUM_ACTIONS_COLUMN_WIDTH + checkboxesWidth
? actionsColumnWidth
: MINIMUM_ACTIONS_COLUMN_WIDTH + checkboxesWidth;
};

View file

@ -18,8 +18,6 @@ import {
DRAG_TYPE_FIELD,
droppableTimelineColumnsPrefix,
} from '../../../../../common/components/drag_and_drop/helpers';
import { StatefulFieldsBrowser } from '../../../fields_browser';
import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from '../../../fields_browser/helpers';
import {
OnColumnRemoved,
OnColumnResized,
@ -29,6 +27,9 @@ import {
OnUpdateColumns,
} from '../../events';
import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers';
import { StatefulFieldsBrowser } from '../../../fields_browser';
import { StatefulRowRenderersBrowser } from '../../../row_renderers_browser';
import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from '../../../fields_browser/helpers';
import {
EventsTh,
EventsThContent,
@ -170,6 +171,7 @@ export const ColumnHeadersComponent = ({
<EventsThGroupActions
actionsColumnWidth={actionsColumnWidth}
data-test-subj="actions-container"
isEventViewer={isEventViewer}
>
{showSelectAllCheckbox && (
<EventsTh>
@ -185,22 +187,23 @@ export const ColumnHeadersComponent = ({
)}
<EventsTh>
<EventsThContent
textAlign={showSelectAllCheckbox ? 'left' : 'center'}
width={DEFAULT_ICON_BUTTON_WIDTH}
>
<StatefulFieldsBrowser
browserFields={browserFields}
columnHeaders={columnHeaders}
data-test-subj="field-browser"
height={FIELD_BROWSER_HEIGHT}
isEventViewer={isEventViewer}
onUpdateColumns={onUpdateColumns}
timelineId={timelineId}
toggleColumn={toggleColumn}
width={FIELD_BROWSER_WIDTH}
/>
</EventsThContent>
<StatefulFieldsBrowser
browserFields={browserFields}
columnHeaders={columnHeaders}
data-test-subj="field-browser"
height={FIELD_BROWSER_HEIGHT}
isEventViewer={isEventViewer}
onUpdateColumns={onUpdateColumns}
timelineId={timelineId}
toggleColumn={toggleColumn}
width={FIELD_BROWSER_WIDTH}
/>
</EventsTh>
<EventsTh>
<StatefulRowRenderersBrowser
data-test-subj="row-renderers-browser"
timelineId={timelineId}
/>
</EventsTh>
{showEventsSelect && (

View file

@ -4,6 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
/** The minimum (fixed) width of the Actions column */
export const MINIMUM_ACTIONS_COLUMN_WIDTH = 50; // px;
/** The (fixed) width of the Actions column */
export const DEFAULT_ACTIONS_COLUMN_WIDTH = 76; // px;
/**

View file

@ -21,7 +21,7 @@ import { Note } from '../../../../../common/lib/note';
import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
import { AssociateNote, UpdateNote } from '../../../notes/helpers';
import { OnColumnResized, OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events';
import { EventsTdContent, EventsTrData } from '../../styles';
import { EventsTd, EventsTdContent, EventsTrData } from '../../styles';
import { Actions } from '../actions';
import { DataDrivenColumns } from '../data_driven_columns';
import { eventHasNotes, getPinOnClick } from '../helpers';
@ -133,22 +133,24 @@ export const EventColumnView = React.memo<Props>(
...acc,
icon: [
...acc.icon,
<EventsTdContent key={action.id} textAlign="center" width={action.width}>
<EuiToolTip
data-test-subj={`${action.dataTestSubj}-tool-tip`}
content={action.content}
>
<EuiButtonIcon
aria-label={action.ariaLabel}
data-test-subj={`${action.dataTestSubj}-button`}
iconType={action.iconType}
isDisabled={
action.isActionDisabled != null ? action.isActionDisabled(ecsData) : false
}
onClick={() => action.onClick({ eventId: id, ecsData, data })}
/>
</EuiToolTip>
</EventsTdContent>,
<EventsTd>
<EventsTdContent key={action.id} textAlign="center" width={action.width}>
<EuiToolTip
data-test-subj={`${action.dataTestSubj}-tool-tip`}
content={action.content}
>
<EuiButtonIcon
aria-label={action.ariaLabel}
data-test-subj={`${action.dataTestSubj}-button`}
iconType={action.iconType}
isDisabled={
action.isActionDisabled != null ? action.isActionDisabled(ecsData) : false
}
onClick={() => action.onClick({ eventId: id, ecsData, data })}
/>
</EuiToolTip>
</EventsTdContent>
</EventsTd>,
],
};
}
@ -176,23 +178,25 @@ export const EventColumnView = React.memo<Props>(
return grouped.contextMenu.length > 0
? [
...grouped.icon,
<EventsTdContent
key="actions-context-menu"
textAlign="center"
width={DEFAULT_ICON_BUTTON_WIDTH}
>
<EuiPopover
id="singlePanel"
button={button}
isOpen={isPopoverOpen}
closePopover={closePopover}
panelPaddingSize="none"
anchorPosition="downLeft"
repositionOnScroll
<EventsTd>
<EventsTdContent
key="actions-context-menu"
textAlign="center"
width={DEFAULT_ICON_BUTTON_WIDTH}
>
<ContextMenuPanel items={grouped.contextMenu} />
</EuiPopover>
</EventsTdContent>,
<EuiPopover
id="singlePanel"
button={button}
isOpen={isPopoverOpen}
closePopover={closePopover}
panelPaddingSize="none"
anchorPosition="downLeft"
repositionOnScroll
>
<ContextMenuPanel items={grouped.contextMenu} />
</EuiPopover>
</EventsTdContent>
</EventsTd>,
]
: grouped.icon;
}, [button, closePopover, id, onClickCb, data, ecsData, timelineActions, isPopoverOpen]);

View file

@ -16,7 +16,7 @@ exports[`GenericFileDetails rendering it renders the default GenericFileDetails
processTitle="/lib/systemd/systemd-journald"
result="success"
secondary="root"
session="unset"
session="242"
text="generic-text-123"
userName="root"
workingDirectory="/"
@ -34,7 +34,7 @@ exports[`GenericFileDetails rendering it renders the default GenericFileDetails
"success",
],
"session": Array [
"unset",
"242",
],
"summary": Object {
"actor": Object {

View file

@ -32,7 +32,7 @@ exports[`GenericRowRenderer #createGenericAuditRowRenderer renders correctly aga
"message_type": null,
"object": Object {
"primary": Array [
"93.184.216.34",
"192.168.216.34",
],
"secondary": Array [
"80",
@ -46,7 +46,7 @@ exports[`GenericRowRenderer #createGenericAuditRowRenderer renders correctly aga
},
"destination": Object {
"ip": Array [
"93.184.216.34",
"192.168.216.34",
],
"port": Array [
80,
@ -113,7 +113,7 @@ exports[`GenericRowRenderer #createGenericAuditRowRenderer renders correctly aga
"zeek": null,
}
}
text="some text"
text="connected using"
timelineId="test"
/>
</RowRendererContainer>
@ -135,7 +135,7 @@ exports[`GenericRowRenderer #createGenericFileRowRenderer renders correctly agai
"success",
],
"session": Array [
"unset",
"242",
],
"summary": Object {
"actor": Object {
@ -259,7 +259,7 @@ exports[`GenericRowRenderer #createGenericFileRowRenderer renders correctly agai
}
}
fileIcon="document"
text="some text"
text="opened file using"
timelineId="test"
/>
</RowRendererContainer>

View file

@ -34,7 +34,7 @@ describe('GenericRowRenderer', () => {
auditd = cloneDeep(mockTimelineData[26].ecs);
connectedToRenderer = createGenericAuditRowRenderer({
actionName: 'connected-to',
text: 'some text',
text: 'connected using',
});
});
test('renders correctly against snapshot', () => {
@ -80,7 +80,7 @@ describe('GenericRowRenderer', () => {
</TestProviders>
);
expect(wrapper.text()).toContain(
'Session246alice@zeek-londonsome textwget(1490)wget www.example.comwith resultsuccessDestination93.184.216.34:80'
'Session246alice@zeek-londonconnected usingwget(1490)wget www.example.comwith resultsuccessDestination192.168.216.34:80'
);
});
});
@ -95,7 +95,7 @@ describe('GenericRowRenderer', () => {
auditdFile = cloneDeep(mockTimelineData[27].ecs);
fileToRenderer = createGenericFileRowRenderer({
actionName: 'opened-file',
text: 'some text',
text: 'opened file using',
});
});
@ -142,7 +142,7 @@ describe('GenericRowRenderer', () => {
</TestProviders>
);
expect(wrapper.text()).toContain(
'Sessionunsetroot@zeek-londonin/some text/proc/15990/attr/currentusingsystemd-journal(27244)/lib/systemd/systemd-journaldwith resultsuccess'
'Session242root@zeek-londonin/opened file using/proc/15990/attr/currentusingsystemd-journal(27244)/lib/systemd/systemd-journaldwith resultsuccess'
);
});
});

View file

@ -10,6 +10,8 @@ import { IconType } from '@elastic/eui';
import { get } from 'lodash/fp';
import React from 'react';
import { RowRendererId } from '../../../../../../../common/types/timeline';
import { RowRenderer, RowRendererContainer } from '../row_renderer';
import { AuditdGenericDetails } from './generic_details';
import { AuditdGenericFileDetails } from './generic_file_details';
@ -22,6 +24,7 @@ export const createGenericAuditRowRenderer = ({
actionName: string;
text: string;
}): RowRenderer => ({
id: RowRendererId.auditd,
isInstance: (ecs) => {
const module: string | null | undefined = get('event.module[0]', ecs);
const action: string | null | undefined = get('event.action[0]', ecs);
@ -54,6 +57,7 @@ export const createGenericFileRowRenderer = ({
text: string;
fileIcon?: IconType;
}): RowRenderer => ({
id: RowRendererId.auditd_file,
isInstance: (ecs) => {
const module: string | null | undefined = get('event.module[0]', ecs);
const action: string | null | undefined = get('event.action[0]', ecs);

View file

@ -10,6 +10,7 @@ import { get } from 'lodash/fp';
import React from 'react';
import styled from 'styled-components';
import { RowRendererId } from '../../../../../../../common/types/timeline';
import { asArrayIfExists } from '../../../../../../common/lib/helpers';
import {
TLS_CLIENT_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME,
@ -84,6 +85,7 @@ export const eventActionMatches = (eventAction: string | object | undefined | nu
};
export const netflowRowRenderer: RowRenderer = {
id: RowRendererId.netflow,
isInstance: (ecs) =>
eventCategoryMatches(get(EVENT_CATEGORY_FIELD, ecs)) ||
eventActionMatches(get(EVENT_ACTION_FIELD, ecs)),

View file

@ -4,13 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable react/display-name */
import React from 'react';
import { RowRendererId } from '../../../../../../common/types/timeline';
import { RowRenderer } from './row_renderer';
const PlainRowRenderer = () => <></>;
PlainRowRenderer.displayName = 'PlainRowRenderer';
export const plainRowRenderer: RowRenderer = {
id: RowRendererId.plain,
isInstance: (_) => true,
renderRow: () => <></>,
renderRow: PlainRowRenderer,
};

View file

@ -7,6 +7,7 @@
import React from 'react';
import { BrowserFields } from '../../../../../common/containers/source';
import type { RowRendererId } from '../../../../../../common/types/timeline';
import { Ecs } from '../../../../../graphql/types';
import { EventsTrSupplement } from '../../styles';
@ -22,6 +23,7 @@ export const RowRendererContainer = React.memo<RowRendererContainerProps>(({ chi
RowRendererContainer.displayName = 'RowRendererContainer';
export interface RowRenderer {
id: RowRendererId;
isInstance: (data: Ecs) => boolean;
renderRow: ({
browserFields,

View file

@ -34,7 +34,6 @@ exports[`SuricataSignature rendering it renders the default SuricataSignature 1`
>
Hello
</GoogleLink>
<ExternalLinkIcon />
</div>
</DefaultDraggable>
</LinkFlexItem>

View file

@ -9,10 +9,13 @@
import { get } from 'lodash/fp';
import React from 'react';
import { RowRendererId } from '../../../../../../../common/types/timeline';
import { RowRenderer, RowRendererContainer } from '../row_renderer';
import { SuricataDetails } from './suricata_details';
export const suricataRowRenderer: RowRenderer = {
id: RowRendererId.suricata,
isInstance: (ecs) => {
const module: string | null | undefined = get('event.module[0]', ecs);
return module != null && module.toLowerCase() === 'suricata';

View file

@ -13,7 +13,6 @@ import {
DraggableWrapper,
} from '../../../../../../common/components/drag_and_drop/draggable_wrapper';
import { escapeDataProviderId } from '../../../../../../common/components/drag_and_drop/helpers';
import { ExternalLinkIcon } from '../../../../../../common/components/external_link_icon';
import { GoogleLink } from '../../../../../../common/components/links';
import { Provider } from '../../../data_providers/provider';
@ -122,7 +121,6 @@ export const SuricataSignature = React.memo<{
<GoogleLink link={signature}>
{signature.split(' ').splice(tokens.length).join(' ')}
</GoogleLink>
<ExternalLinkIcon />
</div>
</DefaultDraggable>
</LinkFlexItem>

View file

@ -9,6 +9,8 @@
import { get } from 'lodash/fp';
import React from 'react';
import { RowRendererId } from '../../../../../../../common/types/timeline';
import { DnsRequestEventDetails } from '../dns/dns_request_event_details';
import { EndgameSecurityEventDetails } from '../endgame/endgame_security_event_details';
import { isFileEvent, isNillEmptyOrNotFinite } from '../helpers';
@ -25,6 +27,7 @@ export const createGenericSystemRowRenderer = ({
actionName: string;
text: string;
}): RowRenderer => ({
id: RowRendererId.system,
isInstance: (ecs) => {
const module: string | null | undefined = get('event.module[0]', ecs);
const action: string | null | undefined = get('event.action[0]', ecs);
@ -55,6 +58,7 @@ export const createEndgameProcessRowRenderer = ({
actionName: string;
text: string;
}): RowRenderer => ({
id: RowRendererId.system_file,
isInstance: (ecs) => {
const action: string | null | undefined = get('event.action[0]', ecs);
const category: string | null | undefined = get('event.category[0]', ecs);
@ -86,6 +90,7 @@ export const createFimRowRenderer = ({
actionName: string;
text: string;
}): RowRenderer => ({
id: RowRendererId.system_fim,
isInstance: (ecs) => {
const action: string | null | undefined = get('event.action[0]', ecs);
const category: string | null | undefined = get('event.category[0]', ecs);
@ -117,6 +122,7 @@ export const createGenericFileRowRenderer = ({
actionName: string;
text: string;
}): RowRenderer => ({
id: RowRendererId.system_file,
isInstance: (ecs) => {
const module: string | null | undefined = get('event.module[0]', ecs);
const action: string | null | undefined = get('event.action[0]', ecs);
@ -147,6 +153,7 @@ export const createSocketRowRenderer = ({
actionName: string;
text: string;
}): RowRenderer => ({
id: RowRendererId.system_socket,
isInstance: (ecs) => {
const action: string | null | undefined = get('event.action[0]', ecs);
return action != null && action.toLowerCase() === actionName;
@ -169,6 +176,7 @@ export const createSecurityEventRowRenderer = ({
}: {
actionName: string;
}): RowRenderer => ({
id: RowRendererId.system_security_event,
isInstance: (ecs) => {
const category: string | null | undefined = get('event.category[0]', ecs);
const action: string | null | undefined = get('event.action[0]', ecs);
@ -192,6 +200,7 @@ export const createSecurityEventRowRenderer = ({
});
export const createDnsRowRenderer = (): RowRenderer => ({
id: RowRendererId.system_dns,
isInstance: (ecs) => {
const dnsQuestionType: string | null | undefined = get('dns.question.type[0]', ecs);
const dnsQuestionName: string | null | undefined = get('dns.question.name[0]', ecs);

View file

@ -9,10 +9,13 @@
import { get } from 'lodash/fp';
import React from 'react';
import { RowRendererId } from '../../../../../../../common/types/timeline';
import { RowRenderer, RowRendererContainer } from '../row_renderer';
import { ZeekDetails } from './zeek_details';
export const zeekRowRenderer: RowRenderer = {
id: RowRendererId.zeek,
isInstance: (ecs) => {
const module: string | null | undefined = get('event.module[0]', ecs);
return module != null && module.toLowerCase() === 'zeek';

View file

@ -15,7 +15,6 @@ import {
DraggableWrapper,
} from '../../../../../../common/components/drag_and_drop/draggable_wrapper';
import { escapeDataProviderId } from '../../../../../../common/components/drag_and_drop/helpers';
import { ExternalLinkIcon } from '../../../../../../common/components/external_link_icon';
import { GoogleLink, ReputationLink } from '../../../../../../common/components/links';
import { Provider } from '../../../data_providers/provider';
import { IS_OPERATOR } from '../../../data_providers/data_provider';
@ -120,7 +119,6 @@ export const Link = React.memo<LinkProps>(({ value, link }) => {
<LinkFlexItem grow={false}>
<div>
<GoogleLink link={link}>{value}</GoogleLink>
<ExternalLinkIcon />
</div>
</LinkFlexItem>
);
@ -129,7 +127,6 @@ export const Link = React.memo<LinkProps>(({ value, link }) => {
<LinkFlexItem grow={false}>
<div>
<GoogleLink link={value} />
<ExternalLinkIcon />
</div>
</LinkFlexItem>
);

View file

@ -10,7 +10,7 @@ import React, { useCallback, useEffect, useMemo } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import deepEqual from 'fast-deep-equal';
import { TimelineId } from '../../../../../common/types/timeline';
import { RowRendererId, TimelineId } from '../../../../../common/types/timeline';
import { BrowserFields } from '../../../../common/containers/source';
import { TimelineItem } from '../../../../graphql/types';
import { Note } from '../../../../common/lib/note';
@ -60,6 +60,7 @@ const StatefulBodyComponent = React.memo<StatefulBodyComponentProps>(
columnHeaders,
data,
eventIdToNoteIds,
excludedRowRendererIds,
height,
id,
isEventViewer = false,
@ -74,7 +75,6 @@ const StatefulBodyComponent = React.memo<StatefulBodyComponentProps>(
clearSelected,
show,
showCheckboxes,
showRowRenderers,
graphEventId,
sort,
toggleColumn,
@ -97,8 +97,7 @@ const StatefulBodyComponent = React.memo<StatefulBodyComponentProps>(
const onAddNoteToEvent: AddNoteToEvent = useCallback(
({ eventId, noteId }: { eventId: string; noteId: string }) =>
addNoteToEvent!({ id, eventId, noteId }),
// eslint-disable-next-line react-hooks/exhaustive-deps
[id]
[id, addNoteToEvent]
);
const onRowSelected: OnRowSelected = useCallback(
@ -135,35 +134,36 @@ const StatefulBodyComponent = React.memo<StatefulBodyComponentProps>(
(sorted) => {
updateSort!({ id, sort: sorted });
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[id]
[id, updateSort]
);
const onColumnRemoved: OnColumnRemoved = useCallback(
(columnId) => removeColumn!({ id, columnId }),
// eslint-disable-next-line react-hooks/exhaustive-deps
[id]
[id, removeColumn]
);
const onColumnResized: OnColumnResized = useCallback(
({ columnId, delta }) => applyDeltaToColumnWidth!({ id, columnId, delta }),
// eslint-disable-next-line react-hooks/exhaustive-deps
[id]
[applyDeltaToColumnWidth, id]
);
// eslint-disable-next-line react-hooks/exhaustive-deps
const onPinEvent: OnPinEvent = useCallback((eventId) => pinEvent!({ id, eventId }), [id]);
const onPinEvent: OnPinEvent = useCallback((eventId) => pinEvent!({ id, eventId }), [
id,
pinEvent,
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
const onUnPinEvent: OnUnPinEvent = useCallback((eventId) => unPinEvent!({ id, eventId }), [id]);
const onUnPinEvent: OnUnPinEvent = useCallback((eventId) => unPinEvent!({ id, eventId }), [
id,
unPinEvent,
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
const onUpdateNote: UpdateNote = useCallback((note: Note) => updateNote!({ note }), []);
const onUpdateNote: UpdateNote = useCallback((note: Note) => updateNote!({ note }), [
updateNote,
]);
const onUpdateColumns: OnUpdateColumns = useCallback(
(columns) => updateColumns!({ id, columns }),
// eslint-disable-next-line react-hooks/exhaustive-deps
[id]
[id, updateColumns]
);
// Sync to selectAll so parent components can select all events
@ -171,8 +171,19 @@ const StatefulBodyComponent = React.memo<StatefulBodyComponentProps>(
if (selectAll) {
onSelectAll({ isSelected: true });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectAll]); // onSelectAll dependency not necessary
}, [onSelectAll, selectAll]);
const enabledRowRenderers = useMemo(() => {
if (
excludedRowRendererIds &&
excludedRowRendererIds.length === Object.keys(RowRendererId).length
)
return [plainRowRenderer];
if (!excludedRowRendererIds) return rowRenderers;
return rowRenderers.filter((rowRenderer) => !excludedRowRendererIds.includes(rowRenderer.id));
}, [excludedRowRendererIds]);
return (
<Body
@ -199,7 +210,7 @@ const StatefulBodyComponent = React.memo<StatefulBodyComponentProps>(
onUnPinEvent={onUnPinEvent}
onUpdateColumns={onUpdateColumns}
pinnedEventIds={pinnedEventIds}
rowRenderers={showRowRenderers ? rowRenderers : [plainRowRenderer]}
rowRenderers={enabledRowRenderers}
selectedEventIds={selectedEventIds}
show={id === TimelineId.active ? show : true}
showCheckboxes={showCheckboxes}
@ -213,6 +224,7 @@ const StatefulBodyComponent = React.memo<StatefulBodyComponentProps>(
deepEqual(prevProps.browserFields, nextProps.browserFields) &&
deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) &&
deepEqual(prevProps.data, nextProps.data) &&
deepEqual(prevProps.excludedRowRendererIds, nextProps.excludedRowRendererIds) &&
prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds &&
prevProps.graphEventId === nextProps.graphEventId &&
deepEqual(prevProps.notesById, nextProps.notesById) &&
@ -225,7 +237,6 @@ const StatefulBodyComponent = React.memo<StatefulBodyComponentProps>(
prevProps.show === nextProps.show &&
prevProps.selectedEventIds === nextProps.selectedEventIds &&
prevProps.showCheckboxes === nextProps.showCheckboxes &&
prevProps.showRowRenderers === nextProps.showRowRenderers &&
prevProps.sort === nextProps.sort
);
@ -245,6 +256,7 @@ const makeMapStateToProps = () => {
columns,
eventIdToNoteIds,
eventType,
excludedRowRendererIds,
graphEventId,
isSelectAllChecked,
loadingEventIds,
@ -252,13 +264,13 @@ const makeMapStateToProps = () => {
selectedEventIds,
show,
showCheckboxes,
showRowRenderers,
} = timeline;
return {
columnHeaders: memoizedColumnHeaders(columns, browserFields),
eventIdToNoteIds,
eventType,
excludedRowRendererIds,
graphEventId,
isSelectAllChecked,
loadingEventIds,
@ -268,7 +280,6 @@ const makeMapStateToProps = () => {
selectedEventIds,
show,
showCheckboxes,
showRowRenderers,
};
};
return mapStateToProps;

View file

@ -97,7 +97,7 @@ export const ProviderItemBadge = React.memo<ProviderItemBadgeProps>(
useEffect(() => {
// optionally register the provider if provided
if (!providerRegistered && register != null) {
if (register != null) {
dispatch(dragAndDropActions.registerProvider({ provider: { ...register, and: [] } }));
setProviderRegistered(true);
}

View file

@ -91,10 +91,14 @@ export const EventsTrHeader = styled.div.attrs(({ className }) => ({
export const EventsThGroupActions = styled.div.attrs(({ className = '' }) => ({
className: `siemEventsTable__thGroupActions ${className}`,
}))<{ actionsColumnWidth: number }>`
}))<{ actionsColumnWidth: number; isEventViewer: boolean }>`
display: flex;
flex: 0 0 ${({ actionsColumnWidth }) => `${actionsColumnWidth}px`};
flex: 0 0
${({ actionsColumnWidth, isEventViewer }) =>
`${!isEventViewer ? actionsColumnWidth + 4 : actionsColumnWidth}px`};
min-width: 0;
padding-left: ${({ isEventViewer }) =>
!isEventViewer ? '4px;' : '0;'}; // match timeline event border
`;
export const EventsThGroupData = styled.div.attrs(({ className = '' }) => ({
@ -151,6 +155,11 @@ export const EventsThContent = styled.div.attrs(({ className = '' }) => ({
width != null
? `${width}px`
: '100%'}; /* Using width: 100% instead of flex: 1 and max-width: 100% for IE11 */
> button.euiButtonIcon,
> .euiToolTipAnchor > button.euiButtonIcon {
margin-left: ${({ theme }) => `-${theme.eui.paddingSizes.xs}`};
}
`;
/* EVENTS BODY */
@ -198,8 +207,7 @@ 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
${({ theme }) => theme.eui.paddingSizes.xl};
padding: 0 ${({ theme }) => theme.eui.paddingSizes.xs} 0 52px;
`;
export const EventsTdGroupActions = styled.div.attrs(({ className = '' }) => ({
@ -249,6 +257,11 @@ export const EventsTdContent = styled.div.attrs(({ className }) => ({
width != null
? `${width}px`
: '100%'}; /* Using width: 100% instead of flex: 1 and max-width: 100% for IE11 */
> button.euiButtonIcon,
> .euiToolTipAnchor > button.euiButtonIcon {
margin-left: ${({ theme }) => `-${theme.eui.paddingSizes.xs}`};
}
`;
/**
@ -334,6 +347,5 @@ export const EventsHeadingHandle = styled.div.attrs(({ className = '' }) => ({
*/
export const EventsLoading = styled(EuiLoadingSpinner)`
margin: ${({ theme }) => theme.eui.euiSizeXS};
vertical-align: top;
vertical-align: middle;
`;

View file

@ -51,6 +51,7 @@ export const allTimelinesQuery = gql`
updatedBy
version
}
excludedRowRendererIds
notes {
eventId
note

View file

@ -75,6 +75,7 @@ export const getAllTimeline = memoizeOne(
return acc;
}, {})
: null,
excludedRowRendererIds: timeline.excludedRowRendererIds,
favorite: timeline.favorite,
noteIds: timeline.noteIds,
notes:

View file

@ -69,6 +69,7 @@ export const oneTimelineQuery = gql`
updatedBy
version
}
excludedRowRendererIds
favorite {
fullName
userName

View file

@ -17,7 +17,7 @@ import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/t
import { EventType, KqlMode, TimelineModel, ColumnHeaderOptions } from './model';
import { TimelineNonEcsData } from '../../../graphql/types';
import { TimelineTypeLiteral } from '../../../../common/types/timeline';
import { TimelineTypeLiteral, RowRendererId } from '../../../../common/types/timeline';
import { InsertTimeline } from './types';
const actionCreator = actionCreatorFactory('x-pack/security_solution/local/timeline');
@ -59,6 +59,7 @@ export const createTimeline = actionCreator<{
start: number;
end: number;
};
excludedRowRendererIds?: RowRendererId[];
filters?: Filter[];
columns: ColumnHeaderOptions[];
itemsPerPage?: number;
@ -69,7 +70,6 @@ export const createTimeline = actionCreator<{
show?: boolean;
sort?: Sort;
showCheckboxes?: boolean;
showRowRenderers?: boolean;
timelineType?: TimelineTypeLiteral;
templateTimelineId?: string;
templateTimelineVersion?: number;
@ -266,3 +266,8 @@ export const clearEventsDeleted = actionCreator<{
export const updateEventType = actionCreator<{ id: string; eventType: EventType }>(
'UPDATE_EVENT_TYPE'
);
export const setExcludedRowRendererIds = actionCreator<{
id: string;
excludedRowRendererIds: RowRendererId[];
}>('SET_TIMELINE_EXCLUDED_ROW_RENDERER_IDS');

View file

@ -18,6 +18,7 @@ export const timelineDefaults: SubsetTimelineModel & Pick<TimelineModel, 'filter
description: '',
eventType: 'all',
eventIdToNoteIds: {},
excludedRowRendererIds: [],
highlightedDropAndProviderId: '',
historyIds: [],
filters: [],
@ -49,7 +50,6 @@ export const timelineDefaults: SubsetTimelineModel & Pick<TimelineModel, 'filter
selectedEventIds: {},
show: false,
showCheckboxes: false,
showRowRenderers: true,
sort: {
columnId: '@timestamp',
sortDirection: Direction.desc,

View file

@ -89,6 +89,7 @@ describe('Epic Timeline', () => {
description: '',
eventIdToNoteIds: {},
eventType: 'all',
excludedRowRendererIds: [],
highlightedDropAndProviderId: '',
historyIds: [],
filters: [
@ -146,7 +147,6 @@ describe('Epic Timeline', () => {
selectedEventIds: {},
show: true,
showCheckboxes: false,
showRowRenderers: true,
sort: { columnId: '@timestamp', sortDirection: Direction.desc },
status: TimelineStatus.active,
width: 1100,
@ -233,6 +233,7 @@ describe('Epic Timeline', () => {
},
description: '',
eventType: 'all',
excludedRowRendererIds: [],
filters: [
{
exists: null,

View file

@ -331,6 +331,7 @@ const timelineInput: TimelineInput = {
dataProviders: null,
description: null,
eventType: null,
excludedRowRendererIds: null,
filters: null,
kqlMode: null,
kqlQuery: null,

View file

@ -16,6 +16,7 @@ import {
removeColumn,
upsertColumn,
applyDeltaToColumnWidth,
setExcludedRowRendererIds,
updateColumns,
updateItemsPerPage,
updateSort,
@ -30,6 +31,7 @@ const timelineActionTypes = [
updateColumns.type,
updateItemsPerPage.type,
updateSort.type,
setExcludedRowRendererIds.type,
];
export const isPageTimeline = (timelineId: string | undefined): boolean =>

View file

@ -21,7 +21,11 @@ import {
} from '../../../timelines/components/timeline/data_providers/data_provider';
import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/model';
import { TimelineNonEcsData } from '../../../graphql/types';
import { TimelineTypeLiteral, TimelineType } from '../../../../common/types/timeline';
import {
TimelineTypeLiteral,
TimelineType,
RowRendererId,
} from '../../../../common/types/timeline';
import { timelineDefaults } from './defaults';
import { ColumnHeaderOptions, KqlMode, TimelineModel, EventType } from './model';
@ -130,6 +134,7 @@ interface AddNewTimelineParams {
start: number;
end: number;
};
excludedRowRendererIds?: RowRendererId[];
filters?: Filter[];
id: string;
itemsPerPage?: number;
@ -140,7 +145,6 @@ interface AddNewTimelineParams {
show?: boolean;
sort?: Sort;
showCheckboxes?: boolean;
showRowRenderers?: boolean;
timelineById: TimelineById;
timelineType: TimelineTypeLiteral;
}
@ -150,6 +154,7 @@ export const addNewTimeline = ({
columns,
dataProviders = [],
dateRange = { start: 0, end: 0 },
excludedRowRendererIds = [],
filters = timelineDefaults.filters,
id,
itemsPerPage = timelineDefaults.itemsPerPage,
@ -157,7 +162,6 @@ export const addNewTimeline = ({
sort = timelineDefaults.sort,
show = false,
showCheckboxes = false,
showRowRenderers = true,
timelineById,
timelineType,
}: AddNewTimelineParams): TimelineById => {
@ -176,6 +180,7 @@ export const addNewTimeline = ({
columns,
dataProviders,
dateRange,
excludedRowRendererIds,
filters,
itemsPerPage,
kqlQuery,
@ -186,7 +191,6 @@ export const addNewTimeline = ({
isSaving: false,
isLoading: false,
showCheckboxes,
showRowRenderers,
timelineType,
...templateTimelineInfo,
},
@ -1436,3 +1440,25 @@ export const updateFilters = ({ id, filters, timelineById }: UpdateFiltersParams
},
};
};
interface UpdateExcludedRowRenderersIds {
id: string;
excludedRowRendererIds: RowRendererId[];
timelineById: TimelineById;
}
export const updateExcludedRowRenderersIds = ({
id,
excludedRowRendererIds,
timelineById,
}: UpdateExcludedRowRenderersIds): TimelineById => {
const timeline = timelineById[id];
return {
...timelineById,
[id]: {
...timeline,
excludedRowRendererIds,
},
};
};

View file

@ -15,6 +15,7 @@ import {
TimelineStatus,
} from '../../../graphql/types';
import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/types';
import type { RowRendererId } from '../../../../common/types/timeline';
export const DEFAULT_PAGE_COUNT = 2; // Eui Pager will not render unless this is a minimum of 2 pages
export type KqlMode = 'filter' | 'search';
@ -54,6 +55,8 @@ export interface TimelineModel {
eventType?: EventType;
/** A map of events in this timeline to the chronologically ordered notes (in this timeline) associated with the event */
eventIdToNoteIds: Record<string, string[]>;
/** A list of Ids of excluded Row Renderers */
excludedRowRendererIds: RowRendererId[];
filters?: Filter[];
/** When non-empty, display a graph view for this event */
graphEventId?: string;
@ -108,8 +111,6 @@ export interface TimelineModel {
show: boolean;
/** When true, shows checkboxes enabling selection. Selected events store in selectedEventIds **/
showCheckboxes: boolean;
/** When true, shows additional rowRenderers below the PlainRowRenderer **/
showRowRenderers: boolean;
/** Specifies which column the timeline is sorted on, and the direction (ascending / descending) */
sort: Sort;
/** status: active | draft */
@ -131,6 +132,7 @@ export type SubsetTimelineModel = Readonly<
| 'description'
| 'eventType'
| 'eventIdToNoteIds'
| 'excludedRowRendererIds'
| 'graphEventId'
| 'highlightedDropAndProviderId'
| 'historyIds'
@ -153,7 +155,6 @@ export type SubsetTimelineModel = Readonly<
| 'selectedEventIds'
| 'show'
| 'showCheckboxes'
| 'showRowRenderers'
| 'sort'
| 'width'
| 'isSaving'

View file

@ -70,6 +70,7 @@ const timelineByIdMock: TimelineById = {
description: '',
deletedEventIds: [],
eventIdToNoteIds: {},
excludedRowRendererIds: [],
highlightedDropAndProviderId: '',
historyIds: [],
id: 'foo',
@ -97,7 +98,6 @@ const timelineByIdMock: TimelineById = {
selectedEventIds: {},
show: true,
showCheckboxes: false,
showRowRenderers: true,
sort: {
columnId: '@timestamp',
sortDirection: Direction.desc,
@ -1119,6 +1119,7 @@ describe('Timeline', () => {
deletedEventIds: [],
description: '',
eventIdToNoteIds: {},
excludedRowRendererIds: [],
highlightedDropAndProviderId: '',
historyIds: [],
isFavorite: false,
@ -1139,7 +1140,6 @@ describe('Timeline', () => {
},
selectedEventIds: {},
show: true,
showRowRenderers: true,
showCheckboxes: false,
sort: {
columnId: '@timestamp',
@ -1215,6 +1215,7 @@ describe('Timeline', () => {
description: '',
deletedEventIds: [],
eventIdToNoteIds: {},
excludedRowRendererIds: [],
highlightedDropAndProviderId: '',
historyIds: [],
isFavorite: false,
@ -1235,7 +1236,6 @@ describe('Timeline', () => {
},
selectedEventIds: {},
show: true,
showRowRenderers: true,
showCheckboxes: false,
sort: {
columnId: '@timestamp',
@ -1421,6 +1421,7 @@ describe('Timeline', () => {
description: '',
deletedEventIds: [],
eventIdToNoteIds: {},
excludedRowRendererIds: [],
highlightedDropAndProviderId: '',
historyIds: [],
isFavorite: false,
@ -1441,7 +1442,6 @@ describe('Timeline', () => {
},
selectedEventIds: {},
show: true,
showRowRenderers: true,
showCheckboxes: false,
sort: {
columnId: '@timestamp',
@ -1517,6 +1517,7 @@ describe('Timeline', () => {
description: '',
deletedEventIds: [],
eventIdToNoteIds: {},
excludedRowRendererIds: [],
highlightedDropAndProviderId: '',
historyIds: [],
isFavorite: false,
@ -1537,7 +1538,6 @@ describe('Timeline', () => {
},
selectedEventIds: {},
show: true,
showRowRenderers: true,
showCheckboxes: false,
sort: {
columnId: '@timestamp',
@ -1619,6 +1619,7 @@ describe('Timeline', () => {
description: '',
deletedEventIds: [],
eventIdToNoteIds: {},
excludedRowRendererIds: [],
highlightedDropAndProviderId: '',
historyIds: [],
isFavorite: false,
@ -1639,7 +1640,6 @@ describe('Timeline', () => {
},
selectedEventIds: {},
show: true,
showRowRenderers: true,
showCheckboxes: false,
sort: {
columnId: '@timestamp',
@ -1722,6 +1722,7 @@ describe('Timeline', () => {
description: '',
deletedEventIds: [],
eventIdToNoteIds: {},
excludedRowRendererIds: [],
highlightedDropAndProviderId: '',
historyIds: [],
isFavorite: false,
@ -1742,7 +1743,6 @@ describe('Timeline', () => {
},
selectedEventIds: {},
show: true,
showRowRenderers: true,
showCheckboxes: false,
sort: {
columnId: '@timestamp',
@ -1917,6 +1917,7 @@ describe('Timeline', () => {
description: '',
deletedEventIds: [],
eventIdToNoteIds: {},
excludedRowRendererIds: [],
highlightedDropAndProviderId: '',
historyIds: [],
isFavorite: false,
@ -1937,7 +1938,6 @@ describe('Timeline', () => {
},
selectedEventIds: {},
show: true,
showRowRenderers: true,
showCheckboxes: false,
sort: {
columnId: '@timestamp',
@ -1995,6 +1995,7 @@ describe('Timeline', () => {
description: '',
deletedEventIds: [],
eventIdToNoteIds: {},
excludedRowRendererIds: [],
highlightedDropAndProviderId: '',
historyIds: [],
isFavorite: false,
@ -2003,7 +2004,6 @@ describe('Timeline', () => {
isLoading: false,
id: 'foo',
savedObjectId: null,
showRowRenderers: true,
kqlMode: 'filter',
kqlQuery: { filterQuery: null, filterQueryDraft: null },
loadingEventIds: [],
@ -2099,6 +2099,7 @@ describe('Timeline', () => {
description: '',
deletedEventIds: [],
eventIdToNoteIds: {},
excludedRowRendererIds: [],
highlightedDropAndProviderId: '',
historyIds: [],
id: 'foo',
@ -2121,7 +2122,6 @@ describe('Timeline', () => {
},
selectedEventIds: {},
show: true,
showRowRenderers: true,
showCheckboxes: false,
sort: {
columnId: '@timestamp',

View file

@ -25,6 +25,7 @@ import {
removeProvider,
setEventsDeleted,
setEventsLoading,
setExcludedRowRendererIds,
setFilters,
setInsertTimeline,
setKqlFilterQueryDraft,
@ -75,6 +76,7 @@ import {
setLoadingTimelineEvents,
setSelectedTimelineEvents,
unPinTimelineEvent,
updateExcludedRowRenderersIds,
updateHighlightedDropAndProvider,
updateKqlFilterQueryDraft,
updateTimelineColumns,
@ -129,13 +131,13 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
id,
dataProviders,
dateRange,
excludedRowRendererIds,
show,
columns,
itemsPerPage,
kqlQuery,
sort,
showCheckboxes,
showRowRenderers,
timelineType = TimelineType.default,
filters,
}
@ -146,6 +148,7 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
columns,
dataProviders,
dateRange,
excludedRowRendererIds,
filters,
id,
itemsPerPage,
@ -153,7 +156,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
sort,
show,
showCheckboxes,
showRowRenderers,
timelineById: state.timelineById,
timelineType,
}),
@ -306,6 +308,14 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
},
},
}))
.case(setExcludedRowRendererIds, (state, { id, excludedRowRendererIds }) => ({
...state,
timelineById: updateExcludedRowRenderersIds({
id,
excludedRowRendererIds,
timelineById: state.timelineById,
}),
}))
.case(setSelected, (state, { id, eventIds, isSelected, isSelectAllChecked }) => ({
...state,
timelineById: setSelectedTimelineEvents({

View file

@ -147,11 +147,28 @@ export const timelineSchema = gql`
custom
}
enum RowRendererId {
auditd
auditd_file
netflow
plain
suricata
system
system_dns
system_endgame_process
system_file
system_fim
system_security_event
system_socket
zeek
}
input TimelineInput {
columns: [ColumnHeaderInput!]
dataProviders: [DataProviderInput!]
description: String
eventType: String
excludedRowRendererIds: [RowRendererId!]
filters: [FilterTimelineInput!]
kqlMode: String
kqlQuery: SerializedFilterQueryInput
@ -252,6 +269,7 @@ export const timelineSchema = gql`
description: String
eventIdToNoteIds: [NoteResult!]
eventType: String
excludedRowRendererIds: [RowRendererId!]
favorite: [FavoriteTimelineResult!]
filters: [FilterTimelineResult!]
kqlMode: String

View file

@ -126,6 +126,8 @@ export interface TimelineInput {
eventType?: Maybe<string>;
excludedRowRendererIds?: Maybe<RowRendererId[]>;
filters?: Maybe<FilterTimelineInput[]>;
kqlMode?: Maybe<string>;
@ -351,6 +353,22 @@ export enum DataProviderType {
template = 'template',
}
export enum RowRendererId {
auditd = 'auditd',
auditd_file = 'auditd_file',
netflow = 'netflow',
plain = 'plain',
suricata = 'suricata',
system = 'system',
system_dns = 'system_dns',
system_endgame_process = 'system_endgame_process',
system_file = 'system_file',
system_fim = 'system_fim',
system_security_event = 'system_security_event',
system_socket = 'system_socket',
zeek = 'zeek',
}
export enum TimelineStatus {
active = 'active',
draft = 'draft',
@ -1963,6 +1981,8 @@ export interface TimelineResult {
eventType?: Maybe<string>;
excludedRowRendererIds?: Maybe<RowRendererId[]>;
favorite?: Maybe<FavoriteTimelineResult[]>;
filters?: Maybe<FilterTimelineResult[]>;
@ -8101,6 +8121,12 @@ export namespace TimelineResultResolvers {
eventType?: EventTypeResolver<Maybe<string>, TypeParent, TContext>;
excludedRowRendererIds?: ExcludedRowRendererIdsResolver<
Maybe<RowRendererId[]>,
TypeParent,
TContext
>;
favorite?: FavoriteResolver<Maybe<FavoriteTimelineResult[]>, TypeParent, TContext>;
filters?: FiltersResolver<Maybe<FilterTimelineResult[]>, TypeParent, TContext>;
@ -8184,6 +8210,11 @@ export namespace TimelineResultResolvers {
Parent = TimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type ExcludedRowRendererIdsResolver<
R = Maybe<RowRendererId[]>,
Parent = TimelineResult,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type FavoriteResolver<
R = Maybe<FavoriteTimelineResult[]>,
Parent = TimelineResult,

View file

@ -44,5 +44,7 @@ export const pickSavedTimeline = (
savedTimeline.status = TimelineStatus.active;
}
savedTimeline.excludedRowRendererIds = savedTimeline.excludedRowRendererIds ?? [];
return savedTimeline;
};

View file

@ -181,7 +181,7 @@ const getTimelinesFromObjects = async (
if (myTimeline != null) {
const timelineNotes = myNotes.filter((n) => n.timelineId === timelineId);
const timelinePinnedEventIds = myPinnedEventIds.filter((p) => p.timelineId === timelineId);
const exportedTimeline = omit('status', myTimeline);
const exportedTimeline = omit(['status', 'excludedRowRendererIds'], myTimeline);
return [
...acc,
{

View file

@ -135,6 +135,9 @@ export const timelineSavedObjectMappings: SavedObjectsType['mappings'] = {
eventType: {
type: 'keyword',
},
excludedRowRendererIds: {
type: 'text',
},
favorite: {
properties: {
keySearch: {