diff --git a/package.json b/package.json index a7990332e761b..3d02d2907cd5e 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.test.tsx index 773790b4da652..506353a9b7047 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.test.tsx @@ -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, }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.tsx index 8b5add5ae73b1..0cb001b6aa3e1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.tsx @@ -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'; @@ -266,6 +267,7 @@ export const useSessionView = ({ }, [scopeId]); const { globalFullScreen } = useGlobalFullScreen(); const { timelineFullScreen } = useTimelineFullScreen(); + const { canAccessEndpointManagement } = useUserPrivileges().endpointPrivileges; const defaults = isTimelineScope(scopeId) ? timelineDefaults : tableDefaults; const { sessionViewConfig, activeTab } = useDeepEqualSelector((state) => ({ @@ -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, diff --git a/x-pack/plugins/session_view/common/constants.ts b/x-pack/plugins/session_view/common/constants.ts index e7efb0b1f11f6..9938076a6c9e1 100644 --- a/x-pack/plugins/session_view/common/constants.ts +++ b/x-pack/plugins/session_view/common/constants.ts @@ -5,6 +5,8 @@ * 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'; @@ -12,6 +14,9 @@ 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'; diff --git a/x-pack/plugins/session_view/common/mocks/responses/session_view_io_events.mock.ts b/x-pack/plugins/session_view/common/mocks/responses/session_view_io_events.mock.ts index 23cf1ede0d9a3..c8be02c00bbed 100644 --- a/x-pack/plugins/session_view/common/mocks/responses/session_view_io_events.mock.ts +++ b/x-pack/plugins/session_view/common/mocks/responses/session_view_io_events.mock.ts @@ -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 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: { diff --git a/x-pack/plugins/session_view/common/types/process_tree/index.ts b/x-pack/plugins/session_view/common/types/process_tree/index.ts index 68f8924abd702..b228502f61a54 100644 --- a/x-pack/plugins/session_view/common/types/process_tree/index.ts +++ b/x-pack/plugins/session_view/common/types/process_tree/index.ts @@ -72,6 +72,7 @@ export interface IOLine { export interface ProcessStartMarker { event: ProcessEvent; line: number; + maxBytesExceeded?: boolean; } export interface IOFields { diff --git a/x-pack/plugins/session_view/public/components/session_view/index.tsx b/x-pack/plugins/session_view/public/components/session_view/index.tsx index 32cc5dffdea5d..18296992de65f 100644 --- a/x-pack/plugins/session_view/public/components/session_view/index.tsx +++ b/x-pack/plugins/session_view/public/components/session_view/index.tsx @@ -53,6 +53,7 @@ export const SessionView = ({ jumpToCursor, investigatedAlertId, loadAlertDetails, + canAccessEndpointManagement, }: SessionViewDeps) => { // don't engage jumpTo if jumping to session leader. if (jumpToEntityId === sessionEntityId) { @@ -422,6 +423,7 @@ export const SessionView = ({ isFullscreen={isFullScreen} onJumpToEvent={onJumpToEvent} autoSeekToEntityId={currentJumpToOutputEntityId} + canAccessEndpointManagement={canAccessEndpointManagement} /> ); diff --git a/x-pack/plugins/session_view/public/components/tty_player/ansi_helpers.ts b/x-pack/plugins/session_view/public/components/tty_player/ansi_helpers.ts new file mode 100644 index 0000000000000..e5ad54f7e5269 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/tty_player/ansi_helpers.ts @@ -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`; + } +}; diff --git a/x-pack/plugins/session_view/public/components/tty_player/hooks.ts b/x-pack/plugins/session_view/public/components/tty_player/hooks.ts index b6891f1dd1d49..30b40beaa094f 100644 --- a/x-pack/plugins/session_view/public/components/tty_player/hooks.ts +++ b/x-pack/plugins/session_view/public/components/tty_player/hooks.ts @@ -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, @@ -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]]; @@ -158,6 +168,7 @@ export interface XtermPlayerDeps { hasNextPage?: boolean; fetchNextPage?: () => void; isFetching?: boolean; + policiesUrl?: string; } export const useXtermPlayer = ({ @@ -169,17 +180,20 @@ 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, @@ -187,6 +201,8 @@ export const useXtermPlayer = ({ convertEol: true, rows: DEFAULT_TTY_ROWS, cols: DEFAULT_TTY_COLS, + allowProposedApi: true, + allowTransparency: true, }); const searchInstance = new SearchAddon(); @@ -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(); } }; @@ -212,6 +228,7 @@ export const useXtermPlayer = ({ return () => { window.removeEventListener('wheel', onScroll, true); + terminal.dispose(); }; }, [terminal, ref]); @@ -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) { diff --git a/x-pack/plugins/session_view/public/components/tty_player/index.test.tsx b/x-pack/plugins/session_view/public/components/tty_player/index.test.tsx index f3332ae5bb7f8..ba56931b4a99b 100644 --- a/x-pack/plugins/session_view/public/components/tty_player/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/tty_player/index.test.tsx @@ -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(() => { @@ -81,5 +82,38 @@ describe('TTYPlayer component', () => { await waitForApiCall(); }); + + it('renders a message warning when max_bytes exceeded', async () => { + renderResult = mockedContext.render(); + + 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( + + ); + + 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)); + }); }); }); diff --git a/x-pack/plugins/session_view/public/components/tty_player/index.tsx b/x-pack/plugins/session_view/public/components/tty_player/index.tsx index c77efc9d8c152..434805ac689db 100644 --- a/x-pack/plugins/session_view/public/components/tty_player/index.tsx +++ b/x-pack/plugins/session_view/public/components/tty_player/index.tsx @@ -13,6 +13,8 @@ import { EuiButton, EuiBetaBadge, } from '@elastic/eui'; +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'; @@ -23,6 +25,8 @@ import { DEFAULT_TTY_ROWS, DEFAULT_TTY_COLS, DEFAULT_TTY_FONT_SIZE, + POLICIES_PAGE_PATH, + SECURITY_APP_ID, } from '../../../common/constants'; import { useFetchIOEvents, useIOLines, useXtermPlayer } from './hooks'; import { TTYPlayerControls } from '../tty_player_controls'; @@ -35,6 +39,7 @@ export interface TTYPlayerDeps { isFullscreen: boolean; onJumpToEvent(event: ProcessEvent): void; autoSeekToEntityId?: string; + canAccessEndpointManagement?: boolean; } export const TTYPlayer = ({ @@ -44,6 +49,7 @@ export const TTYPlayer = ({ isFullscreen, onJumpToEvent, autoSeekToEntityId, + canAccessEndpointManagement, }: TTYPlayerDeps) => { const ref = useRef(null); const { ref: scrollRef, height: containerHeight = 1 } = useResizeObserver({}); @@ -53,7 +59,16 @@ export const TTYPlayer = ({ const { lines, processStartMarkers } = useIOLines(data?.pages); const [fontSize, setFontSize] = useState(DEFAULT_TTY_FONT_SIZE); const [isPlaying, setIsPlaying] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); const [currentAutoSeekEntityId, setCurrentAutoSeekEntityId] = useState(''); + const { getUrlForApp } = useKibana().services.application; + const policiesUrl = useMemo( + () => + canAccessEndpointManagement + ? getUrlForApp(SECURITY_APP_ID, { path: POLICIES_PAGE_PATH }) + : '', + [canAccessEndpointManagement, getUrlForApp] + ); const { search, currentLine, seekToLine } = useXtermPlayer({ ref, @@ -64,6 +79,7 @@ export const TTYPlayer = ({ hasNextPage, fetchNextPage, isFetching, + policiesUrl, }); const currentProcessEvent = lines[Math.min(lines.length - 1, currentLine)]?.event; @@ -113,11 +129,18 @@ export const TTYPlayer = ({ const styles = useStyles(tty, show); + const clearSearch = useCallback(() => { + if (searchQuery) { + setSearchQuery(''); + } + }, [searchQuery]); + const onSeekLine = useMemo(() => { return throttle((line: number) => { + clearSearch(); seekToLine(line); }, 100); - }, [seekToLine]); + }, [clearSearch, seekToLine]); const onTogglePlayback = useCallback(() => { // if at the end, seek to beginning @@ -127,6 +150,12 @@ export const TTYPlayer = ({ setIsPlaying(!isPlaying); }, [currentLine, isPlaying, lines.length, seekToLine]); + useEffect(() => { + if (isPlaying) { + clearSearch(); + } + }, [clearSearch, isPlaying]); + return (
@@ -140,6 +169,8 @@ export const TTYPlayer = ({ seekToLine={seekToLine} xTermSearchFn={search} setIsPlaying={setIsPlaying} + searchQuery={searchQuery} + setSearchQuery={setSearchQuery} /> @@ -157,11 +188,17 @@ export const TTYPlayer = ({ - + - + diff --git a/x-pack/plugins/session_view/public/components/tty_player/translations.ts b/x-pack/plugins/session_view/public/components/tty_player/translations.ts new file mode 100644 index 0000000000000..8d1dfd7497410 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/tty_player/translations.ts @@ -0,0 +1,25 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const PROCESS_DATA_LIMIT_EXCEEDED_START = i18n.translate( + 'xpack.sessionView.processDataLimitExceededStart', + { + defaultMessage: 'Data limit reached for', + } +); + +export const PROCESS_DATA_LIMIT_EXCEEDED_END = i18n.translate( + 'xpack.sessionView.processDataLimitExceededEnd', + { + defaultMessage: 'See "max_kilobytes_per_process" in advanced policy configuration.', + } +); + +export const VIEW_POLICIES = i18n.translate('xpack.sessionView.viewPoliciesLink', { + defaultMessage: 'VIEW POLICIES', +}); diff --git a/x-pack/plugins/session_view/public/components/tty_player/xterm_search.ts b/x-pack/plugins/session_view/public/components/tty_player/xterm_search.ts index 3c430d691e3f7..fee2aff4b5458 100644 --- a/x-pack/plugins/session_view/public/components/tty_player/xterm_search.ts +++ b/x-pack/plugins/session_view/public/components/tty_player/xterm_search.ts @@ -9,7 +9,7 @@ * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ -import { Terminal, IDisposable, ITerminalAddon, ISelectionPosition } from 'xterm'; +import { Terminal, IDisposable, ITerminalAddon, IBufferRange } from 'xterm'; export interface ISearchOptions { regex?: boolean; @@ -83,14 +83,14 @@ export class SearchAddon implements ITerminalAddon { let startCol = 0; let startRow = 0; - let currentSelection: ISelectionPosition | undefined; + let currentSelection: IBufferRange | undefined; if (this._terminal.hasSelection()) { const incremental = searchOptions ? searchOptions.incremental : false; // Start from the selection end if there is a selection // For incremental search, use existing row currentSelection = this._terminal.getSelectionPosition()!; - startRow = incremental ? currentSelection.startRow : currentSelection.endRow; - startCol = incremental ? currentSelection.startColumn : currentSelection.endColumn; + startRow = incremental ? currentSelection.start.y : currentSelection.end.y; + startCol = incremental ? currentSelection.start.x : currentSelection.end.x; } if (searchOptions?.lastLineOnly) { @@ -139,7 +139,7 @@ export class SearchAddon implements ITerminalAddon { // If there is only one result, wrap back and return selection if it exists. if (!result && currentSelection) { - searchPosition.startRow = currentSelection.startRow; + searchPosition.startRow = currentSelection.start.y; searchPosition.startCol = 0; result = this._findInLine(term, searchPosition, searchOptions); } @@ -170,12 +170,12 @@ export class SearchAddon implements ITerminalAddon { let startCol = this._terminal.cols; let result: ISearchResult | undefined; const incremental = searchOptions ? searchOptions.incremental : false; - let currentSelection: ISelectionPosition | undefined; + let currentSelection: IBufferRange | undefined; if (this._terminal.hasSelection()) { currentSelection = this._terminal.getSelectionPosition()!; // Start from selection start if there is a selection - startRow = currentSelection.startRow; - startCol = currentSelection.startColumn; + startRow = currentSelection.start.y; + startCol = currentSelection.start.x; } else if (searchOptions?.lastLineOnly) { startRow = this._terminal.buffer.active.cursorY - 1; startCol = this._terminal.cols; @@ -194,8 +194,8 @@ export class SearchAddon implements ITerminalAddon { if (!isOldResultHighlighted) { // If selection was not able to be expanded to the right, then try reverse search if (currentSelection) { - searchPosition.startRow = currentSelection.endRow; - searchPosition.startCol = currentSelection.endColumn; + searchPosition.startRow = currentSelection.end.y; + searchPosition.startCol = currentSelection.end.x; } result = this._findInLine(term, searchPosition, searchOptions, true); } diff --git a/x-pack/plugins/session_view/public/components/tty_player_controls/index.test.tsx b/x-pack/plugins/session_view/public/components/tty_player_controls/index.test.tsx index 8a536e1ae0228..61a3958c1b88d 100644 --- a/x-pack/plugins/session_view/public/components/tty_player_controls/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/tty_player_controls/index.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; import { ProcessEvent } from '../../../common/types/process_tree'; import { TTYPlayerControls, TTYPlayerControlsDeps } from '.'; +import { TTYPlayerLineMarkerType } from './tty_player_controls_markers'; const MOCK_PROCESS_EVENT_START: ProcessEvent = { process: { @@ -100,11 +101,11 @@ describe('TTYPlayerControls component', () => { expect(props.onSeekLine).toHaveBeenCalledWith(9); }); - it('render output markers', async () => { + it('render process_changed markers', async () => { renderResult = mockedContext.render(); expect( renderResult.queryAllByRole('button', { - name: 'output', + name: TTYPlayerLineMarkerType.ProcessChanged, }) ).toHaveLength(props.processStartMarkers.length); }); @@ -115,12 +116,10 @@ describe('TTYPlayerControls component', () => { event: { process: { ...MOCK_PROCESS_EVENT_MIDDLE, - io: { - max_bytes_per_process_exceeded: true, - }, }, }, line: 2, + maxBytesExceeded: true, }, { event: MOCK_PROCESS_EVENT_END, line: 4 }, ]; @@ -129,12 +128,12 @@ describe('TTYPlayerControls component', () => { ); expect( renderResult.queryAllByRole('button', { - name: 'output', + name: TTYPlayerLineMarkerType.ProcessChanged, }) ).toHaveLength(2); expect( renderResult.queryAllByRole('button', { - name: 'data_limited', + name: TTYPlayerLineMarkerType.ProcessDataLimitReached, }) ).toHaveLength(1); }); diff --git a/x-pack/plugins/session_view/public/components/tty_player_controls/tty_player_controls_markers/index.tsx b/x-pack/plugins/session_view/public/components/tty_player_controls/tty_player_controls_markers/index.tsx index b10ba00e1fc2a..e84ed7fcf34a9 100644 --- a/x-pack/plugins/session_view/public/components/tty_player_controls/tty_player_controls_markers/index.tsx +++ b/x-pack/plugins/session_view/public/components/tty_player_controls/tty_player_controls_markers/index.tsx @@ -19,9 +19,14 @@ type Props = { onSeekLine(line: number): void; }; +export enum TTYPlayerLineMarkerType { + ProcessChanged = 'process_changed', + ProcessDataLimitReached = 'data_limited', +} + type TTYPlayerLineMarker = { line: number; - type: 'output' | 'data_limited'; + type: TTYPlayerLineMarkerType; name: string; }; @@ -44,10 +49,11 @@ export const TTYPlayerControlsMarkers = ({ return []; } return processStartMarkers.map( - ({ event, line }) => + ({ event, line, maxBytesExceeded }) => ({ - type: - event.process?.io?.max_bytes_per_process_exceeded === true ? 'data_limited' : 'output', + type: maxBytesExceeded + ? TTYPlayerLineMarkerType.ProcessDataLimitReached + : TTYPlayerLineMarkerType.ProcessChanged, line, name: event.process?.name, } as TTYPlayerLineMarker) diff --git a/x-pack/plugins/session_view/public/components/tty_player_controls/tty_player_controls_markers/styles.ts b/x-pack/plugins/session_view/public/components/tty_player_controls/tty_player_controls_markers/styles.ts index f48cfd3795eb0..48c7c67128c64 100644 --- a/x-pack/plugins/session_view/public/components/tty_player_controls/tty_player_controls_markers/styles.ts +++ b/x-pack/plugins/session_view/public/components/tty_player_controls/tty_player_controls_markers/styles.ts @@ -9,7 +9,7 @@ import { useMemo } from 'react'; import { CSSObject } from '@emotion/react'; import { useEuiTheme } from '../../../hooks'; -type TTYPlayerLineMarkerType = 'output' | 'data_limited'; +import { TTYPlayerLineMarkerType } from '.'; export const useStyles = (progress: number) => { const { euiTheme, euiVars } = useEuiTheme(); @@ -30,7 +30,7 @@ export const useStyles = (progress: number) => { }; const getMarkerBackgroundColor = (type: TTYPlayerLineMarkerType, selected: boolean) => { - if (type === 'data_limited') { + if (type === TTYPlayerLineMarkerType.ProcessDataLimitReached) { return euiVars.terminalOutputMarkerWarning; } if (selected) { @@ -105,7 +105,7 @@ export const useStyles = (progress: number) => { left: progress + '%', top: 16, fill: - type === 'data_limited' + type === TTYPlayerLineMarkerType.ProcessDataLimitReached ? euiVars.terminalOutputMarkerWarning : euiVars.terminalOutputMarkerAccent, }); diff --git a/x-pack/plugins/session_view/public/components/tty_search_bar/index.test.tsx b/x-pack/plugins/session_view/public/components/tty_search_bar/index.test.tsx index 06fa17a6c151c..7b8adbc47d52d 100644 --- a/x-pack/plugins/session_view/public/components/tty_search_bar/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/tty_search_bar/index.test.tsx @@ -7,7 +7,6 @@ import React from 'react'; import { renderHook } from '@testing-library/react-hooks'; import userEvent from '@testing-library/user-event'; -import { fireEvent } from '@testing-library/dom'; import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; import { sessionViewIOEventsMock } from '../../../common/mocks/responses/session_view_io_events.mock'; import { useIOLines } from '../tty_player/hooks'; @@ -35,6 +34,8 @@ describe('TTYSearchBar component', () => { seekToLine: jest.fn(), xTermSearchFn: jest.fn(), setIsPlaying: jest.fn(), + searchQuery: '', + setSearchQuery: jest.fn(), }; }); @@ -44,33 +45,20 @@ describe('TTYSearchBar component', () => { }); it('does a search when a user enters text and hits enter', async () => { - renderResult = mockedContext.render(); - - const searchInput = renderResult.queryByTestId('sessionView:searchBar')?.querySelector('input'); - if (searchInput) { - userEvent.type(searchInput, '-h'); - fireEvent.keyUp(searchInput, { key: 'Enter', code: 'Enter' }); - } + renderResult = mockedContext.render(); expect(props.seekToLine).toHaveBeenCalledTimes(1); // there is a slight delay in the seek in xtermjs, so we wait 100ms before trying to highlight a result. await new Promise((r) => setTimeout(r, 100)); - expect(props.xTermSearchFn).toHaveBeenCalledTimes(2); - expect(props.xTermSearchFn).toHaveBeenNthCalledWith(1, '', 0); - expect(props.xTermSearchFn).toHaveBeenNthCalledWith(2, '-h', 6); + expect(props.xTermSearchFn).toHaveBeenCalledTimes(1); + expect(props.xTermSearchFn).toHaveBeenNthCalledWith(1, '-h', 6); expect(props.setIsPlaying).toHaveBeenCalledWith(false); }); it('calls seekToline and xTermSearchFn when currentMatch changes', async () => { - renderResult = mockedContext.render(); - - const searchInput = renderResult.queryByTestId('sessionView:searchBar')?.querySelector('input'); - if (searchInput) { - userEvent.type(searchInput, '-h'); - fireEvent.keyUp(searchInput, { key: 'Enter', code: 'Enter' }); - } + renderResult = mockedContext.render(); await new Promise((r) => setTimeout(r, 100)); @@ -83,27 +71,23 @@ describe('TTYSearchBar component', () => { expect(props.seekToLine).toHaveBeenNthCalledWith(1, 26); expect(props.seekToLine).toHaveBeenNthCalledWith(2, 100); - expect(props.xTermSearchFn).toHaveBeenCalledTimes(3); - expect(props.xTermSearchFn).toHaveBeenNthCalledWith(1, '', 0); - expect(props.xTermSearchFn).toHaveBeenNthCalledWith(2, '-h', 6); - expect(props.xTermSearchFn).toHaveBeenNthCalledWith(3, '-h', 13); - expect(props.setIsPlaying).toHaveBeenCalledTimes(3); + expect(props.xTermSearchFn).toHaveBeenCalledTimes(2); + expect(props.xTermSearchFn).toHaveBeenNthCalledWith(1, '-h', 6); + expect(props.xTermSearchFn).toHaveBeenNthCalledWith(2, '-h', 13); + expect(props.setIsPlaying).toHaveBeenCalledTimes(2); }); it('calls xTermSearchFn with empty query when search is cleared', async () => { - renderResult = mockedContext.render(); - - const searchInput = renderResult.queryByTestId('sessionView:searchBar')?.querySelector('input'); - if (searchInput) { - userEvent.type(searchInput, '-h'); - fireEvent.keyUp(searchInput, { key: 'Enter', code: 'Enter' }); - } + renderResult = mockedContext.render(); await new Promise((r) => setTimeout(r, 100)); userEvent.click(renderResult.getByTestId('clearSearchButton')); await new Promise((r) => setTimeout(r, 100)); - expect(props.xTermSearchFn).toHaveBeenNthCalledWith(3, '', 0); + renderResult.rerender(); + + expect(props.setSearchQuery).toHaveBeenNthCalledWith(1, ''); + expect(props.xTermSearchFn).toHaveBeenNthCalledWith(2, '', 0); expect(props.setIsPlaying).toHaveBeenCalledWith(false); }); }); diff --git a/x-pack/plugins/session_view/public/components/tty_search_bar/index.tsx b/x-pack/plugins/session_view/public/components/tty_search_bar/index.tsx index 18b829127ab2d..47d166167bb2a 100644 --- a/x-pack/plugins/session_view/public/components/tty_search_bar/index.tsx +++ b/x-pack/plugins/session_view/public/components/tty_search_bar/index.tsx @@ -20,6 +20,8 @@ export interface TTYSearchBarDeps { seekToLine(index: number): void; xTermSearchFn(query: string, index: number): void; setIsPlaying(value: boolean): void; + searchQuery: string; + setSearchQuery(value: string): void; } const STRIP_NEWLINES_REGEX = /^(\r\n|\r|\n|\n\r)/; @@ -29,9 +31,10 @@ export const TTYSearchBar = ({ seekToLine, xTermSearchFn, setIsPlaying, + searchQuery, + setSearchQuery, }: TTYSearchBarDeps) => { const [currentMatch, setCurrentMatch] = useState(null); - const [searchQuery, setSearchQuery] = useState(''); const jumpToMatch = useCallback( (match) => { @@ -105,7 +108,7 @@ export const TTYSearchBar = ({ setSearchQuery(query); setCurrentMatch(null); }, - [setIsPlaying] + [setIsPlaying, setSearchQuery] ); const onSetCurrentMatch = useCallback( diff --git a/x-pack/plugins/session_view/public/test/index.tsx b/x-pack/plugins/session_view/public/test/index.tsx index 244560d366ac4..6ccc8ca2b1991 100644 --- a/x-pack/plugins/session_view/public/test/index.tsx +++ b/x-pack/plugins/session_view/public/test/index.tsx @@ -17,6 +17,7 @@ import { CoreStart } from '@kbn/core/public'; import { coreMock } from '@kbn/core/public/mocks'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; +import { SECURITY_APP_ID, SESSION_VIEW_APP_ID } from '../../common/constants'; type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult; @@ -46,8 +47,10 @@ const createCoreStartMock = ( // Mock the certain APP Ids returned by `application.getUrlForApp()` coreStart.application.getUrlForApp.mockImplementation((appId) => { switch (appId) { - case 'sessionView': + case SESSION_VIEW_APP_ID: return '/app/sessionView'; + case SECURITY_APP_ID: + return '/app/security'; default: return `${appId} not mocked!`; } diff --git a/x-pack/plugins/session_view/public/types.ts b/x-pack/plugins/session_view/public/types.ts index d276f0e9518a9..fa5f9d1ebb04d 100644 --- a/x-pack/plugins/session_view/public/types.ts +++ b/x-pack/plugins/session_view/public/types.ts @@ -27,6 +27,7 @@ export interface SessionViewDeps { // Callback used when alert flyout panel is closed handleOnAlertDetailsClosed: () => void ) => void; + canAccessEndpointManagement?: boolean; } export interface EuiTabProps { diff --git a/yarn.lock b/yarn.lock index 598a7fc608340..544049e10e0bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29229,10 +29229,10 @@ xtend@~2.1.1: dependencies: object-keys "~0.4.0" -xterm@^4.18.0: - version "4.18.0" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.18.0.tgz#a1f6ab2c330c3918fb094ae5f4c2562987398ea1" - integrity sha512-JQoc1S0dti6SQfI0bK1AZvGnAxH4MVw45ZPFSO6FHTInAiau3Ix77fSxNx3mX4eh9OL4AYa8+4C8f5UvnSfppQ== +xterm@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.0.0.tgz#0af50509b33d0dc62fde7a4ec17750b8e453cc5c" + integrity sha512-tmVsKzZovAYNDIaUinfz+VDclraQpPUnAME+JawosgWRMphInDded/PuY0xmU5dOhyeYZsI0nz5yd8dPYsdLTA== y18n@^3.2.0: version "3.2.2"