Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,7 @@
"vinyl": "^2.2.0",
"whatwg-fetch": "^3.0.0",
"xml2js": "^0.4.22",
"xterm": "^4.18.0",
"xterm": "^5.0.0",
"yauzl": "^2.10.0",
"yazl": "^2.5.1"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ describe('useSessionView with active timeline and a session id and graph event i
height: 1000,
sessionEntityId: 'test',
loadAlertDetails: mockDetails,
canAccessEndpointManagement: false,
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
useGlobalFullScreen,
} from '../../../../common/containers/use_full_screen';
import { detectionsTimelineIds } from '../../../containers/helpers';
import { useUserPrivileges } from '../../../../common/components/user_privileges';
import { timelineActions, timelineSelectors } from '../../../store/timeline';
import { timelineDefaults } from '../../../store/timeline/defaults';
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
Expand Down Expand Up @@ -266,6 +267,7 @@ export const useSessionView = ({
}, [scopeId]);
const { globalFullScreen } = useGlobalFullScreen();
const { timelineFullScreen } = useTimelineFullScreen();
const { canAccessEndpointManagement } = useUserPrivileges().endpointPrivileges;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't know about this one, nice!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does using the canAccessEndpointManagement accomplish?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ansi message will contain a link to /security/administration/policy which is a listing of the defend integration policies. Only users with this permission can access it. I'm passing it as a parameter since I can't import this hook into session_view as it would introduce a circular dependency.


const defaults = isTimelineScope(scopeId) ? timelineDefaults : tableDefaults;
const { sessionViewConfig, activeTab } = useDeepEqualSelector((state) => ({
Expand Down Expand Up @@ -310,9 +312,17 @@ export const useSessionView = ({
loadAlertDetails: openDetailsPanel,
isFullScreen: fullScreen,
height: heightMinusSearchBar,
canAccessEndpointManagement,
})
: null;
}, [fullScreen, openDetailsPanel, sessionView, sessionViewConfig, height]);
}, [
height,
sessionViewConfig,
sessionView,
openDetailsPanel,
fullScreen,
canAccessEndpointManagement,
]);

