[Session view] [serverless] versioned APIs and other improvements (#158982)

## Summary

### Issues: 
- https://github.com/elastic/kibana/issues/158687
- https://github.com/elastic/kibana/issues/158686

### Fixes
- API routes now versioned
- Moved index selection logic to session_view plugin, added tests
- fields now defined for all ES queries
- types consolidated, and put under a latest/v1 export paradigm

### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Karl Godard 2023-06-07 15:44:51 -07:00 committed by GitHub
parent 006499c12e
commit ed0d341757
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
77 changed files with 727 additions and 566 deletions

View file

@ -61,7 +61,7 @@
"tags": [],
"label": "EventAction",
"description": [],
"path": "x-pack/plugins/session_view/common/types/process_tree/index.ts",
"path": "x-pack/plugins/session_view/common/index.ts",
"deprecated": false,
"trackAdoption": false,
"initialIsOpen": false
@ -106,7 +106,7 @@
"tags": [],
"label": "EventAction",
"description": [],
"path": "x-pack/plugins/session_view/common/types/process_tree/index.ts",
"path": "x-pack/plugins/session_view/common/index.ts",
"deprecated": false,
"trackAdoption": false,
"initialIsOpen": false
@ -131,4 +131,4 @@
],
"objects": []
}
}
}

View file

@ -6,7 +6,7 @@
*/
export interface SessionViewConfig {
processIndex: string;
index: string;
sessionEntityId: string;
sessionStartTime: string;
jumpToEntityId?: string;

View file

@ -6,7 +6,7 @@
*/
export interface SessionViewConfig {
processIndex: string;
index: string;
sessionEntityId: string;
sessionStartTime: string;
jumpToEntityId?: string;

View file

@ -39,7 +39,7 @@ import { useTourContext } from '../guided_onboarding_tour';
import { AlertsCasesTourSteps, SecurityStepId } from '../guided_onboarding_tour/tour_config';
import { isDetectionsAlertsTable } from '../top_n/helpers';
import { GuidedOnboardingTourStep } from '../guided_onboarding_tour/tour_step';
import { DEFAULT_ACTION_BUTTON_WIDTH, isAlert, getSessionViewProcessIndex } from './helpers';
import { DEFAULT_ACTION_BUTTON_WIDTH, isAlert } from './helpers';
const ActionsContainer = styled.div`
align-items: center;
@ -152,13 +152,9 @@ const ActionsComponent: React.FC<ActionProps> = ({
const { process, _id, _index, timestamp, kibana } = ecsData;
const sessionEntityId = process?.entry_leader?.entity_id?.[0];
const sessionStartTime = process?.entry_leader?.start?.[0];
const processIndex = getSessionViewProcessIndex(kibana?.alert?.ancestors?.index?.[0] || _index);
const index = kibana?.alert?.ancestors?.index?.[0] || _index;
if (
processIndex === undefined ||
sessionEntityId === undefined ||
sessionStartTime === undefined
) {
if (index === undefined || sessionEntityId === undefined || sessionStartTime === undefined) {
return null;
}
@ -168,7 +164,7 @@ const ActionsComponent: React.FC<ActionProps> = ({
(investigatedAlertId && ecsData.kibana?.alert.original_time?.[0]) || timestamp;
return {
processIndex,
index,
sessionEntityId,
sessionStartTime,
jumpToEntityId,

View file

@ -6,12 +6,7 @@
*/
import { euiThemeVars } from '@kbn/ui-theme';
import {
DEFAULT_ACTION_BUTTON_WIDTH,
getActionsColumnWidth,
isAlert,
getSessionViewProcessIndex,
} from './helpers';
import { DEFAULT_ACTION_BUTTON_WIDTH, getActionsColumnWidth, isAlert } from './helpers';
describe('isAlert', () => {
test('it returns true when the eventType is an alert', () => {
@ -53,67 +48,3 @@ describe('getActionsColumnWidth', () => {
);
});
});
describe('getSessionViewProcessIndex', () => {
test('it returns process index for cloud_defend alert event index', () => {
const result = getSessionViewProcessIndex(
'.ds-logs-cloud_defend.alerts-default-2023.04.25-000001'
);
expect(result).toEqual('logs-cloud_defend.process*');
});
test('it returns process index for cloud_defend file event index', () => {
const result = getSessionViewProcessIndex(
'.ds-logs-cloud_defend.file-default-2023.04.25-000001'
);
expect(result).toEqual('logs-cloud_defend.process*');
});
test('it returns process index for cloud_defend process event index', () => {
const result = getSessionViewProcessIndex(
'.ds-logs-cloud_defend.process-default-2023.04.25-000001'
);
expect(result).toEqual('logs-cloud_defend.process*');
});
test('it returns process index for cloud_defend that includes cluster', () => {
const result = getSessionViewProcessIndex(
'aws_ec2:.ds-logs-cloud_defend.process-default-2023.04.25-000001'
);
expect(result).toEqual('aws_ec2:logs-cloud_defend.process*');
});
test('it returns process index for endpoint file index', () => {
const result = getSessionViewProcessIndex(
'.ds-logs-endpoint.events.file-default-2023.04.25-000001'
);
expect(result).toEqual('logs-endpoint.events.process*');
});
test('it returns process index for endpoint alerts index', () => {
const result = getSessionViewProcessIndex('.ds-logs-endpoint.alerts-default-2023.04.25-000001');
expect(result).toEqual('logs-endpoint.events.process*');
});
test('it returns process index for endpoint process index', () => {
const result = getSessionViewProcessIndex(
'.ds-logs-endpoint.events.process-default-2023.04.25-000001'
);
expect(result).toEqual('logs-endpoint.events.process*');
});
test('it returns process index for endpoint that includes cluster', () => {
const result = getSessionViewProcessIndex(
'azure-01:.ds-logs-endpoint.events.process-default-2023.04.25-000001'
);
expect(result).toEqual('azure-01:logs-endpoint.events.process*');
});
});

View file

@ -50,23 +50,3 @@ export const getActionsColumnWidth = (actionButtonCount: number): number => {
return contentWidth + leftRightCellPadding;
};
// Currently both logs-endpoint.events.process* and logs-cloud_defend.process* are valid sources for session data.
// To avoid cross cluster searches, the original index of the event is used to infer the index to find data for the
// rest of the session.
export const getSessionViewProcessIndex = (eventIndex?: string | null) => {
if (!eventIndex) {
return;
}
const match = eventIndex.match(/([a-z0-9_-]+:)?\.ds-logs-(endpoint|cloud_defend)/i);
const cluster = match?.[1];
const clusterStr = cluster ? `${cluster}` : '';
const service = match?.[2];
if (service === 'endpoint') {
return `${clusterStr}logs-endpoint.events.process*`;
} else if (service === 'cloud_defend') {
return `${clusterStr}logs-cloud_defend.process*`;
}
};

View file

@ -7,7 +7,7 @@
import React, { useMemo, useEffect } from 'react';
import type { Filter } from '@kbn/es-query';
import { ENTRY_SESSION_ENTITY_ID_PROPERTY, EventAction } from '@kbn/session-view-plugin/public';
import { ENTRY_SESSION_ENTITY_ID_PROPERTY } from '@kbn/session-view-plugin/public';
import { useDispatch } from 'react-redux';
import { EVENT_ACTION } from '@kbn/rule-data-utils';
import { TableId, dataTableActions } from '@kbn/securitysolution-data-table';
@ -38,9 +38,9 @@ export const defaultSessionsFilter: Required<Pick<Filter, 'meta' | 'query'>> = {
bool: {
// show sessions table results by filtering events where event.action is fork, exec, or end
should: [
{ term: { [EVENT_ACTION]: EventAction.exec } },
{ term: { [EVENT_ACTION]: EventAction.fork } },
{ term: { [EVENT_ACTION]: EventAction.end } },
{ term: { [EVENT_ACTION]: 'exec' } },
{ term: { [EVENT_ACTION]: 'fork' } },
{ term: { [EVENT_ACTION]: 'end' } },
],
},
},

View file

@ -14,7 +14,6 @@ import { SESSION_VIEW_ERROR_MESSAGE } from './translations';
import { SESSION_VIEW_ERROR_TEST_ID, SESSION_VIEW_TEST_ID } from './test_ids';
import { useKibana } from '../../../common/lib/kibana';
import { useLeftPanelContext } from '../context';
import { getSessionViewProcessIndex } from '../../../common/components/header_actions/helpers';
export const SESSION_VIEW_ID = 'session_view';
export const SESSION_ENTITY_ID = 'process.entry_leader.entity_id';
@ -28,13 +27,12 @@ export const SessionView: FC = () => {
const { sessionView } = useKibana().services;
const { getFieldsData, indexName } = useLeftPanelContext();
const processIndex = getSessionViewProcessIndex(
getField(getFieldsData(KIBANA_ANCESTOR_INDEX)) || indexName
);
const ancestorIndex = getField(getFieldsData(KIBANA_ANCESTOR_INDEX)); // e.g in case of alert, we want to grab it's origin index
const sessionEntityId = getField(getFieldsData(SESSION_ENTITY_ID));
const sessionStartTime = getField(getFieldsData(SESSION_START_TIME));
const index = ancestorIndex || indexName;
if (!processIndex || !sessionEntityId || !sessionStartTime) {
if (!index || !sessionEntityId || !sessionStartTime) {
return (
<EuiEmptyPrompt
iconType="error"
@ -49,7 +47,7 @@ export const SessionView: FC = () => {
return (
<div data-test-subj={SESSION_VIEW_TEST_ID}>
{sessionView.getSessionView({
processIndex,
index,
sessionEntityId,
sessionStartTime,
isFullScreen: true,

View file

@ -253,7 +253,7 @@ describe('GraphOverlay', () => {
[timelineId]: {
...mockGlobalState.timeline.timelineById[timelineId],
sessionViewConfig: {
processIndex: 'logs-endpoint.events.process*',
index: 'logs-endpoint.events.process*',
sessionEntityId: 'testId',
sessionStartTime: '2021-10-14T08:05:34.853Z',
},

View file

@ -8,6 +8,7 @@
export const SESSION_VIEW_APP_ID = 'sessionView';
// routes
export const CURRENT_API_VERSION = '1';
export const PROCESS_EVENTS_ROUTE = '/internal/session_view/process_events';
export const ALERTS_ROUTE = '/internal/session_view/alerts';
export const ALERT_STATUS_ROUTE = '/internal/session_view/alert_status';
@ -73,3 +74,105 @@ export const ALERT_ICONS: { [key: string]: string } = {
};
export const DEFAULT_ALERT_FILTER_VALUE = 'all';
export const ALERT = 'alert';
// a list of fields to pull in ES queries for process_events_route, io_events_route and alerts.
export const PROCESS_EVENT_FIELDS = [
'@timestamp',
'event.id',
'event.kind',
'event.type',
'event.action',
'event.category',
'user.id',
'user.name',
'group.id',
'group.name',
'host.architecture',
'host.hostname',
'host.id',
'host.ip',
'host.mac',
'host.name',
'host.os.family',
'host.os.full',
'host.os.kernel',
'host.os.name',
'host.platform',
'host.version',
'host.boot.id',
'process.entity_id',
'process.args',
'process.command_line',
'process.executable',
'process.name',
'process.interactive',
'process.working_directory',
'process.pid',
'process.start',
'process.end',
'process.user.id',
'process.user.name',
'process.group.id',
'process.group.name',
'process.supplemental_groups',
'process.exit_code',
'process.tty.*',
'process.entry_leader.*',
'process.session_leader.*',
'process.group_leader.*',
'process.parent.*',
'process.previous',
'container.id',
'container.name',
'container.image.name',
'container.image.tag',
'container.image.hash.all',
'orchestrator.resource.name',
'orchestrator.resource.type',
'orchestrator.resource.ip',
'orchestrator.resource.parent.type',
'orchestrator.resource.labels',
'orchestrator.resource.annotations',
'orchestrator.namespace',
'orchestrator.cluster.name',
'orchestrator.cluster.id',
'cloud.instance.name',
'cloud.account.id',
'cloud.project.id',
'cloud.project.name',
'cloud.provider',
'cloud.region',
];
export const IO_EVENT_FIELDS = PROCESS_EVENT_FIELDS.concat(['process.io.*']);
export const ALERT_FIELDS = PROCESS_EVENT_FIELDS.concat([
'file.extension',
'file.path',
'file.name',
'network.type',
'network.transport',
'network.protocol',
'destination.address',
'destination.ip',
'destination.protocol',
'source.address',
'source.ip',
'source.protocol',
'kibana.alert.uuid',
'kibana.alert.reason',
'kibana.alert.workflow_status',
'kibana.alert.status',
'kibana.alert.original_time',
'kibana.alert.original_event.action',
'kibana.alert.rule.category',
'kibana.alert.rule.consumer',
'kibana.alert.rule.description',
'kibana.alert.rule.enabled',
'kibana.alert.rule.name',
'kibana.alert.rule.risk_score',
'kibana.alert.rule.severity',
'kibana.alert.rule.uuid',
'kibana.alert.rule.parameters.query',
'kibana.alert.rule.query',
]);

View file

@ -5,6 +5,30 @@
* 2.0.
*/
import { ENTRY_SESSION_ENTITY_ID_PROPERTY } from './constants';
import { EventAction } from './types/process_tree';
export { ENTRY_SESSION_ENTITY_ID_PROPERTY, EventAction };
export { ENTRY_SESSION_ENTITY_ID_PROPERTY } from './constants';
export type {
Aggregate,
AlertStatusEventEntityIdMap,
AlertTypeCount,
EventAction,
EventKind,
IOLine,
Process,
ProcessEvent,
ProcessEventAlert,
ProcessEventAlertCategory,
ProcessEventIPAddress,
ProcessEventResults,
ProcessEventsPage,
ProcessFields,
ProcessEventHost,
ProcessEventContainer,
ProcessEventOrchestrator,
ProcessEventCloud,
ProcessMap,
ProcessStartMarker,
Teletype,
} from './types/latest';
import type * as v1 from './types/v1';
export type { v1 };

View file

@ -5,17 +5,14 @@
* 2.0.
*/
import {
import type {
Process,
ProcessEvent,
ProcessEventsPage,
ProcessFields,
EventAction,
EventKind,
ProcessMap,
AlertTypeCount,
ProcessEventAlertCategory,
} from '../../types/process_tree';
} from '../..';
export const TEST_PROCESS_INDEX = 'logs-endpoint.events.process*';
export const TEST_SESSION_START_TIME = '2021-10-14T08:05:34.853Z';
@ -162,9 +159,9 @@ export const mockEvents: ProcessEvent[] = [
start: '2021-11-23T15:25:04.210Z',
},
event: {
action: EventAction.fork,
action: 'fork',
category: ['process'],
kind: EventKind.event,
kind: 'event',
id: '1',
},
host: {
@ -321,9 +318,9 @@ export const mockEvents: ProcessEvent[] = [
start: '2021-11-23T15:25:04.218Z',
},
event: {
action: EventAction.exec,
action: 'exec',
category: ['process'],
kind: EventKind.event,
kind: 'event',
id: '2',
},
},
@ -465,9 +462,9 @@ export const mockEvents: ProcessEvent[] = [
working_directory: '/home/vagrant',
},
event: {
action: EventAction.end,
action: 'end',
category: ['process'],
kind: EventKind.event,
kind: 'event',
id: '3',
},
host: {
@ -629,9 +626,9 @@ export const mockEvents: ProcessEvent[] = [
working_directory: '/home/vagrant',
},
event: {
action: EventAction.end,
action: 'end',
category: ['process'],
kind: EventKind.event,
kind: 'event',
id: '4',
},
host: {
@ -654,9 +651,9 @@ export const mockEvents: ProcessEvent[] = [
] as ProcessEvent[];
export const mockAlertTypeCounts: AlertTypeCount[] = [
{ category: ProcessEventAlertCategory.file, count: 0 },
{ category: ProcessEventAlertCategory.network, count: 2 },
{ category: ProcessEventAlertCategory.process, count: 1 },
{ category: 'file', count: 0 },
{ category: 'network', count: 2 },
{ category: 'process', count: 1 },
];
export const mockAlerts: ProcessEvent[] = [
@ -679,7 +676,7 @@ export const mockAlerts: ProcessEvent[] = [
reason: 'process event created low alert cmd test alert.',
original_time: '2021-11-23T15:25:04.218Z',
original_event: {
action: EventAction.exec,
action: 'exec',
},
uuid: '6bb22512e0e588d1a2449b61f164b216e366fba2de39e65d002ae734d71a6c38',
},
@ -824,9 +821,9 @@ export const mockAlerts: ProcessEvent[] = [
},
},
event: {
action: EventAction.exec,
action: 'exec',
category: ['process'],
kind: EventKind.signal,
kind: 'signal',
id: '5',
},
host: {
@ -865,7 +862,7 @@ export const mockAlerts: ProcessEvent[] = [
reason: 'process event created low alert cmd test alert.',
original_time: '2021-11-23T15:25:05.202Z',
original_event: {
action: EventAction.end,
action: 'end',
},
uuid: '2873463965b70d37ab9b2b3a90ac5a03b88e76e94ad33568285cadcefc38ed75',
},
@ -1011,9 +1008,9 @@ export const mockAlerts: ProcessEvent[] = [
},
},
event: {
action: EventAction.end,
action: 'end',
category: ['process'],
kind: EventKind.signal,
kind: 'signal',
id: '6',
},
host: {
@ -1035,7 +1032,7 @@ export const mockAlerts: ProcessEvent[] = [
},
];
export const mockFileAlert = {
export const mockFileAlert: ProcessEvent = {
kibana: {
alert: {
rule: {
@ -1054,7 +1051,7 @@ export const mockFileAlert = {
reason: 'process event created low alert File telemetry.',
original_time: '2021-11-23T15:25:05.202Z',
original_event: {
action: EventAction.end,
action: 'end',
},
uuid: '2873463965b70d37ab9b2b3a90ac5a03b88e76e94ad33568285cadcefc38ed75',
},
@ -1205,9 +1202,9 @@ export const mockFileAlert = {
name: 'new_file.txt',
},
event: {
action: EventAction.exec,
action: 'exec',
category: ['file'],
kind: EventKind.signal,
kind: 'signal',
id: '6',
},
host: {
@ -1228,7 +1225,7 @@ export const mockFileAlert = {
},
};
export const mockNetworkAlert = {
export const mockNetworkAlert: ProcessEvent = {
kibana: {
alert: {
rule: {
@ -1247,7 +1244,7 @@ export const mockNetworkAlert = {
reason: 'process event created low alert File telemetry.',
original_time: '2021-11-23T15:25:05.202Z',
original_event: {
action: EventAction.end,
action: 'end',
},
uuid: '2873463965b70d37ab9b2b3a90ac5a03b88e76e94ad33568285cadcefc38ed75',
},
@ -1408,9 +1405,9 @@ export const mockNetworkAlert = {
port: 1111,
},
event: {
action: EventAction.exec,
action: 'exec',
category: ['network'],
kind: EventKind.signal,
kind: 'signal',
id: '6',
},
host: {
@ -1687,9 +1684,9 @@ export const childProcessMock: Process = {
({
'@timestamp': '2021-11-23T15:25:05.210Z',
event: {
kind: EventKind.event,
kind: 'event',
category: ['process'],
action: EventAction.exec,
action: 'exec',
id: '1',
},
host: {
@ -1772,9 +1769,9 @@ export const processMock: Process = {
({
'@timestamp': '2021-11-23T15:25:04.210Z',
event: {
kind: EventKind.event,
kind: 'event',
category: ['process'],
action: EventAction.exec,
action: 'exec',
id: '2',
},
host: {
@ -1962,15 +1959,15 @@ export const mockProcessMap = mockEvents.reduce(
autoExpand: false,
searchMatched: null,
orphans: [],
addEvent: (_) => undefined,
addAlert: (_) => undefined,
addChild: (_) => undefined,
addEvent: () => undefined,
addAlert: () => undefined,
addChild: () => undefined,
clearSearch: () => undefined,
getChildren: () => [],
hasOutput: () => false,
hasAlerts: () => false,
getAlerts: () => [],
updateAlertsStatus: (_) => undefined,
updateAlertsStatus: () => undefined,
hasExec: () => false,
getOutput: () => '',
getDetails: () => event,

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { ProcessEventResults } from '../../types/process_tree';
import type { ProcessEventResults } from '../..';
export const sessionViewIOEventsMock: ProcessEventResults = {
events: [

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { ProcessEventResults } from '../../types/process_tree';
import type { ProcessEventResults } from '../..';
export const sessionViewProcessEventsMock: ProcessEventResults = {
events: [

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { ProcessEventResults } from '../../types/process_tree';
import type { ProcessEventResults } from '../..';
export const sessionViewProcessEventsMergedMock: ProcessEventResults = {
events: [

View file

@ -4,8 +4,4 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export interface Aggregate {
key: string | number;
doc_count: number;
}
export * from './v1';

View file

@ -1,11 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export interface DisplayOptionsState {
timestamp: boolean;
verboseMode: boolean;
}

View file

@ -4,6 +4,10 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export interface Aggregate {
key: string | number;
doc_count: number;
}
export interface AlertStatusEventEntityIdMap {
[alertUuid: string]: {
@ -12,30 +16,15 @@ export interface AlertStatusEventEntityIdMap {
};
}
export enum ProcessEventAlertCategory {
all = 'all',
file = 'file',
network = 'network',
process = 'process',
}
export interface AlertTypeCount {
category: ProcessEventAlertCategory;
count: number;
}
export type DefaultAlertFilterType = 'all';
export type EventKind = 'event' | 'signal';
export type EventCategory = 'process' | 'file' | 'network';
export type EventAction = 'fork' | 'exec' | 'end' | 'text_output';
export enum EventKind {
event = 'event',
signal = 'signal',
}
export enum EventAction {
fork = 'fork',
exec = 'exec',
end = 'end',
text_output = 'text_output',
}
export type ProcessEventAlertCategory = EventCategory | 'all';
export interface User {
id?: string;
@ -107,10 +96,6 @@ export interface ProcessFields {
end?: string;
user?: User;
group?: Group;
real_user?: User;
real_group?: Group;
saved_user?: User;
saved_group?: Group;
supplemental_groups?: Group[];
exit_code?: number;
entry_meta?: EntryMeta;
@ -191,7 +176,7 @@ export interface ProcessEvent {
'@timestamp'?: string;
event?: {
kind?: EventKind;
category?: string | string[];
category?: EventCategory | EventCategory[];
action?: EventAction | EventAction[];
type?: string | string[];
id?: string;
@ -273,6 +258,8 @@ export interface ProcessEventOrchestrator {
parent?: {
type?: string;
};
labels?: string[];
annotations?: string[];
};
namespace?: string;
cluster?: {

View file

@ -5,19 +5,18 @@
* 2.0.
*/
import { ProcessEventAlertCategory } from '../types/process_tree';
import { getAlertIconTooltipContent } from './alert_icon_tooltip_content';
describe('getAlertTypeTooltipContent(category)', () => {
it('should display `File alert` for tooltip content', () => {
expect(getAlertIconTooltipContent(ProcessEventAlertCategory.file)).toEqual('File alert');
expect(getAlertIconTooltipContent('file')).toEqual('File alert');
});
it('should display `Process alert` for tooltip content', () => {
expect(getAlertIconTooltipContent(ProcessEventAlertCategory.process)).toEqual('Process alert');
expect(getAlertIconTooltipContent('process')).toEqual('Process alert');
});
it('should display `Network alert` for tooltip content', () => {
expect(getAlertIconTooltipContent(ProcessEventAlertCategory.network)).toEqual('Network alert');
expect(getAlertIconTooltipContent('network')).toEqual('Network alert');
});
});

View file

@ -5,16 +5,18 @@
* 2.0.
*/
import { ProcessEventAlertCategory } from '../types/process_tree';
import { ProcessEventAlertCategory } from '..';
import * as i18n from '../translations';
export const getAlertIconTooltipContent = (processEventAlertCategory: string) => {
export const getAlertIconTooltipContent = (
processEventAlertCategory: ProcessEventAlertCategory
) => {
let tooltipContent = '';
switch (processEventAlertCategory) {
case ProcessEventAlertCategory.file:
case 'file':
tooltipContent = i18n.ALERT_TYPE_TOOLTIP_FILE;
break;
case ProcessEventAlertCategory.network:
case 'network':
tooltipContent = i18n.ALERT_TYPE_TOOLTIP_NETWORK;
break;
default:

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { Process } from '../types/process_tree';
import { Process } from '..';
export const sortProcesses = (a: Process, b: Process) => {
const eventAStartTime = a.getDetails()?.process?.start || 0;

View file

@ -16,7 +16,7 @@ import {
JUMP_TO_PROCESS_TEST_ID,
} from '.';
import { mockAlerts } from '../../../common/mocks/constants/session_view_process.mock';
import { ProcessEvent } from '../../../common/types/process_tree';
import { ProcessEvent } from '../../../common';
describe('DetailPanelAlertActions component', () => {
let render: () => ReturnType<AppContextTestRender['render']>;

View file

@ -8,7 +8,7 @@ import React, { useState, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiPopover, EuiContextMenuPanel, EuiButtonIcon, EuiContextMenuItem } from '@elastic/eui';
import { ProcessEvent } from '../../../common/types/process_tree';
import { ProcessEvent } from '../../../common';
export const BUTTON_TEST_ID = 'sessionView:detailPanelAlertActionsBtn';
export const SHOW_DETAILS_TEST_ID = 'sessionView:detailPanelAlertActionShowDetails';

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { EuiIcon, EuiText, EuiAccordion, EuiNotificationBadge } from '@elastic/eui';
import { ProcessEvent } from '../../../common/types/process_tree';
import type { ProcessEvent } from '../../../common';
import { dataOrDash } from '../../utils/data_or_dash';
import { useStyles } from '../detail_panel_alert_list_item/styles';
import { DetailPanelAlertListItem } from '../detail_panel_alert_list_item';

View file

@ -20,7 +20,7 @@ import {
} from '@elastic/eui';
import { getAlertIconTooltipContent } from '../../../common/utils/alert_icon_tooltip_content';
import { ALERT_ICONS } from '../../../common/constants';
import { ProcessEvent, ProcessEventAlertCategory } from '../../../common/types/process_tree';
import type { ProcessEvent, ProcessEventAlertCategory } from '../../../common';
import { useStyles } from './styles';
import { DetailPanelAlertActions } from '../detail_panel_alert_actions';
import { dataOrDash } from '../../utils/data_or_dash';
@ -64,12 +64,12 @@ export const DetailPanelAlertListItem = ({
const { args, name: processName } = event.process ?? {};
const { event: processEvent } = event;
const forceState = !isInvestigated ? 'open' : undefined;
const category = Array.isArray(processEvent?.category)
? processEvent?.category?.[0]
: processEvent?.category;
const processEventAlertCategory = category ?? ProcessEventAlertCategory.process;
const category = (
Array.isArray(processEvent?.category) ? processEvent?.category?.[0] : processEvent?.category
) as ProcessEventAlertCategory;
const processEventAlertCategory: ProcessEventAlertCategory = category ?? 'process';
const alertCategoryDetailDisplayText =
category !== ProcessEventAlertCategory.process
category !== 'process'
? `${dataOrDash(processName)} ${getAlertCategoryDisplayText(event, category)}`
: dataOrDash(args?.join(' '));
const alertIconTooltipContent = getAlertIconTooltipContent(processEventAlertCategory);

View file

@ -9,7 +9,7 @@ import { EuiEmptyPrompt, EuiButtonGroup, EuiHorizontalRule, EuiButton } from '@e
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { groupBy } from 'lodash';
import { ProcessEvent } from '../../../common/types/process_tree';
import type { ProcessEvent } from '../../../common';
import { useStyles } from './styles';
import { DetailPanelAlertListItem } from '../detail_panel_alert_list_item';
import { DetailPanelAlertGroupItem } from '../detail_panel_alert_group_item';

View file

@ -4,12 +4,12 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
import type {
ProcessEventHost,
ProcessEventContainer,
ProcessEventOrchestrator,
ProcessEventCloud,
} from '../../../common/types/process_tree';
} from '../../../common';
import { DASH } from '../../constants';
import { getHostData, getContainerData, getOrchestratorData, getCloudData } from './helpers';

View file

@ -5,12 +5,12 @@
* 2.0.
*/
import {
import type {
ProcessEventHost,
ProcessEventContainer,
ProcessEventOrchestrator,
ProcessEventCloud,
} from '../../../common/types/process_tree';
} from '../../../common';
import { DASH } from '../../constants';
import {
DetailPanelHost,

View file

@ -7,12 +7,12 @@
import React from 'react';
import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
import {
import type {
ProcessEventHost,
ProcessEventContainer,
ProcessEventOrchestrator,
ProcessEventCloud,
} from '../../../common/types/process_tree';
} from '../../../common';
import { DetailPanelMetadataTab } from '.';
const TEST_ARCHITECTURE = 'x86_64';

View file

@ -7,12 +7,12 @@
import React, { useMemo } from 'react';
import { EuiTextColor, EuiPanel } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import {
import type {
ProcessEventHost,
ProcessEventContainer,
ProcessEventOrchestrator,
ProcessEventCloud,
} from '../../../common/types/process_tree';
} from '../../../common';
import { DetailPanelAccordion } from '../detail_panel_accordion';
import { DetailPanelCopy } from '../detail_panel_copy';
import { DetailPanelListItem } from '../detail_panel_list_item';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { EventAction, Process, ProcessFields } from '../../../common/types/process_tree';
import type { Process, ProcessFields } from '../../../common';
import { DetailPanelProcess, DetailPanelProcessLeader } from '../../types';
import { DASH } from '../../constants';
import { dataOrDash } from '../../utils/data_or_dash';
@ -132,7 +132,7 @@ export const getDetailPanelProcess = (process: Process | null): DetailPanelProce
}
processData.executable = executables.map((exe, i) => {
const action = i === 0 ? EventAction.fork : EventAction.exec;
const action = i === 0 ? 'fork' : 'exec';
return [exe, `(${action})`];
});

View file

@ -7,7 +7,7 @@
import React, { ReactNode, useCallback, useMemo } from 'react';
import { EuiTextColor } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Process } from '../../../common/types/process_tree';
import type { Process } from '../../../common';
import { DetailPanelAccordion } from '../detail_panel_accordion';
import { DetailPanelCopy } from '../detail_panel_copy';
import { DetailPanelDescriptionList } from '../detail_panel_description_list';

View file

@ -10,12 +10,12 @@ import {
mockAlerts,
mockProcessMap,
} from '../../../common/mocks/constants/session_view_process.mock';
import {
import type {
AlertStatusEventEntityIdMap,
Process,
ProcessMap,
ProcessEvent,
} from '../../../common/types/process_tree';
} from '../../../common';
import { ALERT_STATUS } from '../../../common/constants';
import {
updateAlertEventStatus,

View file

@ -7,14 +7,13 @@
import { v4 as uuidv4 } from 'uuid';
import { escapeRegExp } from 'lodash';
import { sortProcesses } from '../../../common/utils/sort_processes';
import {
import type {
AlertStatusEventEntityIdMap,
EventKind,
Process,
ProcessEvent,
ProcessMap,
ProcessFields,
} from '../../../common/types/process_tree';
} from '../../../common';
import { ProcessImpl } from './hooks';
// Creates an instance of Process, from a nested leader process fieldset
@ -94,7 +93,7 @@ export const updateProcessMap = (processMap: ProcessMap, events: ProcessEvent[])
if (event.kibana?.alert) {
process.addAlert(event);
} else if (event.event?.kind === EventKind.event) {
} else if (event.event?.kind === 'event') {
process.addEvent(event);
}
});

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { EventAction } from '../../../common/types/process_tree';
import { mockEvents } from '../../../common/mocks/constants/session_view_process.mock';
import { ProcessImpl } from './hooks';
@ -23,7 +22,7 @@ describe('ProcessTree hooks', () => {
result = process.getDetails();
expect(result.event?.action).toEqual(EventAction.exec);
expect(result.event?.action).toEqual('exec');
});
});
});

View file

@ -7,7 +7,7 @@
import memoizeOne from 'memoize-one';
import { sortedUniqBy } from 'lodash';
import { useState, useEffect, useMemo } from 'react';
import {
import type {
AlertStatusEventEntityIdMap,
EventAction,
EventKind,
@ -15,7 +15,7 @@ import {
ProcessEvent,
ProcessMap,
ProcessEventsPage,
} from '../../../common/types/process_tree';
} from '../../../common';
import {
inferProcessFromLeaderInfo,
updateAlertEventStatus,
@ -140,7 +140,7 @@ export class ProcessImpl implements Process {
}
hasOutput() {
return !!this.findEventByAction(this.events, EventAction.text_output);
return !!this.findEventByAction(this.events, 'text_output');
}
hasAlerts() {
@ -164,11 +164,11 @@ export class ProcessImpl implements Process {
}
hasExec() {
return !!this.findEventByAction(this.events, EventAction.exec);
return !!this.findEventByAction(this.events, 'exec');
}
hasExited() {
return !!this.findEventByAction(this.events, EventAction.end);
return !!this.findEventByAction(this.events, 'end');
}
getDetails() {
@ -181,7 +181,7 @@ export class ProcessImpl implements Process {
}
getEndTime() {
const endEvent = this.findEventByAction(this.events, EventAction.end);
const endEvent = this.findEventByAction(this.events, 'end');
return endEvent?.['@timestamp'] || '';
}
@ -241,11 +241,7 @@ export class ProcessImpl implements Process {
const filtered = events.filter((processEvent) => {
const action = processEvent?.event?.action;
return (
action?.includes(EventAction.fork) ||
action?.includes(EventAction.exec) ||
action?.includes(EventAction.end)
);
return action?.includes('fork') || action?.includes('exec') || action?.includes('end');
});
// there are some anomalous processes which are omitting event.action

View file

@ -11,7 +11,7 @@ import {
nullMockData,
deepNullMockData,
} from '../../../common/mocks/constants/session_view_process.mock';
import { Process } from '../../../common/types/process_tree';
import type { Process } from '../../../common';
import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
import { ProcessTreeDeps, ProcessTree } from '.';
import { useDateFormat } from '../../hooks';

View file

@ -11,11 +11,7 @@ import { BackToInvestigatedAlert } from '../back_to_investigated_alert';
import { useProcessTree } from './hooks';
import { collapseProcessTree } from './helpers';
import { ProcessTreeLoadMoreButton } from '../process_tree_load_more_button';
import {
AlertStatusEventEntityIdMap,
Process,
ProcessEventsPage,
} from '../../../common/types/process_tree';
import type { AlertStatusEventEntityIdMap, Process, ProcessEventsPage } from '../../../common';
import { useScroll } from '../../hooks/use_scroll';
import { useStyles } from './styles';
import { PROCESS_EVENTS_PER_PAGE } from '../../../common/constants';

View file

@ -17,11 +17,7 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { ALERT_ICONS } from '../../../common/constants';
import {
ProcessEvent,
ProcessEventAlert,
ProcessEventAlertCategory,
} from '../../../common/types/process_tree';
import type { ProcessEvent, ProcessEventAlert, ProcessEventAlertCategory } from '../../../common';
import { dataOrDash } from '../../utils/data_or_dash';
import { getBadgeColorFromAlertStatus } from './helpers';
import { useStyles } from './styles';
@ -48,7 +44,7 @@ export const ProcessTreeAlert = ({
const { event } = alert;
const { uuid, rule, workflow_status: status } = alert.kibana?.alert || {};
const category = event?.category?.[0];
const category = event?.category?.[0] as ProcessEventAlertCategory;
const alertIconType = useMemo(() => {
if (category && category in ALERT_ICONS) return ALERT_ICONS[category];
return ALERT_ICONS.process;
@ -76,7 +72,7 @@ export const ProcessTreeAlert = ({
return null;
}
const { name } = rule;
const processEventAlertCategory = category ?? ProcessEventAlertCategory.process;
const processEventAlertCategory: ProcessEventAlertCategory = category ?? 'process';
const alertCategoryDetailDisplayText = getAlertCategoryDisplayText(alert, category);
const alertIconTooltipContent = getAlertIconTooltipContent(processEventAlertCategory);
const eventType = Array.isArray(event?.type) ? event?.type?.[0] : event?.type;

View file

@ -7,13 +7,12 @@
import React, { useState, useEffect, useRef, MouseEvent, useCallback, useMemo } from 'react';
import { useStyles } from './styles';
import {
import type {
ProcessEventAlertCategory,
DefaultAlertFilterType,
ProcessEvent,
ProcessEventAlert,
AlertTypeCount,
} from '../../../common/types/process_tree';
} from '../../../common';
import { ProcessTreeAlert } from '../process_tree_alert';
import { DEFAULT_ALERT_FILTER_VALUE, MOUSE_EVENT_PLACEHOLDER } from '../../../common/constants';
import { ProcessTreeAlertsFilter } from '../process_tree_alerts_filter';
@ -36,9 +35,8 @@ export function ProcessTreeAlerts({
onShowAlertDetails,
}: ProcessTreeAlertsDeps) {
const [selectedAlert, setSelectedAlert] = useState<ProcessEventAlert | null>(null);
const [selectedProcessEventAlertCategory, setSelectedProcessEventAlertCategory] = useState<
ProcessEventAlertCategory | DefaultAlertFilterType
>(DEFAULT_ALERT_FILTER_VALUE);
const [selectedProcessEventAlertCategory, setSelectedProcessEventAlertCategory] =
useState<ProcessEventAlertCategory>(DEFAULT_ALERT_FILTER_VALUE);
const styles = useStyles();
useEffect(() => {
@ -83,11 +81,7 @@ export function ProcessTreeAlerts({
);
const handleProcessEventAlertCategorySelected = useCallback((eventCategory) => {
if (ProcessEventAlertCategory.hasOwnProperty(eventCategory)) {
setSelectedProcessEventAlertCategory(eventCategory);
} else {
setSelectedProcessEventAlertCategory(ProcessEventAlertCategory.all);
}
setSelectedProcessEventAlertCategory(eventCategory);
}, []);
const filteredProcessEventAlerts = useMemo(() => {

View file

@ -18,11 +18,7 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { DEFAULT_ALERT_FILTER_VALUE } from '../../../common/constants';
import {
ProcessEventAlertCategory,
DefaultAlertFilterType,
AlertTypeCount,
} from '../../../common/types/process_tree';
import type { ProcessEventAlertCategory, AlertTypeCount } from '../../../common';
import { useStyles } from './styles';
import { FILTER_MENU_OPTIONS, SELECTED_PROCESS } from './translations';
@ -30,7 +26,7 @@ export interface ProcessTreeAlertsFilterDeps {
totalAlertsCount: number;
alertTypeCounts: AlertTypeCount[];
filteredAlertsCount: number;
onAlertEventCategorySelected: (value: ProcessEventAlertCategory | DefaultAlertFilterType) => void;
onAlertEventCategorySelected: (value: ProcessEventAlertCategory) => void;
}
export const ProcessTreeAlertsFilter = ({
@ -42,7 +38,7 @@ export const ProcessTreeAlertsFilter = ({
const { filterStatus, popover } = useStyles();
const [selectedProcessEventAlertCategory, setSelectedProcessEventAlertCategory] =
useState<ProcessEventAlertCategory>(ProcessEventAlertCategory.all);
useState<ProcessEventAlertCategory>('all');
const [isPopoverOpen, setPopover] = useState(false);
@ -88,7 +84,7 @@ export const ProcessTreeAlertsFilter = ({
);
const alertEventCategoryFilterMenuItems = useMemo(() => {
const getIconType = (eventCategory: ProcessEventAlertCategory | DefaultAlertFilterType) => {
const getIconType = (eventCategory: ProcessEventAlertCategory) => {
return eventCategory === selectedProcessEventAlertCategory ? 'check' : 'empty';
};
@ -114,7 +110,7 @@ export const ProcessTreeAlertsFilter = ({
icon={getIconType(DEFAULT_ALERT_FILTER_VALUE)}
onClick={onSelectedProcessEventAlertCategory}
>
{FILTER_MENU_OPTIONS[ProcessEventAlertCategory.all]}
{FILTER_MENU_OPTIONS.all}
</EuiContextMenuItem>,
...alertEventFilterMenuItems,
];

View file

@ -7,7 +7,7 @@
import React, { useMemo } from 'react';
import { EuiButton, EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { AlertTypeCount } from '../../../common/types/process_tree';
import type { AlertTypeCount } from '../../../common';
import { useButtonStyles } from './use_button_styles';
import { ALERT_ICONS } from '../../../common/constants';

View file

@ -24,11 +24,7 @@ import { EuiButton, EuiIcon, EuiToolTip, formatDate, EuiButtonIcon } from '@elas
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { chain } from 'lodash';
import {
AlertTypeCount,
Process,
ProcessEventAlertCategory,
} from '../../../common/types/process_tree';
import type { AlertTypeCount, Process, ProcessEventAlertCategory } from '../../../common';
import { dataOrDash } from '../../utils/data_or_dash';
import { useVisible } from '../../hooks/use_visible';
import { ProcessTreeAlerts } from '../process_tree_alerts';

View file

@ -9,12 +9,12 @@ import { useQuery, useInfiniteQuery } from '@tanstack/react-query';
import { EuiSearchBarOnChangeArgs } from '@elastic/eui';
import { CoreStart } from '@kbn/core/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import {
import type {
AlertStatusEventEntityIdMap,
EventAction,
ProcessEvent,
ProcessEventResults,
} from '../../../common/types/process_tree';
EventAction,
} from '../../../common';
import {
ALERTS_ROUTE,
PROCESS_EVENTS_ROUTE,
@ -25,6 +25,7 @@ import {
QUERY_KEY_PROCESS_EVENTS,
QUERY_KEY_ALERTS,
QUERY_KEY_GET_TOTAL_IO_BYTES,
CURRENT_API_VERSION,
} from '../../../common/constants';
export const useFetchSessionViewProcessEvents = (
@ -51,6 +52,7 @@ export const useFetchSessionViewProcessEvents = (
}
const res = await http.get<ProcessEventResults>(PROCESS_EVENTS_ROUTE, {
version: CURRENT_API_VERSION,
query: {
index,
sessionEntityId,
@ -69,12 +71,10 @@ export const useFetchSessionViewProcessEvents = (
const isRefetch = pages.length === 1 && jumpToCursor;
if (isRefetch || lastPage.events.length >= PROCESS_EVENTS_PER_PAGE) {
const filtered = lastPage.events.filter((event) => {
const action = event.event?.action;
const action = event.event?.action as EventAction;
return (
action &&
(action.includes(EventAction.fork) ||
action.includes(EventAction.exec) ||
action.includes(EventAction.end))
(action.includes('fork') || action.includes('exec') || action.includes('end'))
);
});
@ -90,12 +90,9 @@ export const useFetchSessionViewProcessEvents = (
},
getPreviousPageParam: (firstPage, pages) => {
const filtered = firstPage.events.filter((event) => {
const action = event.event?.action;
const action = event.event?.action as EventAction;
return (
action &&
(action.includes(EventAction.fork) ||
action.includes(EventAction.exec) ||
action.includes(EventAction.end))
action && (action.includes('fork') || action.includes('exec') || action.includes('end'))
);
});
@ -142,6 +139,7 @@ export const useFetchSessionViewAlerts = (
const { cursor } = pageParam;
const res = await http.get<ProcessEventResults>(ALERTS_ROUTE, {
version: CURRENT_API_VERSION,
query: {
sessionEntityId,
sessionStartTime,
@ -189,6 +187,7 @@ export const useFetchAlertStatus = (
}
const res = await http.get<ProcessEventResults>(ALERT_STATUS_ROUTE, {
version: CURRENT_API_VERSION,
query: {
alertUuid,
},
@ -227,6 +226,7 @@ export const useFetchGetTotalIOBytes = (
cachingKeys,
async () => {
return http.get<{ total: number }>(GET_TOTAL_IO_BYTES_ROUTE, {
version: CURRENT_API_VERSION,
query: {
index,
sessionEntityId,

View file

@ -54,7 +54,7 @@ describe('SessionView component', () => {
render = () =>
(renderResult = mockedContext.render(
<SessionView
processIndex={TEST_PROCESS_INDEX}
index={TEST_PROCESS_INDEX}
sessionStartTime={TEST_SESSION_START_TIME}
sessionEntityId="test-entity-id"
/>

View file

@ -21,13 +21,9 @@ import useLocalStorage from 'react-use/lib/useLocalStorage';
import byteSize from 'byte-size';
import { SectionLoading } from '../../shared_imports';
import { ProcessTree } from '../process_tree';
import {
AlertStatusEventEntityIdMap,
Process,
ProcessEvent,
} from '../../../common/types/process_tree';
import { DisplayOptionsState } from '../../../common/types/session_view';
import { SessionViewDeps } from '../../types';
import type { AlertStatusEventEntityIdMap, Process, ProcessEvent } from '../../../common';
import type { DisplayOptionsState } from '../session_view_display_options';
import type { SessionViewDeps } from '../../types';
import { SessionViewDetailPanel } from '../session_view_detail_panel';
import { SessionViewSearchBar } from '../session_view_search_bar';
import { SessionViewDisplayOptions } from '../session_view_display_options';
@ -46,7 +42,7 @@ import { REFRESH_SESSION, TOGGLE_TTY_PLAYER, DETAIL_PANEL } from './translations
* The main wrapper component for the session view.
*/
export const SessionView = ({
processIndex,
index,
sessionEntityId,
sessionStartTime,
height,
@ -132,7 +128,7 @@ export const SessionView = ({
hasPreviousPage,
refetch,
} = useFetchSessionViewProcessEvents(
processIndex,
index,
sessionEntityId,
sessionStartTime,
currentJumpToCursor
@ -148,7 +144,7 @@ export const SessionView = ({
} = useFetchSessionViewAlerts(sessionEntityId, sessionStartTime, investigatedAlertId);
const { data: totalTTYOutputBytes, refetch: refetchTotalTTYOutput } = useFetchGetTotalIOBytes(
processIndex,
index,
sessionEntityId,
sessionStartTime
);
@ -160,8 +156,8 @@ export const SessionView = ({
}, [totalTTYOutputBytes?.total]);
const handleRefresh = useCallback(() => {
refetch({ refetchPage: (_page, index, allPages) => allPages.length - 1 === index });
refetchAlerts({ refetchPage: (_page, index, allPages) => allPages.length - 1 === index });
refetch({ refetchPage: (_page, i, allPages) => allPages.length - 1 === i });
refetchAlerts({ refetchPage: (_page, i, allPages) => allPages.length - 1 === i });
refetchTotalTTYOutput();
}, [refetch, refetchAlerts, refetchTotalTTYOutput]);
@ -199,9 +195,9 @@ export const SessionView = ({
}, [newUpdatedAlertsStatus, fetchAlertStatus]);
const onSearchIndexChange = useCallback(
(index: number) => {
(i: number) => {
if (searchResults) {
const process = searchResults[index];
const process = searchResults[i];
if (process) {
onProcessSelected(process);
@ -431,7 +427,7 @@ export const SessionView = ({
}}
</EuiResizableContainer>
<TTYPlayer
index={processIndex}
index={index}
show={showTTY}
sessionEntityId={sessionEntityId}
sessionStartTime={sessionStartTime}

View file

@ -8,7 +8,7 @@ import React, { useState, useMemo, useCallback } from 'react';
import { EuiTabs, EuiTab, EuiNotificationBadge } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EuiTabProps } from '../../types';
import { Process, ProcessEvent } from '../../../common/types/process_tree';
import type { Process, ProcessEvent } from '../../../common';
import { getSelectedTabContent } from './helpers';
import { DetailPanelProcessTab } from '../detail_panel_process_tab';
import { DetailPanelMetadataTab } from '../detail_panel_metadata_tab';

View file

@ -18,7 +18,6 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiSelectableOption } from '@elastic/eui/src/components/selectable/selectable_option';
import { DisplayOptionsState } from '../../../common/types/session_view';
import { useStyles } from './styles';
const TIMESTAMP_OPTION_KEY = 'Timestamp';
@ -40,6 +39,11 @@ const VERBOSE_TOOLTIP_CONTENT = i18n.translate(
}
);
export interface DisplayOptionsState {
timestamp: boolean;
verboseMode: boolean;
}
export const SessionViewDisplayOptions = ({
onChange,
displayOptions,

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { Teletype } from '../../../common/types/process_tree';
import type { Teletype } from '../../../common';
import {
PROCESS_DATA_LIMIT_EXCEEDED_START,
PROCESS_DATA_LIMIT_EXCEEDED_END,

View file

@ -7,7 +7,7 @@
import { renderHook, act } from '@testing-library/react-hooks';
import { sessionViewIOEventsMock } from '../../../common/mocks/responses/session_view_io_events.mock';
import { useIOLines, useXtermPlayer, XtermPlayerDeps } from './hooks';
import { ProcessEventsPage } from '../../../common/types/process_tree';
import type { ProcessEventsPage } from '../../../common';
import { DEFAULT_TTY_FONT_SIZE, DEFAULT_TTY_PLAYSPEED_MS } from '../../../common/constants';
const VIM_LINE_START = 22;

View file

@ -14,13 +14,13 @@ import { SearchAddon } from './xterm_search';
import { useEuiTheme } from '../../hooks';
import { renderTruncatedMsg } from './ansi_helpers';
import {
import type {
IOLine,
ProcessStartMarker,
ProcessEvent,
ProcessEventResults,
ProcessEventsPage,
} from '../../../common/types/process_tree';
} from '../../../common';
import {
IO_EVENTS_ROUTE,
IO_EVENTS_PER_PAGE,
@ -31,6 +31,7 @@ import {
DEFAULT_TTY_COLS,
TTY_LINE_SPLITTER_REGEX,
TTY_LINES_PRE_SEEK,
CURRENT_API_VERSION,
} from '../../../common/constants';
export const useFetchIOEvents = (
@ -46,6 +47,7 @@ export const useFetchIOEvents = (
async ({ pageParam = {} }) => {
const { cursor } = pageParam;
const res = await http.get<ProcessEventResults>(IO_EVENTS_ROUTE, {
version: CURRENT_API_VERSION,
query: {
index,
sessionEntityId,

View file

@ -17,7 +17,7 @@ import { useKibana } from '@kbn/kibana-react-plugin/public';
import { CoreStart } from '@kbn/core/public';
import useResizeObserver from 'use-resize-observer';
import { throttle } from 'lodash';
import { ProcessEvent } from '../../../common/types/process_tree';
import type { ProcessEvent } from '../../../common';
import { TTYSearchBar } from '../tty_search_bar';
import { TTYTextSizer } from '../tty_text_sizer';
import { useStyles } from './styles';

View file

@ -9,7 +9,7 @@ import { useMemo } from 'react';
import { CSSObject, css } from '@emotion/react';
import { transparentize } from '@elastic/eui';
import { useEuiTheme } from '../../hooks';
import { Teletype } from '../../../common/types/process_tree';
import type { Teletype } from '../../../common';
export const useStyles = (tty?: Teletype, show?: boolean) => {
const { euiTheme, euiVars } = useEuiTheme();

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
import { ProcessEvent } from '../../../common/types/process_tree';
import type { ProcessEvent } from '../../../common';
import { TTYPlayerControls, TTYPlayerControlsDeps } from '.';
import { TTYPlayerLineMarkerType } from './tty_player_controls_markers';

View file

@ -16,7 +16,7 @@ import {
EuiRangeProps,
} from '@elastic/eui';
import { findIndex } from 'lodash';
import { ProcessStartMarker, ProcessEvent } from '../../../common/types/process_tree';
import type { ProcessStartMarker, ProcessEvent } from '../../../common';
import { useStyles } from './styles';
import {
TTY_END,

View file

@ -7,7 +7,7 @@
import React, { useMemo } from 'react';
import { EuiRange, EuiRangeProps, EuiToolTip } from '@elastic/eui';
import type { ProcessStartMarker } from '../../../../common/types/process_tree';
import type { ProcessStartMarker } from '../../../../common';
import { useStyles } from './styles';
import { PlayHead } from './play_head';

View file

@ -10,7 +10,7 @@ import userEvent from '@testing-library/user-event';
import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
import { sessionViewIOEventsMock } from '../../../common/mocks/responses/session_view_io_events.mock';
import { useIOLines } from '../tty_player/hooks';
import { ProcessEventsPage } from '../../../common/types/process_tree';
import type { ProcessEventsPage } from '../../../common';
import { TTYSearchBar, TTYSearchBarDeps } from '.';
// TTYSearchBar is a HOC to SessionViewSearchBar which is already well tested

View file

@ -7,7 +7,7 @@
import React, { useMemo, useState, useCallback } from 'react';
import stripAnsi from 'strip-ansi';
import { SessionViewSearchBar } from '../session_view_search_bar';
import { IOLine } from '../../../common/types/process_tree';
import type { IOLine } from '../../../common';
interface SearchResult {
line: IOLine;

View file

@ -12,7 +12,7 @@ import {
EuiFlexItem,
EuiToolTip,
} from '@elastic/eui';
import { Teletype } from '../../../common/types/process_tree';
import type { Teletype } from '../../../common';
import { DEFAULT_TTY_FONT_SIZE } from '../../../common/constants';
import { ZOOM_IN, ZOOM_FIT, ZOOM_OUT } from './translations';
import { useStyles } from './styles';

View file

@ -5,12 +5,10 @@
* 2.0.
*/
import { ENTRY_SESSION_ENTITY_ID_PROPERTY } from '../common/constants';
import { EventAction } from '../common/types/process_tree';
import { SessionViewPlugin } from './plugin';
export type { SessionViewStart } from './types';
export { ENTRY_SESSION_ENTITY_ID_PROPERTY, EventAction };
export { ENTRY_SESSION_ENTITY_ID_PROPERTY } from '../common';
export function plugin() {
return new SessionViewPlugin();

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { getIndexPattern, DEFAULT_INDEX, CLOUD_DEFEND_INDEX, ENDPOINT_INDEX } from '.';
const ENDPOINT_EVENT_INDEX = '.ds-logs-endpoint.events.process-default';
const CLOUD_DEFEND_EVENT_INDEX = '.ds-logs-cloud_defend.process-default';
const TEST_CLUSTER = 'aws';
describe('getIndexPattern', () => {
it('gets endpoint index pattern for events from endpoint', () => {
expect(getIndexPattern(ENDPOINT_EVENT_INDEX)).toEqual(ENDPOINT_INDEX);
});
it('gets cloud_defend index pattern for events from cloud-defend', () => {
expect(getIndexPattern(CLOUD_DEFEND_EVENT_INDEX)).toEqual(CLOUD_DEFEND_INDEX);
});
it('gets logs-* for everything else', () => {
expect(getIndexPattern('asdfasdfasdf')).toEqual(DEFAULT_INDEX);
});
it('preserves the cluster portion of the endpoint event index', () => {
expect(getIndexPattern(TEST_CLUSTER + ':' + ENDPOINT_EVENT_INDEX)).toEqual(
TEST_CLUSTER + ':' + ENDPOINT_INDEX
);
});
});

View file

@ -15,11 +15,46 @@ const queryClient = new QueryClient();
const SessionViewLazy = lazy(() => import('../components/session_view'));
const SUPPORTED_PACKAGES = ['endpoint', 'cloud_defend'];
const INDEX_REGEX = new RegExp(
`([a-z0-9_-]+\:)?[a-z0-9\-.]*(${SUPPORTED_PACKAGES.join('|')})`,
'i'
);
export const DEFAULT_INDEX = 'logs-*';
export const CLOUD_DEFEND_INDEX = 'logs-cloud_defend.*';
export const ENDPOINT_INDEX = 'logs-endpoint.events.process*';
// Currently both logs-endpoint.events.process* and logs-cloud_defend.process* are valid sources for session data.
// To avoid cross cluster searches, the original index of the event is used to infer the index to find data for the
// rest of the session.
export const getIndexPattern = (eventIndex?: string | null) => {
if (!eventIndex) {
return DEFAULT_INDEX;
}
const match = eventIndex.match(INDEX_REGEX);
const cluster = match?.[1];
const clusterStr = cluster ? `${cluster}` : '';
const service = match?.[2];
let index = DEFAULT_INDEX;
if (service === 'endpoint') {
index = ENDPOINT_INDEX;
} else if (service === 'cloud_defend') {
index = CLOUD_DEFEND_INDEX;
}
return clusterStr + index;
};
export const getSessionViewLazy = (props: SessionViewDeps) => {
const index = getIndexPattern(props.index);
return (
<QueryClientProvider client={queryClient}>
<Suspense fallback={<EuiLoadingSpinner />}>
<SessionViewLazy {...props} />
<SessionViewLazy {...props} index={index} />
</Suspense>
</QueryClientProvider>
);

View file

@ -11,7 +11,7 @@ export type SessionViewServices = CoreStart;
export interface SessionViewDeps {
// we pass in the index of the session leader that spawned session_view, this avoids having to query multiple cross cluster indices
processIndex: string;
index: string;
// the root node of the process tree to render. e.g process.entry.entity_id or process.session_leader.entity_id
sessionEntityId: string;

View file

@ -10,19 +10,14 @@ import {
mockNetworkAlert,
} from '../../common/mocks/constants/session_view_process.mock';
import { getAlertCategoryDisplayText, getAlertNetworkDisplay } from './alert_category_display_text';
import { ProcessEventAlertCategory } from '../../common/types/process_tree';
describe('getAlertCategoryDisplayText(alert, category)', () => {
it('should display file path when alert category is file', () => {
expect(getAlertCategoryDisplayText(mockFileAlert, ProcessEventAlertCategory?.file)).toEqual(
mockFileAlert?.file?.path
);
expect(getAlertCategoryDisplayText(mockFileAlert, 'file')).toEqual(mockFileAlert?.file?.path);
});
it('should display rule name when alert category is process', () => {
expect(getAlertCategoryDisplayText(mockAlerts[0], ProcessEventAlertCategory.process)).toEqual(
''
);
expect(getAlertCategoryDisplayText(mockAlerts[0], 'process')).toEqual('');
});
it('should display rule name when alert category is undefined', () => {
@ -31,20 +26,18 @@ describe('getAlertCategoryDisplayText(alert, category)', () => {
it('should display rule name when file path is undefined', () => {
const fileAlert = { ...mockFileAlert, file: {} };
expect(getAlertCategoryDisplayText(fileAlert, ProcessEventAlertCategory.file)).toEqual('');
expect(getAlertCategoryDisplayText(fileAlert, 'file')).toEqual('');
});
it('should display rule name when destination address is undefined and alert category is network', () => {
const networkAlert = { ...mockNetworkAlert, destination: undefined };
expect(getAlertCategoryDisplayText(networkAlert, ProcessEventAlertCategory.network)).toEqual(
''
);
expect(getAlertCategoryDisplayText(networkAlert, 'network')).toEqual('');
});
});
describe('getAlertNetworkDisplay(destination)', () => {
it('should show destination address and port', () => {
const text = `${mockNetworkAlert.destination.address}:${mockNetworkAlert.destination.port}`;
expect(getAlertNetworkDisplay(mockNetworkAlert.destination)).toEqual(text);
const text = `${mockNetworkAlert?.destination?.address}:${mockNetworkAlert?.destination?.port}`;
expect(getAlertNetworkDisplay(mockNetworkAlert?.destination)).toEqual(text);
});
it('should show only ip address when port does not exist', () => {

View file

@ -5,24 +5,20 @@
* 2.0.
*/
import {
ProcessEvent,
ProcessEventAlertCategory,
ProcessEventIPAddress,
} from '../../common/types/process_tree';
import type { ProcessEvent, ProcessEventIPAddress } from '../../common';
import { dataOrDash } from './data_or_dash';
export const getAlertCategoryDisplayText = (alert: ProcessEvent, category: string | undefined) => {
export const getAlertCategoryDisplayText = (alert: ProcessEvent, category?: string) => {
const destination = alert?.destination;
const filePath = alert?.file?.path;
if (filePath && category === ProcessEventAlertCategory.file) return dataOrDash(filePath);
if (destination?.address && category === ProcessEventAlertCategory.network)
if (filePath && category === 'file') return dataOrDash(filePath);
if (destination?.address && category === 'network')
return dataOrDash(getAlertNetworkDisplay(destination));
return '';
};
export const getAlertNetworkDisplay = (destination: ProcessEventIPAddress) => {
export const getAlertNetworkDisplay = (destination?: ProcessEventIPAddress) => {
const hasIpAddressPort = !!destination?.address && !!destination?.port;
const ipAddressPort = `${destination?.address}:${destination?.port}`;
return `${hasIpAddressPort ? ipAddressPort : destination?.address}`;

View file

@ -38,7 +38,7 @@ export class SessionViewPlugin implements Plugin {
// Register server routes
if (this.router) {
registerRoutes(this.router, plugins.ruleRegistry);
registerRoutes(this.router, this.logger, plugins.ruleRegistry);
}
}

View file

@ -5,7 +5,8 @@
* 2.0.
*/
import { schema } from '@kbn/config-schema';
import { IRouter } from '@kbn/core/server';
import { IRouter, Logger } from '@kbn/core/server';
import { transformError } from '@kbn/securitysolution-es-utils';
import type {
AlertsClient,
RuleRegistryPluginStartContract,
@ -19,25 +20,43 @@ import { expandDottedObject } from '../../common/utils/expand_dotted_object';
export const registerAlertStatusRoute = (
router: IRouter,
logger: Logger,
ruleRegistry: RuleRegistryPluginStartContract
) => {
router.get(
{
router.versioned
.get({
access: 'internal',
path: ALERT_STATUS_ROUTE,
validate: {
query: schema.object({
alertUuid: schema.string(),
}),
})
.addVersion(
{
version: '1',
validate: {
request: {
query: schema.object({
alertUuid: schema.string(),
}),
},
},
},
},
async (_context, request, response) => {
const client = await ruleRegistry.getRacClientWithRequest(request);
const { alertUuid } = request.query;
const body = await searchAlertByUuid(client, alertUuid);
async (_context, request, response) => {
const client = await ruleRegistry.getRacClientWithRequest(request);
const { alertUuid } = request.query;
try {
const body = await searchAlertByUuid(client, alertUuid);
return response.ok({ body });
}
);
return response.ok({ body });
} catch (err) {
const error = transformError(err);
logger.error(`Failed to fetch alert status: ${err}`);
return response.customError({
body: { message: error.message },
statusCode: error.statusCode,
});
}
}
);
};
export const searchAlertByUuid = async (client: AlertsClient, alertUuid: string) => {

View file

@ -5,7 +5,8 @@
* 2.0.
*/
import { schema } from '@kbn/config-schema';
import { IRouter } from '@kbn/core/server';
import { transformError } from '@kbn/securitysolution-es-utils';
import { IRouter, Logger } from '@kbn/core/server';
import type {
AlertsClient,
RuleRegistryPluginStartContract,
@ -17,46 +18,61 @@ import {
ALERT_UUID_PROPERTY,
ALERT_ORIGINAL_TIME_PROPERTY,
PREVIEW_ALERTS_INDEX,
ALERT_FIELDS,
} from '../../common/constants';
import { expandDottedObject } from '../../common/utils/expand_dotted_object';
export const registerAlertsRoute = (
router: IRouter,
logger: Logger,
ruleRegistry: RuleRegistryPluginStartContract
) => {
router.get(
{
router.versioned
.get({
access: 'internal',
path: ALERTS_ROUTE,
validate: {
query: schema.object({
sessionEntityId: schema.string(),
sessionStartTime: schema.string(),
investigatedAlertId: schema.maybe(schema.string()),
cursor: schema.maybe(schema.string()),
}),
})
.addVersion(
{
version: '1',
validate: {
request: {
query: schema.object({
sessionEntityId: schema.string(),
sessionStartTime: schema.string(),
investigatedAlertId: schema.maybe(schema.string()),
cursor: schema.maybe(schema.string()),
}),
},
},
},
},
async (_context, request, response) => {
const client = await ruleRegistry.getRacClientWithRequest(request);
const { sessionEntityId, sessionStartTime, investigatedAlertId, cursor } = request.query;
async (_context, request, response) => {
const client = await ruleRegistry.getRacClientWithRequest(request);
const { sessionEntityId, sessionStartTime, investigatedAlertId, cursor } = request.query;
try {
const body = await searchAlerts(
client,
sessionEntityId,
ALERTS_PER_PAGE,
investigatedAlertId,
[sessionStartTime],
cursor
);
try {
const body = await searchAlerts(
client,
sessionEntityId,
ALERTS_PER_PAGE,
investigatedAlertId,
[sessionStartTime],
cursor
);
return response.ok({ body });
} catch (err) {
return response.badRequest(err.message);
return response.ok({ body });
} catch (err) {
const error = transformError(err);
logger.error(`Failed to fetch alerts: ${err}`);
return response.customError({
body: { message: error.message },
statusCode: error.statusCode,
});
}
}
}
);
);
};
export const searchAlerts = async (
@ -101,12 +117,14 @@ export const searchAlerts = async (
index: indices.join(','),
sort: [{ '@timestamp': 'asc' }],
search_after: cursor ? [cursor] : undefined,
_source: ALERT_FIELDS,
});
// if an alert is being investigated, fetch it on it's own, as it's not guaranteed to come back in the above request.
// we only need to do this for the first page of alerts.
if (!cursor && investigatedAlertId) {
const investigatedAlertSearch = await client.find({
_source: ALERT_FIELDS,
query: {
match: {
[ALERT_UUID_PROPERTY]: investigatedAlertId,

View file

@ -3,7 +3,8 @@
* 2.0.
*/
import { schema } from '@kbn/config-schema';
import { IRouter } from '@kbn/core/server';
import { transformError } from '@kbn/securitysolution-es-utils';
import { IRouter, Logger } from '@kbn/core/server';
import { EVENT_ACTION } from '@kbn/rule-data-utils';
import {
GET_TOTAL_IO_BYTES_ROUTE,
@ -12,64 +13,77 @@ import {
TIMESTAMP_PROPERTY,
} from '../../common/constants';
export const registerGetTotalIOBytesRoute = (router: IRouter) => {
router.get(
{
export const registerGetTotalIOBytesRoute = (router: IRouter, logger: Logger) => {
router.versioned
.get({
access: 'internal',
path: GET_TOTAL_IO_BYTES_ROUTE,
validate: {
query: schema.object({
index: schema.string(),
sessionEntityId: schema.string(),
sessionStartTime: schema.string(),
}),
})
.addVersion(
{
version: '1',
validate: {
request: {
query: schema.object({
index: schema.string(),
sessionEntityId: schema.string(),
sessionStartTime: schema.string(),
}),
},
},
},
},
async (context, request, response) => {
const client = (await context.core).elasticsearch.client.asCurrentUser;
const { index, sessionEntityId, sessionStartTime } = request.query;
async (context, request, response) => {
const client = (await context.core).elasticsearch.client.asCurrentUser;
const { index, sessionEntityId, sessionStartTime } = request.query;
try {
const search = await client.search({
index: [index],
body: {
query: {
bool: {
must: [
{ term: { [ENTRY_SESSION_ENTITY_ID_PROPERTY]: sessionEntityId } },
{ term: { [EVENT_ACTION]: 'text_output' } },
{
range: {
// optimization to prevent data before this session from being hit.
[TIMESTAMP_PROPERTY]: {
gte: sessionStartTime,
try {
const search = await client.search({
index: [index],
body: {
query: {
bool: {
must: [
{ term: { [ENTRY_SESSION_ENTITY_ID_PROPERTY]: sessionEntityId } },
{ term: { [EVENT_ACTION]: 'text_output' } },
{
range: {
// optimization to prevent data before this session from being hit.
[TIMESTAMP_PROPERTY]: {
gte: sessionStartTime,
},
},
},
},
],
],
},
},
},
size: 0,
aggs: {
total_bytes_captured: {
sum: {
field: TOTAL_BYTES_CAPTURED_PROPERTY,
size: 0,
aggs: {
total_bytes_captured: {
sum: {
field: TOTAL_BYTES_CAPTURED_PROPERTY,
},
},
},
},
},
});
});
const agg: any = search.aggregations?.total_bytes_captured;
const agg: any = search.aggregations?.total_bytes_captured;
return response.ok({ body: { total: agg?.value || 0 } });
} catch (err) {
// unauthorized
if (err?.meta?.statusCode === 403) {
return response.ok({ body: { total: 0 } });
return response.ok({ body: { total: agg?.value || 0 } });
} catch (err) {
const error = transformError(err);
logger.error(`Failed to fetch total io bytes: ${err}`);
// unauthorized
if (err?.meta?.statusCode === 403) {
return response.ok({ body: { total: 0 } });
}
return response.customError({
body: { message: error.message },
statusCode: error.statusCode,
});
}
return response.badRequest(err.message);
}
}
);
);
};

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { IRouter } from '@kbn/core/server';
import { IRouter, Logger } from '@kbn/core/server';
import { RuleRegistryPluginStartContract } from '@kbn/rule-registry-plugin/server';
import { registerProcessEventsRoute } from './process_events_route';
import { registerAlertsRoute } from './alerts_route';
@ -12,10 +12,14 @@ import { registerAlertStatusRoute } from './alert_status_route';
import { registerIOEventsRoute } from './io_events_route';
import { registerGetTotalIOBytesRoute } from './get_total_io_bytes_route';
export const registerRoutes = (router: IRouter, ruleRegistry: RuleRegistryPluginStartContract) => {
registerProcessEventsRoute(router, ruleRegistry);
registerAlertsRoute(router, ruleRegistry);
registerAlertStatusRoute(router, ruleRegistry);
registerIOEventsRoute(router);
registerGetTotalIOBytesRoute(router);
export const registerRoutes = (
router: IRouter,
logger: Logger,
ruleRegistry: RuleRegistryPluginStartContract
) => {
registerProcessEventsRoute(router, logger, ruleRegistry);
registerAlertsRoute(router, logger, ruleRegistry);
registerAlertStatusRoute(router, logger, ruleRegistry);
registerIOEventsRoute(router, logger);
registerGetTotalIOBytesRoute(router, logger);
};

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
import { EventAction, EventKind } from '../../common/types/process_tree';
import { searchProcessWithIOEvents } from './io_events_route';
const TEST_PROCESS_INDEX = 'logs-endpoint.events.process*';
@ -60,8 +59,8 @@ describe('io_events_route.ts', () => {
expect(body.length).toBe(1);
body.forEach((event) => {
expect(event._source?.event?.action).toBe(EventAction.text_output);
expect(event._source?.event?.kind).toBe(EventKind.event);
expect(event._source?.event?.action).toBe('text_output');
expect(event._source?.event?.kind).toBe('event');
expect(event._source?.process?.entity_id).toBe('mockId');
});
});

View file

@ -5,11 +5,11 @@
* 2.0.
*/
import { schema } from '@kbn/config-schema';
import { IRouter } from '@kbn/core/server';
import { transformError } from '@kbn/securitysolution-es-utils';
import { IRouter, Logger } from '@kbn/core/server';
import { EVENT_ACTION, TIMESTAMP } from '@kbn/rule-data-utils';
import type { ElasticsearchClient } from '@kbn/core/server';
import { Aggregate } from '../../common/types/aggregate';
import { EventAction, EventKind } from '../../common/types/process_tree';
import type { Aggregate } from '../../common';
import {
IO_EVENTS_ROUTE,
IO_EVENTS_PER_PAGE,
@ -17,73 +17,88 @@ import {
TIMESTAMP_PROPERTY,
PROCESS_ENTITY_ID_PROPERTY,
PROCESS_EVENTS_PER_PAGE,
IO_EVENT_FIELDS,
} from '../../common/constants';
export const registerIOEventsRoute = (router: IRouter) => {
router.get(
{
export const registerIOEventsRoute = (router: IRouter, logger: Logger) => {
router.versioned
.get({
access: 'internal',
path: IO_EVENTS_ROUTE,
validate: {
query: schema.object({
index: schema.string(),
sessionEntityId: schema.string(),
sessionStartTime: schema.string(),
cursor: schema.maybe(schema.string()),
pageSize: schema.maybe(schema.number()),
}),
})
.addVersion(
{
version: '1',
validate: {
request: {
query: schema.object({
index: schema.string(),
sessionEntityId: schema.string(),
sessionStartTime: schema.string(),
cursor: schema.maybe(schema.string()),
pageSize: schema.maybe(schema.number({ min: 1, max: IO_EVENTS_PER_PAGE })), // currently only set in FTR tests to test pagination
}),
},
},
},
},
async (context, request, response) => {
const client = (await context.core).elasticsearch.client.asCurrentUser;
const {
index,
sessionEntityId,
sessionStartTime,
cursor,
pageSize = IO_EVENTS_PER_PAGE,
} = request.query;
async (context, request, response) => {
const client = (await context.core).elasticsearch.client.asCurrentUser;
const {
index,
sessionEntityId,
sessionStartTime,
cursor,
pageSize = IO_EVENTS_PER_PAGE,
} = request.query;
try {
const search = await client.search({
index: [index],
body: {
query: {
bool: {
must: [
{ term: { [ENTRY_SESSION_ENTITY_ID_PROPERTY]: sessionEntityId } },
{ term: { [EVENT_ACTION]: 'text_output' } },
{
range: {
// optimization to prevent data before this session from being hit.
[TIMESTAMP_PROPERTY]: {
gte: sessionStartTime,
try {
const search = await client.search({
index: [index],
body: {
query: {
bool: {
must: [
{ term: { [ENTRY_SESSION_ENTITY_ID_PROPERTY]: sessionEntityId } },
{ term: { [EVENT_ACTION]: 'text_output' } },
{
range: {
// optimization to prevent data before this session from being hit.
[TIMESTAMP_PROPERTY]: {
gte: sessionStartTime,
},
},
},
},
],
],
},
},
size: Math.min(pageSize, IO_EVENTS_PER_PAGE),
sort: [{ [TIMESTAMP]: 'asc' }],
search_after: cursor ? [cursor] : undefined,
fields: IO_EVENT_FIELDS,
},
size: Math.min(pageSize, IO_EVENTS_PER_PAGE),
sort: [{ [TIMESTAMP]: 'asc' }],
search_after: cursor ? [cursor] : undefined,
},
});
});
const events = search.hits.hits;
const total =
typeof search.hits.total === 'number' ? search.hits.total : search.hits.total?.value;
const events = search.hits.hits;
const total =
typeof search.hits.total === 'number' ? search.hits.total : search.hits.total?.value;
return response.ok({ body: { total, events } });
} catch (err) {
// unauthorized
if (err?.meta?.statusCode === 403) {
return response.ok({ body: { total: 0, events: [] } });
return response.ok({ body: { total, events } });
} catch (err) {
const error = transformError(err);
logger.error(`Failed to fetch io events: ${err}`);
// unauthorized
if (err?.meta?.statusCode === 403) {
return response.ok({ body: { total: 0, events: [] } });
}
return response.customError({
body: { message: error.message },
statusCode: error.statusCode,
});
}
return response.badRequest(err.message);
}
}
);
);
};
export const searchProcessWithIOEvents = async (
@ -136,8 +151,8 @@ export const searchProcessWithIOEvents = async (
return buckets.map((bucket) => ({
_source: {
event: {
kind: EventKind.event,
action: EventAction.text_output,
kind: 'event',
action: 'text_output',
id: bucket.key,
},
process: {

View file

@ -13,7 +13,7 @@ import {
mockAlerts,
} from '../../common/mocks/constants/session_view_process.mock';
import { getAlertsClientMockInstance, resetAlertingAuthMock } from './alerts_client_mock.test';
import { EventAction, EventKind, ProcessEvent } from '../../common/types/process_tree';
import type { ProcessEvent } from '../../common';
const getEmptyResponse = async () => {
return {
@ -76,10 +76,10 @@ describe('process_events_route.ts', () => {
expect(body.events.length).toBe(mockEvents.length + mockAlerts.length);
const eventsOnly = body.events.filter(
(event) => (event._source as ProcessEvent)?.event?.kind === EventKind.event
(event) => (event._source as ProcessEvent)?.event?.kind === 'event'
);
const alertsOnly = body.events.filter(
(event) => (event._source as ProcessEvent)?.event?.kind === EventKind.signal
(event) => (event._source as ProcessEvent)?.event?.kind === 'signal'
);
expect(eventsOnly.length).toBe(mockEvents.length);
expect(alertsOnly.length).toBe(mockAlerts.length);
@ -104,7 +104,7 @@ describe('process_events_route.ts', () => {
const eventsWithoutOutput = body.events.filter((event) => {
const { action, kind } = (event._source as ProcessEvent)?.event || {};
return action !== EventAction.text_output && kind === EventKind.event;
return action !== 'text_output' && kind === 'event';
});
expect(eventsWithoutOutput[0]._source).toEqual(mockEvents[mockEvents.length - 1]);

View file

@ -5,9 +5,10 @@
* 2.0.
*/
import { schema } from '@kbn/config-schema';
import { transformError } from '@kbn/securitysolution-es-utils';
import _ from 'lodash';
import type { ElasticsearchClient } from '@kbn/core/server';
import { IRouter } from '@kbn/core/server';
import { IRouter, Logger } from '@kbn/core/server';
import type {
AlertsClient,
RuleRegistryPluginStartContract,
@ -19,57 +20,73 @@ import {
PROCESS_EVENTS_PER_PAGE,
ENTRY_SESSION_ENTITY_ID_PROPERTY,
TIMESTAMP_PROPERTY,
PROCESS_EVENT_FIELDS,
} from '../../common/constants';
import { ProcessEvent } from '../../common/types/process_tree';
import { ProcessEvent } from '../../common';
import { searchAlerts } from './alerts_route';
import { searchProcessWithIOEvents } from './io_events_route';
export const registerProcessEventsRoute = (
router: IRouter,
logger: Logger,
ruleRegistry: RuleRegistryPluginStartContract
) => {
router.get(
{
router.versioned
.get({
access: 'internal',
path: PROCESS_EVENTS_ROUTE,
validate: {
query: schema.object({
index: schema.string(),
sessionEntityId: schema.string(),
sessionStartTime: schema.string(),
cursor: schema.maybe(schema.string()),
forward: schema.maybe(schema.boolean()),
pageSize: schema.maybe(schema.number()),
}),
})
.addVersion(
{
version: '1',
validate: {
request: {
query: schema.object({
index: schema.string(),
sessionEntityId: schema.string(),
sessionStartTime: schema.string(),
cursor: schema.maybe(schema.string()),
forward: schema.maybe(schema.boolean()),
pageSize: schema.maybe(schema.number({ min: 1, max: PROCESS_EVENTS_PER_PAGE })), // currently only set in FTR tests to test pagination
}),
},
},
},
},
async (context, request, response) => {
const client = (await context.core).elasticsearch.client.asCurrentUser;
const alertsClient = await ruleRegistry.getRacClientWithRequest(request);
const { index, sessionEntityId, sessionStartTime, cursor, forward, pageSize } = request.query;
async (context, request, response) => {
const client = (await context.core).elasticsearch.client.asCurrentUser;
const alertsClient = await ruleRegistry.getRacClientWithRequest(request);
const { index, sessionEntityId, sessionStartTime, cursor, forward, pageSize } =
request.query;
try {
const body = await fetchEventsAndScopedAlerts(
client,
alertsClient,
index,
sessionEntityId,
sessionStartTime,
cursor,
forward,
pageSize
);
try {
const body = await fetchEventsAndScopedAlerts(
client,
alertsClient,
index,
sessionEntityId,
sessionStartTime,
cursor,
forward,
pageSize
);
return response.ok({ body });
} catch (err) {
// unauthorized
if (err.meta.statusCode === 403) {
return response.ok({ body: { total: 0, events: [] } });
return response.ok({ body });
} catch (err) {
const error = transformError(err);
logger.error(`Failed to fetch process events: ${err}`);
// unauthorized
if (err?.meta?.statusCode === 403) {
return response.ok({ body: { total: 0, events: [] } });
}
return response.customError({
body: { message: error.message },
statusCode: error.statusCode,
});
}
return response.badRequest(err.message);
}
}
);
);
};
export const fetchEventsAndScopedAlerts = async (
@ -114,6 +131,7 @@ export const fetchEventsAndScopedAlerts = async (
size: Math.min(pageSize, PROCESS_EVENTS_PER_PAGE),
sort: [{ '@timestamp': forward ? 'asc' : 'desc' }],
search_after: cursorMillis ? [cursorMillis] : undefined,
fields: PROCESS_EVENT_FIELDS,
},
});

View file

@ -34,6 +34,7 @@
"@kbn/config-schema",
"@kbn/alerting-plugin",
"@kbn/rule-data-utils",
"@kbn/securitysolution-es-utils",
],
"exclude": [
"target/**/*",

View file

@ -6,7 +6,7 @@
*/
import expect from '@kbn/expect';
import { IO_EVENTS_ROUTE } from '@kbn/session-view-plugin/common/constants';
import { IO_EVENTS_ROUTE, CURRENT_API_VERSION } from '@kbn/session-view-plugin/common/constants';
import { FtrProviderContext } from '../../common/ftr_provider_context';
const MOCK_INDEX = 'logs-endpoint.events.process*';
@ -22,6 +22,13 @@ export default function ioEventsTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
function getTestRoute() {
return supertest
.get(IO_EVENTS_ROUTE)
.set('kbn-xsrf', 'foo')
.set('Elastic-Api-Version', CURRENT_API_VERSION);
}
describe(`Session view - ${IO_EVENTS_ROUTE} - with a basic license`, () => {
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/session_view/process_events');
@ -33,8 +40,21 @@ export default function ioEventsTests({ getService }: FtrProviderContext) {
await esArchiver.unload('x-pack/test/functional/es_archives/session_view/io_events');
});
it(`${IO_EVENTS_ROUTE} fails when an invalid api version is specified`, async () => {
const response = await supertest
.get(IO_EVENTS_ROUTE)
.set('kbn-xsrf', 'foo')
.set('Elastic-Api-Version', '999999')
.query({
index: MOCK_INDEX,
sessionEntityId: MOCK_SESSION_ENTITY_ID,
sessionStartTime: MOCK_SESSION_START_TIME,
});
expect(response.status).to.be(400);
});
it(`${IO_EVENTS_ROUTE} returns a page of IO events`, async () => {
const response = await supertest.get(IO_EVENTS_ROUTE).set('kbn-xsrf', 'foo').query({
const response = await getTestRoute().query({
index: MOCK_INDEX,
sessionEntityId: MOCK_SESSION_ENTITY_ID,
sessionStartTime: MOCK_SESSION_START_TIME,
@ -56,7 +76,7 @@ export default function ioEventsTests({ getService }: FtrProviderContext) {
});
it(`${IO_EVENTS_ROUTE} returns a page of IO events (w cursor)`, async () => {
const response = await supertest.get(IO_EVENTS_ROUTE).set('kbn-xsrf', 'foo').query({
const response = await getTestRoute().query({
index: MOCK_INDEX,
sessionEntityId: MOCK_SESSION_ENTITY_ID,
sessionStartTime: MOCK_SESSION_START_TIME,

View file

@ -6,7 +6,10 @@
*/
import expect from '@kbn/expect';
import { PROCESS_EVENTS_ROUTE } from '@kbn/session-view-plugin/common/constants';
import {
PROCESS_EVENTS_ROUTE,
CURRENT_API_VERSION,
} from '@kbn/session-view-plugin/common/constants';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { User } from '../../../rule_registry/common/lib/authentication/types';
@ -44,6 +47,13 @@ export default function processEventsTests({ getService }: FtrProviderContext) {
const supertestWithoutAuth = getService('supertestWithoutAuth');
const esArchiver = getService('esArchiver');
function getTestRoute() {
return supertest
.get(PROCESS_EVENTS_ROUTE)
.set('kbn-xsrf', 'foo')
.set('Elastic-Api-Version', CURRENT_API_VERSION);
}
describe(`Session view - ${PROCESS_EVENTS_ROUTE} - with a basic license`, () => {
describe(`using typical process event data`, () => {
before(async () => {
@ -58,8 +68,21 @@ export default function processEventsTests({ getService }: FtrProviderContext) {
await esArchiver.unload('x-pack/test/functional/es_archives/session_view/io_events');
});
it(`${PROCESS_EVENTS_ROUTE} fails when an invalid api version is specified`, async () => {
const response = await supertest
.get(PROCESS_EVENTS_ROUTE)
.set('kbn-xsrf', 'foo')
.set('Elastic-Api-Version', '999999')
.query({
index: MOCK_INDEX,
sessionEntityId: MOCK_SESSION_ENTITY_ID,
sessionStartTime: MOCK_SESSION_START_TIME,
});
expect(response.status).to.be(400);
});
it(`${PROCESS_EVENTS_ROUTE} returns a page of process events`, async () => {
const response = await supertest.get(PROCESS_EVENTS_ROUTE).set('kbn-xsrf', 'foo').query({
const response = await getTestRoute().query({
index: MOCK_INDEX,
sessionEntityId: MOCK_SESSION_ENTITY_ID,
sessionStartTime: MOCK_SESSION_START_TIME,
@ -71,7 +94,7 @@ export default function processEventsTests({ getService }: FtrProviderContext) {
});
it(`${PROCESS_EVENTS_ROUTE} returns a page of process events (w alerts) (paging forward)`, async () => {
const response = await supertest.get(PROCESS_EVENTS_ROUTE).set('kbn-xsrf', 'foo').query({
const response = await getTestRoute().query({
index: MOCK_INDEX,
sessionEntityId: MOCK_SESSION_ENTITY_ID,
sessionStartTime: MOCK_SESSION_START_TIME,
@ -88,7 +111,7 @@ export default function processEventsTests({ getService }: FtrProviderContext) {
});
it(`${PROCESS_EVENTS_ROUTE} returns a page of process events (w alerts) (paging backwards)`, async () => {
const response = await supertest.get(PROCESS_EVENTS_ROUTE).set('kbn-xsrf', 'foo').query({
const response = await getTestRoute().query({
index: MOCK_INDEX,
sessionEntityId: MOCK_SESSION_ENTITY_ID,
sessionStartTime: MOCK_SESSION_START_TIME,
@ -196,7 +219,7 @@ export default function processEventsTests({ getService }: FtrProviderContext) {
});
it(`${PROCESS_EVENTS_ROUTE} returns a page of process events`, async () => {
const response = await supertest.get(PROCESS_EVENTS_ROUTE).set('kbn-xsrf', 'foo').query({
const response = await getTestRoute().query({
index: MOCK_INDEX,
sessionEntityId: MOCK_SESSION_ENTITY_ID,
sessionStartTime: MOCK_SESSION_START_TIME,