return {
openDetailsPanel,
Expand Down
5 changes: 5 additions & 0 deletions x-pack/plugins/session_view/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@
* 2.0.
*/

export const SESSION_VIEW_APP_ID = 'sessionView';

// routes
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';
export const IO_EVENTS_ROUTE = '/internal/session_view/io_events';
export const GET_TOTAL_IO_BYTES_ROUTE = '/internal/session_view/get_total_io_bytes';

export const SECURITY_APP_ID = 'security';
export const POLICIES_PAGE_PATH = '/administration/policy';

// index patterns
export const PROCESS_EVENTS_INDEX = '*:logs-endpoint.events.process*,logs-endpoint.events.process*'; // match on both cross cluster and local indices
export const PREVIEW_ALERTS_INDEX = '.preview.alerts-security.alerts-default';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ export const sessionViewIOEventsMock: ProcessEventResults = {
total_bytes_captured: 1024,
total_bytes_skipped: 0,
bytes_skipped: [],
max_bytes_per_process_exceeded: true,
text: '\u001b[38;5;130m 84 \n 85 \u001b[mif [[ $KILL_IMMEDIATELY == 1 ]]; then\n\u001b[38;5;130m 86 \u001b[m echo "WARNING: Not waiting for connections to close gracefully"\n\u001b[38;5;130m 87 \u001b[m echo "Press any key to continue... wsrep_reject_queries will be set to \'ALL_KILL\'"\n\u001b[38;5;130m 88 \u001b[m read a\n\u001b[38;5;130m 89 \u001b[m mysql -h127.0.0.1 -P3306 -uroot -e "set global wsrep_reject_queries=\'ALL_KILL\'"\n\u001b[38;5;130m 90 \u001b[melse\n\u001b[38;5;130m 91 \u001b[m # Stop accepting queries in mariadb, do not kill opened connections\n\u001b[38;5;130m 92 \u001b[m mysql -h127.0.0.1 -P3306 -uroot -e "set global wsrep_reject_queries=\'ALL\'"\n\u001b[38;5;130m 93 \u001b[mfi\n\u001b[38;5;130m 94 \n 95 \u001b[mexit_code=$?\n\u001b[38;5;130m 96 \u001b[mif [[ $exit_code != 0 ]]; then\n\u001b[38;5;130m 97 \u001b[m >&2 echo "Failed to set the reject of queries on Mysql node, exiting."\n\u001b[38;5;130m 98 \u001b[m exit $exit_code\n\u001b[38;5;130m 99 \u001b[melse\n\u001b[38;5;130m 100 \u001b[m echo "Successfully stopped accepting queries."\n\u001b[38;5;130m 101 \u001b[m if [[ $KILL_IMMEDIATELY == 1 ]]; then\n\u001b[38;5;130m 102 \u001b[m\u001b[8Cexit\n\u001b[38;5;130m 103 \u001b[m fi\n\u001b[38;5;130m 104 \u001b[mfi\n\u001b[38;5;130m 105 \n 106 \u001b[mif [[ $GRACE_PERIOD == -1 ]]; then\n\u001b[38;5;130m 107 \u001b[m set_number_grace_seconds\n\u001b[38;5;130m 108 \u001b[mfi\n\u001b[38;5;130m 109 \n 110 \u001b[mwait_for_connections\n\u001b[38;5;130m 111 \u001b[mif [[ $DB_CONNECTIONS_NUMBER != 0 ]]; then\n\u001b[38;5;130m 112 \u001b[m get_number_db_connections\n\u001b[38;5;130m 113 \u001b[m >&2 echo "ERROR: There are still $DB_CONNECTIONS_NUMBER opened DB connections."\n\u001b[38;5;130m 114 \u001b[m exit 3\n\u001b[38;5;130m 115 \u001b[mfi\b\b\u001b[?25h\u001b[?25l\nType :qa! and press <Enter> to abandon all changes and exit Vim\u0007\u001b[58;9H\u001b[?25h\u0007\u001b[?25l\u001b[59;1H\u001b[K\u001b[59;1H:\u001b[?2004h\u001b[?25hqa!\r\u001b[?25l\u001b[?2004l\u001b[59;1H\u001b[K\u001b[59;1H\u001b[?2004l\u001b[?1l\u001b>\u001b[?25h\u001b[?1049l\u001b[23;0;0t,\u001bkroot@staging-host:~\u001b\\\n',
},
tty: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export interface IOLine {
export interface ProcessStartMarker {
event: ProcessEvent;
line: number;
maxBytesExceeded?: boolean;
}

export interface IOFields {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const SessionView = ({
jumpToCursor,
investigatedAlertId,
loadAlertDetails,
canAccessEndpointManagement,
}: SessionViewDeps) => {
// don't engage jumpTo if jumping to session leader.
if (jumpToEntityId === sessionEntityId) {
Expand Down Expand Up @@ -422,6 +423,7 @@ export const SessionView = ({
isFullscreen={isFullScreen}
onJumpToEvent={onJumpToEvent}
autoSeekToEntityId={currentJumpToOutputEntityId}
canAccessEndpointManagement={canAccessEndpointManagement}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { Teletype } from '../../../common/types/process_tree';
import {
PROCESS_DATA_LIMIT_EXCEEDED_START,
PROCESS_DATA_LIMIT_EXCEEDED_END,
VIEW_POLICIES,
} from './translations';

export const renderTruncatedMsg = (tty?: Teletype, policiesUrl?: string, processName?: string) => {
if (tty?.columns) {
const lineBreak = '-'.repeat(tty.columns);
const message = ` ⚠ ${PROCESS_DATA_LIMIT_EXCEEDED_START} \x1b[1m${processName}.\x1b[22m ${PROCESS_DATA_LIMIT_EXCEEDED_END}`;
const link = policiesUrl
? `\x1b[${Math.min(
message.length + 2,
tty.columns - VIEW_POLICIES.length - 4
)}G\x1b[1m\x1b]8;;${policiesUrl}\x1b\\[ ${VIEW_POLICIES} ]\x1b]8;;\x1b\\\x1b[22m`
: '';

return `\n\x1b[33m${lineBreak}\n${message}${link}\n${lineBreak}\x1b[0m\n\n`;
}
};
42 changes: 36 additions & 6 deletions x-pack/plugins/session_view/public/components/tty_player/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { CoreStart } from '@kbn/core/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { SearchAddon } from './xterm_search';
import { useEuiTheme } from '../../hooks';
import { renderTruncatedMsg } from './ansi_helpers';

import {
IOLine,
Expand Down Expand Up @@ -103,6 +104,15 @@ export const useIOLines = (pages: ProcessEventsPage[] | undefined) => {
newMarkers.push(processLineInfo);
}

if (process.io.max_bytes_per_process_exceeded) {
const marker = newMarkers.find(
(item) => item.event.process?.entity_id === process.entity_id
);
if (marker) {
marker.maxBytesExceeded = true;
}
}

const splitLines = process.io.text.split(TTY_LINE_SPLITTER_REGEX);
const combinedLines = [splitLines[0]];

Expand Down Expand Up @@ -158,6 +168,7 @@ export interface XtermPlayerDeps {
hasNextPage?: boolean;
fetchNextPage?: () => void;
isFetching?: boolean;
policiesUrl?: string;
}

export const useXtermPlayer = ({
Expand All @@ -169,24 +180,29 @@ export const useXtermPlayer = ({
hasNextPage,
fetchNextPage,
isFetching,
policiesUrl,
}: XtermPlayerDeps) => {
const { euiTheme } = useEuiTheme();
const { font, colors } = euiTheme;
const [currentLine, setCurrentLine] = useState(0);
const [playSpeed] = useState(DEFAULT_TTY_PLAYSPEED_MS); // potentially configurable
const tty = lines?.[currentLine]?.event.process?.tty;

const processName = lines?.[currentLine]?.event.process?.name;
const [terminal, searchAddon] = useMemo(() => {
const term = new Terminal({
theme: {
selection: colors.warning,
selectionBackground: colors.warning,
selectionForeground: colors.ink,
yellow: colors.warning,
},
fontFamily: font.familyCode,
fontSize: DEFAULT_TTY_FONT_SIZE,
scrollback: 0,
convertEol: true,
rows: DEFAULT_TTY_ROWS,
cols: DEFAULT_TTY_COLS,
allowProposedApi: true,
allowTransparency: true,
});

const searchInstance = new SearchAddon();
Expand All @@ -203,7 +219,7 @@ export const useXtermPlayer = ({
// even though we set scrollback: 0 above, xterm steals the wheel events and prevents the outer container from scrolling
// this handler fixes that
const onScroll = (event: WheelEvent) => {
if ((event?.target as HTMLDivElement)?.className === 'xterm-cursor-layer') {
if ((event?.target as HTMLDivElement)?.offsetParent?.classList.contains('xterm-screen')) {
event.stopImmediatePropagation();
}
};
Expand All @@ -212,6 +228,7 @@ export const useXtermPlayer = ({

return () => {
window.removeEventListener('wheel', onScroll, true);
terminal.dispose();
};
}, [terminal, ref]);

Expand Down Expand Up @@ -241,17 +258,30 @@ export const useXtermPlayer = ({
if (line?.value !== undefined) {
terminal.write(line.value);
}

const nextLine = lines[lineNumber + index + 1];
const maxBytesExceeded = line.event.process?.io?.max_bytes_per_process_exceeded;

// if next line is start of next event
// and process has exceeded max bytes
// render msg
if (!clear && (!nextLine || nextLine.event !== line.event) && maxBytesExceeded) {
const msg = renderTruncatedMsg(tty, policiesUrl, processName);
if (msg) {
terminal.write(msg);
}
}
});
},
[lines, terminal]
[lines, policiesUrl, processName, terminal, tty]
);

useEffect(() => {
const fontChanged = terminal.getOption('fontSize') !== fontSize;
const fontChanged = terminal.options.fontSize !== fontSize;
const ttyChanged = tty && (terminal.rows !== tty?.rows || terminal.cols !== tty?.columns);

if (fontChanged) {
terminal.setOption('fontSize', fontSize);
terminal.options.fontSize = fontSize;
}

if (tty?.rows && tty?.columns && ttyChanged) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
*/

import React from 'react';
import { waitFor } from '@testing-library/react';
import { waitFor, act } from '@testing-library/react';
import { sessionViewIOEventsMock } from '../../../common/mocks/responses/session_view_io_events.mock';
import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
import { TTYPlayerDeps, TTYPlayer } from '.';
import userEvent from '@testing-library/user-event';

describe('TTYPlayer component', () => {
beforeAll(() => {
Expand Down Expand Up @@ -81,5 +82,38 @@ describe('TTYPlayer component', () => {

await waitForApiCall();
});

it('renders a message warning when max_bytes exceeded', async () => {
renderResult = mockedContext.render(<TTYPlayer {...props} />);

await waitForApiCall();
await new Promise((r) => setTimeout(r, 10));

const seekToEndBtn = renderResult.getByTestId('sessionView:TTYPlayerControlsEnd');

act(() => {
userEvent.click(seekToEndBtn);
});

waitFor(() => expect(renderResult.queryAllByText('Data limit reached')).toHaveLength(1));
expect(renderResult.queryByText('[ VIEW POLICIES ]')).toBeFalsy();
});

it('renders a message warning when max_bytes exceeded with link to policies page', async () => {
renderResult = mockedContext.render(
<TTYPlayer {...props} canAccessEndpointManagement={true} />
);

await waitForApiCall();
await new Promise((r) => setTimeout(r, 10));

const seekToEndBtn = renderResult.getByTestId('sessionView:TTYPlayerControlsEnd');

act(() => {
userEvent.click(seekToEndBtn);
});

waitFor(() => expect(renderResult.queryAllByText('[ VIEW POLICIES ]')).toHaveLength(1));
});
});
});
Loading