From e7aabcdfaeec769991b0a38f2bd97d6287c22876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20=C3=81brah=C3=A1m?= Date: Mon, 19 Aug 2024 16:05:13 +0200 Subject: [PATCH 01/16] [EDR Workflows] Improve event filters related cy tests (#190610) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary reducing potential flakyness in cypress tests handling event filters, by applying same change as #189961: enter text instead of selecting from dropdown > [!note] > revert da247c571d7c5741c618501b0614facd939e8323 before merge - done ✅ flaky runner: https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/6782 ✅ --- .../cypress/fixtures/artifacts_page.ts | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/cypress/fixtures/artifacts_page.ts b/x-pack/plugins/security_solution/public/management/cypress/fixtures/artifacts_page.ts index 47f09f65dd0946..97a6807a59f0af 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/fixtures/artifacts_page.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/fixtures/artifacts_page.ts @@ -188,14 +188,11 @@ export const getArtifactsListTestsData = (): ArtifactsFixtureType[] => [ selector: 'eventFilters-form-description-input', value: 'This is the event filter description', }, - { - type: 'click', - selector: 'fieldAutocompleteComboBox', - }, + { type: 'input', - customSelector: '[data-test-subj="fieldAutocompleteComboBox"] input', - value: '@timestamp{downArrow}{enter}', + selector: 'fieldAutocompleteComboBox', + value: '@timestamp', }, { type: 'click', @@ -239,12 +236,9 @@ export const getArtifactsListTestsData = (): ArtifactsFixtureType[] => [ value: 'This is the event filter description edited', }, { - type: 'click', + type: 'input', selector: 'fieldAutocompleteComboBox', - }, - { - type: 'click', - customSelector: 'button[title="agent.name"]', + value: '{selectAll}agent.name', }, { type: 'input', From cf58ef9e516bc3146adee23fedd193364c3255f0 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Mon, 19 Aug 2024 16:06:54 +0200 Subject: [PATCH 02/16] [OneDiscover][UnifiedDocViewer] Add dedicated column for Pinning/Unpinning rows (#190344) - Closes https://github.com/elastic/kibana/issues/188413 ## Summary This PR adds a dedicated column for pinning/unpinning fields inside DocViewer. ![Aug-13-2024 15-06-25](https://github.com/user-attachments/assets/93496cdd-e730-4ee6-8597-c78d7bffe07f) ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [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 - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --- .../src/components/field_name/field_name.tsx | 22 +-- .../table_cell_actions.test.tsx.snap | 128 +----------------- .../doc_viewer_table/get_pin_control.test.tsx | 63 +++++++++ .../doc_viewer_table/get_pin_control.tsx | 86 ++++++++++++ .../components/doc_viewer_table/table.scss | 20 ++- .../components/doc_viewer_table/table.tsx | 6 + .../doc_viewer_table/table_cell.tsx | 3 +- .../doc_viewer_table/table_cell_actions.tsx | 35 ----- .../apps/discover/group3/_doc_viewer.ts | 42 +++++- test/functional/services/data_grid.ts | 18 +++ .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 13 files changed, 236 insertions(+), 190 deletions(-) create mode 100644 src/plugins/unified_doc_viewer/public/components/doc_viewer_table/get_pin_control.test.tsx create mode 100644 src/plugins/unified_doc_viewer/public/components/doc_viewer_table/get_pin_control.tsx diff --git a/packages/kbn-unified-doc-viewer/src/components/field_name/field_name.tsx b/packages/kbn-unified-doc-viewer/src/components/field_name/field_name.tsx index d7f380195947a3..4c57fa6cc0dc9e 100644 --- a/packages/kbn-unified-doc-viewer/src/components/field_name/field_name.tsx +++ b/packages/kbn-unified-doc-viewer/src/components/field_name/field_name.tsx @@ -8,14 +8,7 @@ import React from 'react'; import './field_name.scss'; -import { - EuiBadge, - EuiFlexGroup, - EuiFlexItem, - EuiToolTip, - EuiHighlight, - EuiIcon, -} from '@elastic/eui'; +import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiToolTip, EuiHighlight } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { FieldIcon, FieldIconProps } from '@kbn/react-field'; @@ -30,7 +23,6 @@ interface Props { fieldIconProps?: Omit; scripted?: boolean; highlight?: string; - isPinned?: boolean; } export function FieldName({ @@ -40,7 +32,6 @@ export function FieldName({ fieldIconProps, scripted = false, highlight = '', - isPinned = false, }: Props) { const typeName = getFieldTypeName(fieldType); const displayName = @@ -63,17 +54,6 @@ export function FieldName({ - {isPinned && ( - - - - )} diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/__snapshots__/table_cell_actions.test.tsx.snap b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/__snapshots__/table_cell_actions.test.tsx.snap index bbc8ee91569ad7..aecef4797f9bdf 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/__snapshots__/table_cell_actions.test.tsx.snap +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/__snapshots__/table_cell_actions.test.tsx.snap @@ -43,95 +43,10 @@ Array [ } } />, - , ] `; -exports[`TableActions getFieldCellActions should render correctly for undefined functions 2`] = ` -Array [ - , -] -`; +exports[`TableActions getFieldCellActions should render correctly for undefined functions 2`] = `Array []`; exports[`TableActions getFieldCellActions should render the panels correctly for defined onFilter function 1`] = ` Array [ @@ -217,47 +132,6 @@ Array [ } } />, - , ] `; diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/get_pin_control.test.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/get_pin_control.test.tsx new file mode 100644 index 00000000000000..74282a52b86c41 --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/get_pin_control.test.tsx @@ -0,0 +1,63 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { DataViewField } from '@kbn/data-views-plugin/common'; +import { TableRow } from './table_cell_actions'; +import { getPinColumnControl } from './get_pin_control'; +import { EuiDataGridCellValueElementProps } from '@elastic/eui/src/components/datagrid/data_grid_types'; + +describe('getPinControl', () => { + const rows: TableRow[] = [ + { + action: { + onFilter: jest.fn(), + flattenedField: 'flattenedField', + onToggleColumn: jest.fn(), + }, + field: { + pinned: true, + onTogglePinned: jest.fn(), + field: 'message', + fieldMapping: new DataViewField({ + type: 'keyword', + name: 'message', + searchable: true, + aggregatable: true, + }), + fieldType: 'keyword', + displayName: 'message', + scripted: false, + }, + value: { + ignored: undefined, + formattedValue: 'test', + }, + }, + ]; + + it('should render correctly', () => { + const control = getPinColumnControl({ rows }); + const Cell = control.rowCellRender as React.FC; + render( + + ); + + screen.getByTestId('unifiedDocViewer_pinControlButton_message').click(); + + expect(rows[0].field.onTogglePinned).toHaveBeenCalledWith('message'); + }); +}); diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/get_pin_control.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/get_pin_control.tsx new file mode 100644 index 00000000000000..0a2c45611dcdfa --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/get_pin_control.tsx @@ -0,0 +1,86 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { + EuiButtonIcon, + EuiDataGridControlColumn, + EuiScreenReaderOnly, + EuiToolTip, + useEuiTheme, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; +import type { TableRow } from './table_cell_actions'; + +interface PinControlCellProps { + row: TableRow; +} + +const PinControlCell: React.FC = React.memo(({ row }) => { + const { euiTheme } = useEuiTheme(); + + const fieldName = row.field.field; + const isPinned = row.field.pinned; + const label = isPinned + ? i18n.translate('unifiedDocViewer.docViews.table.unpinFieldLabel', { + defaultMessage: 'Unpin field', + }) + : i18n.translate('unifiedDocViewer.docViews.table.pinFieldLabel', { + defaultMessage: 'Pin field', + }); + + return ( +
+ + { + row.field.onTogglePinned(fieldName); + }} + /> + +
+ ); +}); + +export const getPinColumnControl = ({ rows }: { rows: TableRow[] }): EuiDataGridControlColumn => { + return { + id: 'pin_field', + width: 32, + headerCellRender: () => ( + + + {i18n.translate('unifiedDocViewer.fieldsTable.pinControlColumnHeader', { + defaultMessage: 'Pin field column', + })} + + + ), + rowCellRender: ({ rowIndex }) => { + const row = rows[rowIndex]; + if (!row) { + return null; + } + return ; + }, + }; +}; diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.scss b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.scss index 91022cc47faf82..330cf364ae55e5 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.scss +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.scss @@ -80,8 +80,26 @@ background-color: tintOrShade($euiColorLightShade, 50%, 0); } - & .euiDataGridRowCell--firstColumn .euiDataGridRowCell__content { + & [data-gridcell-column-id='name'] .euiDataGridRowCell__content { padding-top: 0; padding-bottom: 0; } + + & [data-gridcell-column-id='pin_field'] .euiDataGridRowCell__content { + padding: $euiSizeXS / 2 0 0 $euiSizeXS; + } + + .kbnDocViewer__fieldsGrid__pinAction { + opacity: 0; + } + + & [data-gridcell-column-id='pin_field']:focus-within { + .kbnDocViewer__fieldsGrid__pinAction { + opacity: 1; + } + } + + .euiDataGridRow:hover .kbnDocViewer__fieldsGrid__pinAction { + opacity: 1; + } } diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.tsx index 64659877910a70..008149966c49d7 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.tsx @@ -53,6 +53,7 @@ import { } from '../doc_viewer_source/get_height'; import { TableFilters, TableFiltersProps, useTableFilters } from './table_filters'; import { TableCell } from './table_cell'; +import { getPinColumnControl } from './get_pin_control'; export type FieldRecord = TableRow; @@ -295,6 +296,10 @@ export const DocViewerTable = ({ const rows = useMemo(() => [...pinnedItems, ...restItems], [pinnedItems, restItems]); + const leadingControlColumns = useMemo(() => { + return [getPinColumnControl({ rows })]; + }, [rows]); + const { curPageIndex, pageSize, totalPages, changePageIndex, changePageSize } = usePager({ initialPageSize: getPageSize(storage), totalItems: rows.length, @@ -492,6 +497,7 @@ export const DocViewerTable = ({ renderCellValue={renderCellValue} renderCellPopover={renderCellPopover} pagination={pagination} + leadingControlColumns={leadingControlColumns} /> diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell.tsx index 094050f2c3b493..ff1027a848cf59 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell.tsx @@ -33,7 +33,7 @@ export const TableCell: React.FC = React.memo( const { action: { flattenedField }, - field: { field, fieldMapping, fieldType, scripted, pinned }, + field: { field, fieldMapping, fieldType, scripted }, value: { formattedValue, ignored }, } = row; @@ -49,7 +49,6 @@ export const TableCell: React.FC = React.memo( fieldMapping?.displayName ?? field, searchTerm )} - isPinned={pinned} /> {isDetails && !!fieldMapping ? ( diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.tsx index 7814405e092018..b5e27837f44ef4 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.tsx @@ -202,38 +202,6 @@ export const FilterExist: React.FC = ({ Component, row }) => ); }; -export const PinToggle: React.FC = ({ Component, row }) => { - if (!row) { - return null; - } - - const { - field: { field, pinned, onTogglePinned }, - } = row; - - // Pinned - const pinnedLabel = pinned - ? i18n.translate('unifiedDocViewer.docViews.table.unpinFieldLabel', { - defaultMessage: 'Unpin field', - }) - : i18n.translate('unifiedDocViewer.docViews.table.pinFieldLabel', { - defaultMessage: 'Pin field', - }); - const pinnedIconType = pinned ? 'pinFilled' : 'pin'; - - return ( - onTogglePinned(field)} - > - {pinnedLabel} - - ); -}; - export const ToggleColumn: React.FC = ({ Component, row }) => { if (!row) { return null; @@ -293,9 +261,6 @@ export function getFieldCellActions({ }, ] : []), - ({ Component, rowIndex }: EuiDataGridColumnCellActionProps) => { - return ; - }, ]; } diff --git a/test/functional/apps/discover/group3/_doc_viewer.ts b/test/functional/apps/discover/group3/_doc_viewer.ts index 66f1f74a4ddbec..0464f2e4f32d50 100644 --- a/test/functional/apps/discover/group3/_doc_viewer.ts +++ b/test/functional/apps/discover/group3/_doc_viewer.ts @@ -24,10 +24,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const dataGrid = getService('dataGrid'); const monacoEditor = getService('monacoEditor'); + const browser = getService('browser'); describe('discover doc viewer', function describeIndexTests() { before(async function () { await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await browser.setWindowSize(1600, 1200); }); beforeEach(async () => { @@ -174,7 +176,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(initialFieldsCount).to.above(numberFieldsCount); const pinnedFieldsCount = 1; - await dataGrid.clickFieldActionInFlyout('agent', 'togglePinFilterButton'); + await dataGrid.togglePinActionInFlyout('agent'); await PageObjects.discover.openFilterByFieldTypeInDocViewer(); expect(await find.allByCssSelector('[data-test-subj*="typeFilter"]')).to.have.length(6); @@ -229,5 +231,43 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); }); + + describe('pinning fields', function () { + it('should be able to pin and unpin fields', async function () { + await dataGrid.clickRowToggle(); + await PageObjects.discover.isShowingDocViewer(); + await retry.waitFor('rendered items', async () => { + return (await find.allByCssSelector('.kbnDocViewer__fieldName')).length > 0; + }); + + let fieldNameCells = await find.allByCssSelector('.kbnDocViewer__fieldName'); + let fieldNames = await Promise.all(fieldNameCells.map((cell) => cell.getVisibleText())); + + expect(fieldNames.join(',').startsWith('_id,_ignored,_index,_score,@message')).to.be(true); + expect(await dataGrid.isFieldPinnedInFlyout('agent')).to.be(false); + + await dataGrid.togglePinActionInFlyout('agent'); + + fieldNameCells = await find.allByCssSelector('.kbnDocViewer__fieldName'); + fieldNames = await Promise.all(fieldNameCells.map((cell) => cell.getVisibleText())); + expect(fieldNames.join(',').startsWith('agent,_id,_ignored')).to.be(true); + expect(await dataGrid.isFieldPinnedInFlyout('agent')).to.be(true); + + await dataGrid.togglePinActionInFlyout('@message'); + + fieldNameCells = await find.allByCssSelector('.kbnDocViewer__fieldName'); + fieldNames = await Promise.all(fieldNameCells.map((cell) => cell.getVisibleText())); + expect(fieldNames.join(',').startsWith('@message,agent,_id,_ignored')).to.be(true); + expect(await dataGrid.isFieldPinnedInFlyout('@message')).to.be(true); + + await dataGrid.togglePinActionInFlyout('@message'); + + fieldNameCells = await find.allByCssSelector('.kbnDocViewer__fieldName'); + fieldNames = await Promise.all(fieldNameCells.map((cell) => cell.getVisibleText())); + expect(fieldNames.join(',').startsWith('agent,_id,_ignored')).to.be(true); + expect(await dataGrid.isFieldPinnedInFlyout('agent')).to.be(true); + expect(await dataGrid.isFieldPinnedInFlyout('@message')).to.be(false); + }); + }); }); } diff --git a/test/functional/services/data_grid.ts b/test/functional/services/data_grid.ts index 70a67d33ffd002..fc6ed65631eaad 100644 --- a/test/functional/services/data_grid.ts +++ b/test/functional/services/data_grid.ts @@ -567,6 +567,24 @@ export class DataGridService extends FtrService { await this.testSubjects.click(`${actionName}-${fieldName}`); } + public async isFieldPinnedInFlyout(fieldName: string): Promise { + return !( + await this.testSubjects.getAttribute(`unifiedDocViewer_pinControl_${fieldName}`, 'class') + )?.includes('kbnDocViewer__fieldsGrid__pinAction'); + } + + public async togglePinActionInFlyout(fieldName: string): Promise { + await this.testSubjects.moveMouseTo(`unifiedDocViewer_pinControl_${fieldName}`); + const isPinned = await this.isFieldPinnedInFlyout(fieldName); + await this.retry.waitFor('pin action to appear', async () => { + return this.testSubjects.exists(`unifiedDocViewer_pinControlButton_${fieldName}`); + }); + await this.testSubjects.click(`unifiedDocViewer_pinControlButton_${fieldName}`); + await this.retry.waitFor('pin action to toggle', async () => { + return (await this.isFieldPinnedInFlyout(fieldName)) !== isPinned; + }); + } + public async expandFieldNameCellInFlyout(fieldName: string): Promise { const buttonSelector = 'euiDataGridCellExpandButton'; await this.testSubjects.click(`tableDocViewRow-${fieldName}-name`); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index af2ab4bc10df84..2fa4a66e8b37b7 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -7929,7 +7929,6 @@ "unifiedDocViewer.json.codeEditorAriaLabel": "Affichage JSON en lecture seule d’un document Elasticsearch", "unifiedDocViewer.json.copyToClipboardLabel": "Copier dans le presse-papiers", "unifiedDocViewer.loadingJSON": "Chargement de JSON", - "unifiedDocViewer.pinnedFieldTooltipContent": "Champ épinglé", "unifiedDocViewer.sourceViewer.errorMessage": "Impossible de récupérer les données pour le moment. Actualisez l'onglet et réessayez.", "unifiedDocViewer.sourceViewer.errorMessageTitle": "Une erreur s'est produite.", "unifiedDocViewer.sourceViewer.refresh": "Actualiser", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 602e8ea6b08ab4..c26fe66d039313 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7923,7 +7923,6 @@ "unifiedDocViewer.json.codeEditorAriaLabel": "Elasticsearch ドキュメントの JSON ビューのみを読み込む", "unifiedDocViewer.json.copyToClipboardLabel": "クリップボードにコピー", "unifiedDocViewer.loadingJSON": "JSONを読み込んでいます", - "unifiedDocViewer.pinnedFieldTooltipContent": "固定されたフィールド", "unifiedDocViewer.sourceViewer.errorMessage": "現在データを取得できませんでした。タブを更新して、再試行してください。", "unifiedDocViewer.sourceViewer.errorMessageTitle": "エラーが発生しました", "unifiedDocViewer.sourceViewer.refresh": "更新", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 21e15ecd10f537..c4f2618a499d60 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7936,7 +7936,6 @@ "unifiedDocViewer.json.codeEditorAriaLabel": "Elasticsearch 文档的只读 JSON 视图", "unifiedDocViewer.json.copyToClipboardLabel": "复制到剪贴板", "unifiedDocViewer.loadingJSON": "正在加载 JSON", - "unifiedDocViewer.pinnedFieldTooltipContent": "已固定字段", "unifiedDocViewer.sourceViewer.errorMessage": "当前无法获取数据。请刷新选项卡以重试。", "unifiedDocViewer.sourceViewer.errorMessageTitle": "发生错误", "unifiedDocViewer.sourceViewer.refresh": "刷新", From 1e64b9e4b2903e797003766b604e0f8f35c51326 Mon Sep 17 00:00:00 2001 From: Jill Guyonnet Date: Mon, 19 Aug 2024 15:22:28 +0100 Subject: [PATCH 03/16] [Fleet] RBAC - Make upgrade agent APIs space aware (#190069) ## Summary Relates to https://github.com/elastic/kibana/issues/185040 This PR makes the following Fleet agents API space aware (behind `useSpaceAwareness` feature flag): * `POST /agents/{agentId}/reassign` * `POST /agents/{agentId}/upgrade` * `POST /agents/bulk_reassign` * `POST /agents/bulk_upgrade` * `POST /agents/{agentId}/actions/{actionId}/cancel` While working on that last endpoint, I noticed and fixed an error in the documentation. ### Checklist - [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 - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed --------- Co-authored-by: Nicolas Chaulet Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine --- .../plugins/fleet/common/openapi/bundled.json | 10 +- .../plugins/fleet/common/openapi/bundled.yaml | 7 +- .../fleet/common/openapi/entrypoint.yaml | 4 +- ...=> agents@actions@{action_id}@cancel.yaml} | 5 - .../server/routes/agent/actions_handlers.ts | 10 +- .../fleet/server/routes/agent/handlers.ts | 2 +- .../fleet/server/routes/agent/index.ts | 4 +- .../server/services/agents/action_runner.ts | 14 +- .../server/services/agents/actions.test.ts | 17 +- .../fleet/server/services/agents/actions.ts | 43 +- .../fleet/server/services/agents/crud.ts | 6 +- .../server/services/agents/reassign.test.ts | 19 +- .../fleet/server/services/agents/reassign.ts | 92 ++-- .../services/agents/reassign_action_runner.ts | 12 +- .../services/agents/update_agent_tags.test.ts | 24 -- .../services/agents/update_agent_tags.ts | 19 +- .../agents/update_agent_tags_action_runner.ts | 12 +- .../server/services/agents/upgrade.test.ts | 21 +- .../fleet/server/services/agents/upgrade.ts | 18 +- .../services/agents/upgrade_action_runner.ts | 17 +- .../apis/space_awareness/actions.ts | 55 ++- .../apis/space_awareness/agents.ts | 402 ++++++++++++++++-- .../apis/space_awareness/api_helper.ts | 41 +- .../apis/space_awareness/helpers.ts | 33 +- 24 files changed, 693 insertions(+), 194 deletions(-) rename x-pack/plugins/fleet/common/openapi/paths/{agents@{agent_id}@actions@{action_id}@cancel.yaml => agents@actions@{action_id}@cancel.yaml} (87%) diff --git a/x-pack/plugins/fleet/common/openapi/bundled.json b/x-pack/plugins/fleet/common/openapi/bundled.json index af5420aaad64e8..989955b748ed55 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.json +++ b/x-pack/plugins/fleet/common/openapi/bundled.json @@ -2520,16 +2520,8 @@ } } }, - "/agents/{agentId}/actions/{actionId}/cancel": { + "/agents/actions/{actionId}/cancel": { "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "agentId", - "in": "path", - "required": true - }, { "schema": { "type": "string" diff --git a/x-pack/plugins/fleet/common/openapi/bundled.yaml b/x-pack/plugins/fleet/common/openapi/bundled.yaml index 9bb1027ef35c35..fe1c2fc68c58a7 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.yaml +++ b/x-pack/plugins/fleet/common/openapi/bundled.yaml @@ -1580,13 +1580,8 @@ paths: properties: action: $ref: '#/components/schemas/agent_action' - /agents/{agentId}/actions/{actionId}/cancel: + /agents/actions/{actionId}/cancel: parameters: - - schema: - type: string - name: agentId - in: path - required: true - schema: type: string name: actionId diff --git a/x-pack/plugins/fleet/common/openapi/entrypoint.yaml b/x-pack/plugins/fleet/common/openapi/entrypoint.yaml index 2de74e31a9a351..1ba15cb190f11e 100644 --- a/x-pack/plugins/fleet/common/openapi/entrypoint.yaml +++ b/x-pack/plugins/fleet/common/openapi/entrypoint.yaml @@ -91,8 +91,8 @@ paths: $ref: 'paths/agents@{agent_id}.yaml' '/agents/{agentId}/actions': $ref: 'paths/agents@{agent_id}@actions.yaml' - '/agents/{agentId}/actions/{actionId}/cancel': - $ref: 'paths/agents@{agent_id}@actions@{action_id}@cancel.yaml' + '/agents/actions/{actionId}/cancel': + $ref: 'paths/agents@actions@{action_id}@cancel.yaml' '/agents/files/{fileId}/{fileName}': $ref: 'paths/agents@files@{file_id}@{file_name}.yaml' '/agents/files/{fileId}': diff --git a/x-pack/plugins/fleet/common/openapi/paths/agents@{agent_id}@actions@{action_id}@cancel.yaml b/x-pack/plugins/fleet/common/openapi/paths/agents@actions@{action_id}@cancel.yaml similarity index 87% rename from x-pack/plugins/fleet/common/openapi/paths/agents@{agent_id}@actions@{action_id}@cancel.yaml rename to x-pack/plugins/fleet/common/openapi/paths/agents@actions@{action_id}@cancel.yaml index 5b939e8c5fdf40..d9ee5127e4b096 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/agents@{agent_id}@actions@{action_id}@cancel.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/agents@actions@{action_id}@cancel.yaml @@ -1,9 +1,4 @@ parameters: - - schema: - type: string - name: agentId - in: path - required: true - schema: type: string name: actionId diff --git a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts index 9b5bbdccb35408..b11dcb719e2d29 100644 --- a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts @@ -59,9 +59,15 @@ export const postCancelActionHandlerBuilder = function ( ): RequestHandler, undefined, undefined> { return async (context, request, response) => { try { - const esClient = (await context.core).elasticsearch.client.asInternalUser; + const core = await context.core; + const esClient = core.elasticsearch.client.asInternalUser; + const soClient = core.savedObjects.client; - const action = await actionsService.cancelAgentAction(esClient, request.params.actionId); + const action = await actionsService.cancelAgentAction( + esClient, + soClient, + request.params.actionId + ); const body: PostNewAgentActionResponse = { item: action, diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index 350eb24847d855..e328c738789808 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -266,7 +266,7 @@ export const putAgentsReassignHandlerDeprecated: RequestHandler< } }; -export const postAgentsReassignHandler: RequestHandler< +export const postAgentReassignHandler: RequestHandler< TypeOf, undefined, TypeOf diff --git a/x-pack/plugins/fleet/server/routes/agent/index.ts b/x-pack/plugins/fleet/server/routes/agent/index.ts index 6c55835a1ed949..7d64bf365f74b2 100644 --- a/x-pack/plugins/fleet/server/routes/agent/index.ts +++ b/x-pack/plugins/fleet/server/routes/agent/index.ts @@ -59,7 +59,7 @@ import { getAgentUploadsHandler, getAgentUploadFileHandler, deleteAgentUploadFileHandler, - postAgentsReassignHandler, + postAgentReassignHandler, postRetrieveAgentsByActionsHandler, } from './handlers'; import { @@ -271,7 +271,7 @@ export const registerAPIRoutes = (router: FleetAuthzRouter, config: FleetConfigT version: API_VERSIONS.public.v1, validate: { request: PostAgentReassignRequestSchema }, }, - postAgentsReassignHandler + postAgentReassignHandler ); router.versioned diff --git a/x-pack/plugins/fleet/server/services/agents/action_runner.ts b/x-pack/plugins/fleet/server/services/agents/action_runner.ts index 3abe4787e61328..f7eea6f3ac5606 100644 --- a/x-pack/plugins/fleet/server/services/agents/action_runner.ts +++ b/x-pack/plugins/fleet/server/services/agents/action_runner.ts @@ -16,6 +16,7 @@ import moment from 'moment'; import type { Agent } from '../../types'; import { appContextService } from '..'; import { SO_SEARCH_LIMIT } from '../../../common/constants'; +import { agentsKueryNamespaceFilter } from '../spaces/agent_namespaces'; import { getAgentActions } from './actions'; import { closePointInTime, getAgentsByKuery } from './crud'; @@ -29,6 +30,7 @@ export interface ActionParams { batchSize?: number; total?: number; actionId?: string; + spaceId?: string; // additional parameters specific to an action e.g. reassign to new policy id [key: string]: any; } @@ -195,15 +197,21 @@ export abstract class ActionRunner { appContextService.getLogger().debug('kuery: ' + this.actionParams.kuery); - const getAgents = () => - getAgentsByKuery(this.esClient, this.soClient, { - kuery: this.actionParams.kuery, + const getAgents = async () => { + const namespaceFilter = await agentsKueryNamespaceFilter(this.actionParams.spaceId); + const kuery = namespaceFilter + ? `${namespaceFilter} AND ${this.actionParams.kuery}` + : this.actionParams.kuery; + + return getAgentsByKuery(this.esClient, this.soClient, { + kuery, showInactive: this.actionParams.showInactive ?? false, page: 1, perPage, pitId, searchAfter: this.retryParams.searchAfter, }); + }; const res = await getAgents(); diff --git a/x-pack/plugins/fleet/server/services/agents/actions.test.ts b/x-pack/plugins/fleet/server/services/agents/actions.test.ts index 4a2fc9e743b2a4..b8cb2ce8c8d6ac 100644 --- a/x-pack/plugins/fleet/server/services/agents/actions.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/actions.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; +import { elasticsearchServiceMock, savedObjectsClientMock } from '@kbn/core/server/mocks'; import type { NewAgentAction, AgentActionType } from '../../../common/types'; @@ -307,16 +307,17 @@ describe('Agent actions', () => { }); describe('cancelAgentAction', () => { - it('throw if the target action is not found', async () => { + it('should throw if the target action is not found', async () => { const esClient = elasticsearchServiceMock.createInternalClient(); esClient.search.mockResolvedValue({ hits: { hits: [], }, } as any); - await expect(() => cancelAgentAction(esClient, 'i-do-not-exists')).rejects.toThrowError( - /Action not found/ - ); + const soClient = savedObjectsClientMock.create(); + await expect(() => + cancelAgentAction(esClient, soClient, 'i-do-not-exists') + ).rejects.toThrowError(/Action not found/); }); it('should create one CANCEL action for each UPGRADE action found', async () => { @@ -343,7 +344,8 @@ describe('Agent actions', () => { ], }, } as any); - await cancelAgentAction(esClient, 'action1'); + const soClient = savedObjectsClientMock.create(); + await cancelAgentAction(esClient, soClient, 'action1'); expect(esClient.create).toBeCalledTimes(2); expect(esClient.create).toBeCalledWith( @@ -382,7 +384,8 @@ describe('Agent actions', () => { ], }, } as any); - await cancelAgentAction(esClient, 'action1'); + const soClient = savedObjectsClientMock.create(); + await cancelAgentAction(esClient, soClient, 'action1'); expect(mockedBulkUpdateAgents).toBeCalled(); expect(mockedBulkUpdateAgents).toBeCalledWith( diff --git a/x-pack/plugins/fleet/server/services/agents/actions.ts b/x-pack/plugins/fleet/server/services/agents/actions.ts index f344aa24e59dd3..11c7174e09312c 100644 --- a/x-pack/plugins/fleet/server/services/agents/actions.ts +++ b/x-pack/plugins/fleet/server/services/agents/actions.ts @@ -30,6 +30,9 @@ import { auditLoggingService } from '../audit_logging'; import { getAgentIdsForAgentPolicies } from '../agent_policies/agent_policies_to_agent_ids'; +import { getCurrentNamespace } from '../spaces/get_current_namespace'; +import { addNamespaceFilteringToQuery } from '../spaces/query_namespaces_filtering'; + import { bulkUpdateAgents } from './crud'; const ONE_MONTH_IN_MS = 2592000000; @@ -305,21 +308,28 @@ export async function getUnenrollAgentActions( return result; } -export async function cancelAgentAction(esClient: ElasticsearchClient, actionId: string) { +export async function cancelAgentAction( + esClient: ElasticsearchClient, + soClient: SavedObjectsClientContract, + actionId: string +) { + const currentNameSpace = getCurrentNamespace(soClient); + const getUpgradeActions = async () => { - const res = await esClient.search({ - index: AGENT_ACTIONS_INDEX, - query: { - bool: { - filter: [ - { - term: { - action_id: actionId, - }, + const query = { + bool: { + filter: [ + { + term: { + action_id: actionId, }, - ], - }, + }, + ], }, + }; + const res = await esClient.search({ + index: AGENT_ACTIONS_INDEX, + query: await addNamespaceFilteringToQuery(query, currentNameSpace), size: SO_SEARCH_LIMIT, }); @@ -348,9 +358,12 @@ export async function cancelAgentAction(esClient: ElasticsearchClient, actionId: const cancelledActions: Array<{ agents: string[] }> = []; const createAction = async (action: FleetServerAgentAction) => { + const namespaces = currentNameSpace ? { namespaces: [currentNameSpace] } : {}; + await createAgentAction(esClient, { id: cancelActionId, type: 'CANCEL', + ...namespaces, agents: action.agents!, data: { target_id: action.action_id, @@ -505,7 +518,11 @@ export interface ActionsService { agentId: string ) => Promise; - cancelAgentAction: (esClient: ElasticsearchClient, actionId: string) => Promise; + cancelAgentAction: ( + esClient: ElasticsearchClient, + soClient: SavedObjectsClientContract, + actionId: string + ) => Promise; createAgentAction: ( esClient: ElasticsearchClient, diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index 7fdf76c76992b0..847d0dd8335c6c 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -27,9 +27,10 @@ import { FleetUnauthorizedError, } from '../../errors'; import { auditLoggingService } from '../audit_logging'; -import { isAgentInNamespace } from '../spaces/agent_namespaces'; import { getCurrentNamespace } from '../spaces/get_current_namespace'; import { isSpaceAwarenessEnabled } from '../spaces/helpers'; +import { isAgentInNamespace } from '../spaces/agent_namespaces'; +import { addNamespaceFilteringToQuery } from '../spaces/query_namespaces_filtering'; import { searchHitToAgent, agentSOAttributesToFleetServerAgentDoc } from './helpers'; import { buildAgentStatusRuntimeField } from './build_status_runtime_field'; @@ -432,6 +433,7 @@ async function _filterAgents( }> { const { page = 1, perPage = 20, sortField = 'enrolled_at', sortOrder = 'desc' } = options; const runtimeFields = await buildAgentStatusRuntimeField(soClient); + const currentNameSpace = getCurrentNamespace(soClient); let res; try { @@ -443,7 +445,7 @@ async function _filterAgents( runtime_mappings: runtimeFields, fields: Object.keys(runtimeFields), sort: [{ [sortField]: { order: sortOrder } }], - query: { bool: { filter: query } }, + query: await addNamespaceFilteringToQuery({ bool: { filter: [query] } }, currentNameSpace), index: AGENTS_INDEX, ignore_unavailable: true, }); diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.test.ts b/x-pack/plugins/fleet/server/services/agents/reassign.test.ts index 4ae78e07d3a8db..5d55fdd5da31e3 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign.test.ts @@ -15,11 +15,15 @@ import { reassignAgent, reassignAgents } from './reassign'; import { createClientMock } from './action.mock'; describe('reassignAgent', () => { + let mocks: ReturnType; + beforeEach(async () => { - const { soClient } = createClientMock(); + mocks = createClientMock(); + appContextService.start( createAppContextStartContractMock({}, false, { - withoutSpaceExtensions: soClient, + internal: mocks.soClient, + withoutSpaceExtensions: mocks.soClient, }) ); }); @@ -29,7 +33,7 @@ describe('reassignAgent', () => { }); describe('reassignAgent (singular)', () => { it('can reassign from regular agent policy to regular', async () => { - const { soClient, esClient, agentInRegularDoc, regularAgentPolicySO } = createClientMock(); + const { soClient, esClient, agentInRegularDoc, regularAgentPolicySO } = mocks; await reassignAgent(soClient, esClient, agentInRegularDoc._id, regularAgentPolicySO.id); // calls ES update with correct values @@ -43,7 +47,7 @@ describe('reassignAgent', () => { }); it('cannot reassign from regular agent policy to hosted', async () => { - const { soClient, esClient, agentInRegularDoc, hostedAgentPolicySO } = createClientMock(); + const { soClient, esClient, agentInRegularDoc, hostedAgentPolicySO } = mocks; await expect( reassignAgent(soClient, esClient, agentInRegularDoc._id, hostedAgentPolicySO.id) ).rejects.toThrowError(HostedAgentPolicyRestrictionRelatedError); @@ -54,7 +58,7 @@ describe('reassignAgent', () => { it('cannot reassign from hosted agent policy', async () => { const { soClient, esClient, agentInHostedDoc, hostedAgentPolicySO, regularAgentPolicySO } = - createClientMock(); + mocks; await expect( reassignAgent(soClient, esClient, agentInHostedDoc._id, regularAgentPolicySO.id) ).rejects.toThrowError(HostedAgentPolicyRestrictionRelatedError); @@ -78,7 +82,7 @@ describe('reassignAgent', () => { agentInHostedDoc, agentInHostedDoc2, regularAgentPolicySO2, - } = createClientMock(); + } = mocks; esClient.search.mockResponse({ hits: { @@ -116,7 +120,8 @@ describe('reassignAgent', () => { }); it('should report errors from ES agent update call', async () => { - const { soClient, esClient, agentInRegularDoc, regularAgentPolicySO2 } = createClientMock(); + const { soClient, esClient, agentInRegularDoc, regularAgentPolicySO2 } = mocks; + esClient.bulk.mockResponse({ items: [ { diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.ts b/x-pack/plugins/fleet/server/services/agents/reassign.ts index 0a5c6f9b51ee00..d5a4d2ab675251 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign.ts @@ -6,6 +6,8 @@ */ import type { SavedObjectsClientContract, ElasticsearchClient } from '@kbn/core/server'; +import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common'; + import type { Agent } from '../../types'; import { agentPolicyService } from '../agent_policy'; import { @@ -16,52 +18,54 @@ import { import { SO_SEARCH_LIMIT } from '../../constants'; +import { agentsKueryNamespaceFilter } from '../spaces/agent_namespaces'; +import { getCurrentNamespace } from '../spaces/get_current_namespace'; + import { getAgentsById, getAgentPolicyForAgent, updateAgent, getAgentsByKuery, openPointInTime, + getAgentById, } from './crud'; import type { GetAgentsOptions } from '.'; import { createAgentAction } from './actions'; import { ReassignActionRunner, reassignBatch } from './reassign_action_runner'; -export async function reassignAgent( +async function verifyNewAgentPolicy( soClient: SavedObjectsClientContract, - esClient: ElasticsearchClient, - agentId: string, newAgentPolicyId: string ) { - const newAgentPolicy = await agentPolicyService.get(soClient, newAgentPolicyId); + let newAgentPolicy; + try { + newAgentPolicy = await agentPolicyService.get(soClient, newAgentPolicyId); + } catch (err) { + if (err instanceof SavedObjectNotFound) { + throw new AgentPolicyNotFoundError(`Agent policy not found: ${newAgentPolicyId}`); + } + } if (!newAgentPolicy) { throw new AgentPolicyNotFoundError(`Agent policy not found: ${newAgentPolicyId}`); } - - await reassignAgentIsAllowed(soClient, esClient, agentId, newAgentPolicyId); - - await updateAgent(esClient, agentId, { - policy_id: newAgentPolicyId, - policy_revision: null, - }); - - await createAgentAction(esClient, { - agents: [agentId], - created_at: new Date().toISOString(), - type: 'POLICY_REASSIGN', - data: { - policy_id: newAgentPolicyId, - }, - }); + if (newAgentPolicy?.is_managed) { + throw new HostedAgentPolicyRestrictionRelatedError( + `Cannot reassign agents to hosted agent policy ${newAgentPolicy.id}` + ); + } } -export async function reassignAgentIsAllowed( +export async function reassignAgent( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, agentId: string, newAgentPolicyId: string ) { + await verifyNewAgentPolicy(soClient, newAgentPolicyId); + + await getAgentById(esClient, soClient, agentId); // throw 404 if agent not in namespace + const agentPolicy = await getAgentPolicyForAgent(soClient, esClient, agentId); if (agentPolicy?.is_managed) { throw new HostedAgentPolicyRestrictionRelatedError( @@ -69,14 +73,23 @@ export async function reassignAgentIsAllowed( ); } - const newAgentPolicy = await agentPolicyService.get(soClient, newAgentPolicyId); - if (newAgentPolicy?.is_managed) { - throw new HostedAgentPolicyRestrictionRelatedError( - `Cannot reassign an agent to hosted agent policy ${newAgentPolicy.id}` - ); - } + await updateAgent(esClient, agentId, { + policy_id: newAgentPolicyId, + policy_revision: null, + }); + + const currentNameSpace = getCurrentNamespace(soClient); + const namespaces = currentNameSpace ? { namespaces: [currentNameSpace] } : {}; - return true; + await createAgentAction(esClient, { + agents: [agentId], + created_at: new Date().toISOString(), + type: 'POLICY_REASSIGN', + data: { + policy_id: newAgentPolicyId, + }, + ...namespaces, + }); } export async function reassignAgents( @@ -88,16 +101,9 @@ export async function reassignAgents( }, newAgentPolicyId: string ): Promise<{ actionId: string }> { - const newAgentPolicy = await agentPolicyService.get(soClient, newAgentPolicyId); - if (!newAgentPolicy) { - throw new AgentPolicyNotFoundError(`Agent policy not found: ${newAgentPolicyId}`); - } - if (newAgentPolicy.is_managed) { - throw new HostedAgentPolicyRestrictionRelatedError( - `Cannot reassign an agent to hosted agent policy ${newAgentPolicy.id}` - ); - } + await verifyNewAgentPolicy(soClient, newAgentPolicyId); + const currentNameSpace = getCurrentNamespace(soClient); const outgoingErrors: Record = {}; let givenAgents: Agent[] = []; if ('agents' in options) { @@ -115,8 +121,10 @@ export async function reassignAgents( } } else if ('kuery' in options) { const batchSize = options.batchSize ?? SO_SEARCH_LIMIT; + const namespaceFilter = await agentsKueryNamespaceFilter(currentNameSpace); + const kuery = namespaceFilter ? `${namespaceFilter} AND ${options.kuery}` : options.kuery; const res = await getAgentsByKuery(esClient, soClient, { - kuery: options.kuery, + kuery, showInactive: options.showInactive ?? false, page: 1, perPage: batchSize, @@ -130,6 +138,7 @@ export async function reassignAgents( soClient, { ...options, + spaceId: currentNameSpace, batchSize, total: res.total, newAgentPolicyId, @@ -139,5 +148,10 @@ export async function reassignAgents( } } - return await reassignBatch(soClient, esClient, { newAgentPolicyId }, givenAgents, outgoingErrors); + return await reassignBatch( + esClient, + { newAgentPolicyId, spaceId: currentNameSpace }, + givenAgents, + outgoingErrors + ); } diff --git a/x-pack/plugins/fleet/server/services/agents/reassign_action_runner.ts b/x-pack/plugins/fleet/server/services/agents/reassign_action_runner.ts index b03146ab6b387b..cd9183b0771bcc 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign_action_runner.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign_action_runner.ts @@ -5,7 +5,7 @@ * 2.0. */ import { v4 as uuidv4 } from 'uuid'; -import type { SavedObjectsClientContract, ElasticsearchClient } from '@kbn/core/server'; +import type { ElasticsearchClient } from '@kbn/core/server'; import type { Agent } from '../../types'; @@ -22,7 +22,7 @@ import { BulkActionTaskType } from './bulk_action_types'; export class ReassignActionRunner extends ActionRunner { protected async processAgents(agents: Agent[]): Promise<{ actionId: string }> { - return await reassignBatch(this.soClient, this.esClient, this.actionParams! as any, agents, {}); + return await reassignBatch(this.esClient, this.actionParams! as any, agents, {}); } protected getTaskType() { @@ -35,16 +35,18 @@ export class ReassignActionRunner extends ActionRunner { } export async function reassignBatch( - soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, options: { newAgentPolicyId: string; actionId?: string; total?: number; + spaceId?: string; }, givenAgents: Agent[], outgoingErrors: Record ): Promise<{ actionId: string }> { + const spaceId = options.spaceId; + const soClient = appContextService.getInternalUserSOClientForSpaceId(spaceId); const errors: Record = { ...outgoingErrors }; const hostedPolicies = await getHostedPolicies(soClient, givenAgents); @@ -86,8 +88,9 @@ export async function reassignBatch( const actionId = options.actionId ?? uuidv4(); const total = options.total ?? givenAgents.length; - const now = new Date().toISOString(); + const namespaces = spaceId ? { namespaces: [spaceId] } : {}; + await createAgentAction(esClient, { id: actionId, agents: agentsToUpdate.map((agent) => agent.id), @@ -97,6 +100,7 @@ export async function reassignBatch( data: { policy_id: options.newAgentPolicyId, }, + ...namespaces, }); await createErrorActionResults( diff --git a/x-pack/plugins/fleet/server/services/agents/update_agent_tags.test.ts b/x-pack/plugins/fleet/server/services/agents/update_agent_tags.test.ts index efeb5649cd5763..cd408d953e31a3 100644 --- a/x-pack/plugins/fleet/server/services/agents/update_agent_tags.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/update_agent_tags.test.ts @@ -423,30 +423,6 @@ describe('update_agent_tags', () => { jest.mocked(isSpaceAwarenessEnabled).mockResolvedValue(true); }); - it('should not update tags for agents in another space', async () => { - soClient.getCurrentNamespace.mockReturnValue('default'); - esClient.search.mockResolvedValue({ - hits: { - hits: [ - { - _id: 'agent1', - _source: { - tags: ['one', 'two', 'three'], - namespaces: ['myspace'], - }, - fields: { - status: 'online', - }, - }, - ], - }, - } as any); - - await updateAgentTags(soClient, esClient, { agentIds: ['agent1'] }, ['one'], ['two']); - - expect(esClient.updateByQuery).not.toHaveBeenCalled(); - }); - it('should add namespace filter to kuery in the default space', async () => { soClient.getCurrentNamespace.mockReturnValue('default'); diff --git a/x-pack/plugins/fleet/server/services/agents/update_agent_tags.ts b/x-pack/plugins/fleet/server/services/agents/update_agent_tags.ts index 7d37581cef997c..4e42ac121ccca6 100644 --- a/x-pack/plugins/fleet/server/services/agents/update_agent_tags.ts +++ b/x-pack/plugins/fleet/server/services/agents/update_agent_tags.ts @@ -27,9 +27,9 @@ export async function updateAgentTags( tagsToAdd: string[], tagsToRemove: string[] ): Promise<{ actionId: string }> { + const currentNameSpace = getCurrentNamespace(soClient); const outgoingErrors: Record = {}; const givenAgents: Agent[] = []; - const currentNameSpace = getCurrentNamespace(soClient); if ('agentIds' in options) { const maybeAgents = await getAgentsById(esClient, soClient, options.agentIds); @@ -48,8 +48,8 @@ export async function updateAgentTags( } } else if ('kuery' in options) { const batchSize = options.batchSize ?? SO_SEARCH_LIMIT; - const namespaceFilter = await agentsKueryNamespaceFilter(currentNameSpace); + const filters = namespaceFilter ? [namespaceFilter] : []; if (options.kuery !== '') { filters.push(options.kuery); @@ -86,8 +86,15 @@ export async function updateAgentTags( ).runActionAsyncWithRetry(); } - return await updateTagsBatch(soClient, esClient, givenAgents, outgoingErrors, { - tagsToAdd, - tagsToRemove, - }); + return await updateTagsBatch( + soClient, + esClient, + givenAgents, + outgoingErrors, + { + tagsToAdd, + tagsToRemove, + }, + currentNameSpace + ); } diff --git a/x-pack/plugins/fleet/server/services/agents/update_agent_tags_action_runner.ts b/x-pack/plugins/fleet/server/services/agents/update_agent_tags_action_runner.ts index 8b68e1b6e9fd83..bb3b5f71cb222b 100644 --- a/x-pack/plugins/fleet/server/services/agents/update_agent_tags_action_runner.ts +++ b/x-pack/plugins/fleet/server/services/agents/update_agent_tags_action_runner.ts @@ -17,8 +17,6 @@ import { appContextService } from '../app_context'; import { FleetError } from '../../errors'; -import { getCurrentNamespace } from '../spaces/get_current_namespace'; - import { ActionRunner } from './action_runner'; import { BulkActionTaskType } from './bulk_action_types'; @@ -63,7 +61,8 @@ export async function updateTagsBatch( total?: number; kuery?: string; retryCount?: number; - } + }, + spaceId?: string ): Promise<{ actionId: string; updated?: number; took?: number }> { const errors: Record = { ...outgoingErrors }; const hostedAgentError = `Cannot modify tags on a hosted agent`; @@ -151,8 +150,7 @@ export async function updateTagsBatch( const versionConflictCount = res.version_conflicts ?? 0; const versionConflictIds = isLastRetry ? getUuidArray(versionConflictCount) : []; - const currentNameSpace = getCurrentNamespace(soClient); - const namespaces = currentNameSpace ? { namespaces: [currentNameSpace] } : {}; + const namespaces = spaceId ? { namespaces: [spaceId] } : {}; // creating an action doc so that update tags shows up in activity // the logic only saves agent count in the action that updated, failed or in case of last retry, conflicted @@ -195,7 +193,7 @@ export async function updateTagsBatch( failures.map((failure) => ({ agentId: failure.id, actionId, - namespace: currentNameSpace, + namespace: spaceId, error: failure.cause.reason, })) ); @@ -210,7 +208,7 @@ export async function updateTagsBatch( versionConflictIds.map((id) => ({ agentId: id, actionId, - namespace: currentNameSpace, + namespace: spaceId, error: 'version conflict on last retry', })) ); diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade.test.ts b/x-pack/plugins/fleet/server/services/agents/upgrade.test.ts index b1e78862fde0e2..7dbfaf86bd2723 100644 --- a/x-pack/plugins/fleet/server/services/agents/upgrade.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/upgrade.test.ts @@ -35,11 +35,15 @@ jest.mock('./action_status', () => { }); describe('sendUpgradeAgentsActions (plural)', () => { + let mocks: ReturnType; + beforeEach(async () => { - const { soClient } = createClientMock(); + mocks = createClientMock(); + appContextService.start( createAppContextStartContractMock({}, false, { - withoutSpaceExtensions: soClient, + internal: mocks.soClient, + withoutSpaceExtensions: mocks.soClient, }) ); }); @@ -48,7 +52,7 @@ describe('sendUpgradeAgentsActions (plural)', () => { appContextService.stop(); }); it('can upgrade from an regular agent policy', async () => { - const { soClient, esClient, agentInRegularDoc, agentInRegularDoc2 } = createClientMock(); + const { soClient, esClient, agentInRegularDoc, agentInRegularDoc2 } = mocks; const idsToAction = [agentInRegularDoc._id, agentInRegularDoc2._id]; await sendUpgradeAgentsActions(soClient, esClient, { agentIds: idsToAction, version: '8.5.0' }); @@ -68,8 +72,7 @@ describe('sendUpgradeAgentsActions (plural)', () => { } }); it('cannot upgrade from a hosted agent policy by default', async () => { - const { soClient, esClient, agentInHostedDoc, agentInRegularDoc, agentInRegularDoc2 } = - createClientMock(); + const { soClient, esClient, agentInHostedDoc, agentInRegularDoc, agentInRegularDoc2 } = mocks; const idsToAction = [agentInRegularDoc._id, agentInHostedDoc._id, agentInRegularDoc2._id]; await sendUpgradeAgentsActions(soClient, esClient, { agentIds: idsToAction, version: '8.5.0' }); @@ -104,8 +107,8 @@ describe('sendUpgradeAgentsActions (plural)', () => { }); it('can upgrade from hosted agent policy with force=true', async () => { - const { soClient, esClient, agentInHostedDoc, agentInRegularDoc, agentInRegularDoc2 } = - createClientMock(); + const { soClient, esClient, agentInHostedDoc, agentInRegularDoc, agentInRegularDoc2 } = mocks; + const idsToAction = [agentInRegularDoc._id, agentInHostedDoc._id, agentInRegularDoc2._id]; await sendUpgradeAgentsActions(soClient, esClient, { agentIds: idsToAction, @@ -129,9 +132,9 @@ describe('sendUpgradeAgentsActions (plural)', () => { }); it('skip upgrade if action id is cancelled', async () => { - const { soClient, esClient, agentInRegularDoc } = createClientMock(); + const { esClient, agentInRegularDoc } = mocks; const agents = [{ id: agentInRegularDoc._id } as Agent]; - await upgradeBatch(soClient, esClient, agents, {}, { + await upgradeBatch(esClient, agents, {}, { actionId: 'cancelled-action', } as any); }); diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade.ts b/x-pack/plugins/fleet/server/services/agents/upgrade.ts index a164b9ff86399c..40d676a68e24df 100644 --- a/x-pack/plugins/fleet/server/services/agents/upgrade.ts +++ b/x-pack/plugins/fleet/server/services/agents/upgrade.ts @@ -10,6 +10,9 @@ import type { Agent } from '../../types'; import { AgentReassignmentError, HostedAgentPolicyRestrictionRelatedError } from '../../errors'; import { SO_SEARCH_LIMIT } from '../../constants'; +import { agentsKueryNamespaceFilter } from '../spaces/agent_namespaces'; +import { getCurrentNamespace } from '../spaces/get_current_namespace'; + import { createAgentAction } from './actions'; import type { GetAgentsOptions } from './crud'; import { openPointInTime } from './crud'; @@ -43,12 +46,16 @@ export async function sendUpgradeAgentAction({ ); } + const currentNameSpace = getCurrentNamespace(soClient); + const namespaces = currentNameSpace ? { namespaces: [currentNameSpace] } : {}; + await createAgentAction(esClient, { agents: [agentId], created_at: now, data, ack_data: data, type: 'UPGRADE', + ...namespaces, }); await updateAgent(esClient, agentId, { upgraded_at: null, @@ -69,9 +76,11 @@ export async function sendUpgradeAgentsActions( batchSize?: number; } ): Promise<{ actionId: string }> { + const currentNameSpace = getCurrentNamespace(soClient); // Full set of agents const outgoingErrors: Record = {}; let givenAgents: Agent[] = []; + if ('agents' in options) { givenAgents = options.agents; } else if ('agentIds' in options) { @@ -87,12 +96,16 @@ export async function sendUpgradeAgentsActions( } } else if ('kuery' in options) { const batchSize = options.batchSize ?? SO_SEARCH_LIMIT; + const namespaceFilter = await agentsKueryNamespaceFilter(currentNameSpace); + const kuery = namespaceFilter ? `${namespaceFilter} AND ${options.kuery}` : options.kuery; + const res = await getAgentsByKuery(esClient, soClient, { - kuery: options.kuery, + kuery, showInactive: options.showInactive ?? false, page: 1, perPage: batchSize, }); + if (res.total <= batchSize) { givenAgents = res.agents; } else { @@ -103,11 +116,12 @@ export async function sendUpgradeAgentsActions( ...options, batchSize, total: res.total, + spaceId: currentNameSpace, }, { pitId: await openPointInTime(esClient) } ).runActionAsyncWithRetry(); } } - return await upgradeBatch(soClient, esClient, givenAgents, outgoingErrors, options); + return await upgradeBatch(esClient, givenAgents, outgoingErrors, options, currentNameSpace); } diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade_action_runner.ts b/x-pack/plugins/fleet/server/services/agents/upgrade_action_runner.ts index d123489fe9eadc..a11b43a5b3ee23 100644 --- a/x-pack/plugins/fleet/server/services/agents/upgrade_action_runner.ts +++ b/x-pack/plugins/fleet/server/services/agents/upgrade_action_runner.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { SavedObjectsClientContract, ElasticsearchClient } from '@kbn/core/server'; +import type { ElasticsearchClient } from '@kbn/core/server'; import { v4 as uuidv4 } from 'uuid'; import moment from 'moment'; @@ -34,7 +34,13 @@ import { getLatestAvailableAgentVersion } from './versions'; export class UpgradeActionRunner extends ActionRunner { protected async processAgents(agents: Agent[]): Promise<{ actionId: string }> { - return await upgradeBatch(this.soClient, this.esClient, agents, {}, this.actionParams! as any); + return await upgradeBatch( + this.esClient, + agents, + {}, + this.actionParams! as any, + this.actionParams?.spaceId + ); } protected getTaskType() { @@ -52,7 +58,6 @@ const isActionIdCancelled = async (esClient: ElasticsearchClient, actionId: stri }; export async function upgradeBatch( - soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, givenAgents: Agent[], outgoingErrors: Record, @@ -65,8 +70,10 @@ export async function upgradeBatch( upgradeDurationSeconds?: number; startTime?: string; total?: number; - } + }, + spaceId?: string ): Promise<{ actionId: string }> { + const soClient = appContextService.getInternalUserSOClientForSpaceId(spaceId); const errors: Record = { ...outgoingErrors }; const hostedPolicies = await getHostedPolicies(soClient, givenAgents); @@ -167,6 +174,7 @@ export async function upgradeBatch( const actionId = options.actionId ?? uuidv4(); const total = options.total ?? givenAgents.length; + const namespaces = spaceId ? { namespaces: [spaceId] } : {}; await createAgentAction(esClient, { id: actionId, @@ -177,6 +185,7 @@ export async function upgradeBatch( total, agents: agentsToUpdate.map((agent) => agent.id), ...rollingUpgradeOptions, + ...namespaces, }); await createErrorActionResults( diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/actions.ts b/x-pack/test/fleet_api_integration/apis/space_awareness/actions.ts index 4f458cd7190cc8..14c3dff3389559 100644 --- a/x-pack/test/fleet_api_integration/apis/space_awareness/actions.ts +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/actions.ts @@ -19,8 +19,7 @@ export default function (providerContext: FtrProviderContext) { const esClient = getService('es'); const kibanaServer = getService('kibanaServer'); - // Failing: See https://github.com/elastic/kibana/issues/189805 - describe.skip('actions', async function () { + describe('actions', async function () { skipIfNoDockerRegistry(providerContext); const apiClient = new SpaceTestApiClient(supertest); @@ -221,16 +220,14 @@ export default function (providerContext: FtrProviderContext) { .set('kbn-xsrf', 'xxxx') .send({ action: { type: 'UNENROLL' } }) .expect(404); - expect(resInDefaultSpace.body.message).to.eql(`${testSpaceAgent1} not found in namespace`); + expect(resInDefaultSpace.body.message).to.eql(`Agent ${testSpaceAgent1} not found`); const resInCustomSpace = await supertest .post(`/s/${TEST_SPACE_1}/api/fleet/agents/${defaultSpaceAgent1}/actions`) .set('kbn-xsrf', 'xxxx') .send({ action: { type: 'UNENROLL' } }) .expect(404); - expect(resInCustomSpace.body.message).to.eql( - `${defaultSpaceAgent1} not found in namespace` - ); + expect(resInCustomSpace.body.message).to.eql(`Agent ${defaultSpaceAgent1} not found`); }); it('should create an action with set namespace in the default space', async () => { @@ -253,5 +250,51 @@ export default function (providerContext: FtrProviderContext) { expect(actionStatusInCustomSpace.items.length).to.eql(1); }); }); + + describe('post /agents/actions/{actionId}/cancel', () => { + it('should return 200 and a CANCEL action if the action is in the same space', async () => { + // Create UPDATE_TAGS action for agents in custom space + await apiClient.bulkUpdateAgentTags( + { + agents: [testSpaceAgent1, testSpaceAgent2], + tagsToAdd: ['tag1'], + }, + TEST_SPACE_1 + ); + + const actionStatusInCustomSpace = await apiClient.getActionStatus(TEST_SPACE_1); + expect(actionStatusInCustomSpace.items.length).to.eql(1); + + const res = await apiClient.cancelAction( + actionStatusInCustomSpace.items[0].actionId, + TEST_SPACE_1 + ); + expect(res.item.type).to.eql('CANCEL'); + }); + + it('should return 404 if the action is in a different space', async () => { + // Create UPDATE_TAGS action for agents in custom space + await apiClient.bulkUpdateAgentTags( + { + agents: [testSpaceAgent1, testSpaceAgent2], + tagsToAdd: ['tag1'], + }, + TEST_SPACE_1 + ); + + const actionStatusInCustomSpace = await apiClient.getActionStatus(TEST_SPACE_1); + expect(actionStatusInCustomSpace.items.length).to.eql(1); + + let err: Error | undefined; + try { + await apiClient.cancelAction(actionStatusInCustomSpace.items[0].actionId); + } catch (_err) { + err = _err; + } + + expect(err).to.be.an(Error); + expect(err?.message).to.match(/404 "Not Found"/); + }); + }); }); } diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/agents.ts b/x-pack/test/fleet_api_integration/apis/space_awareness/agents.ts index b4f7241dec0fb6..f41f83f71ccb73 100644 --- a/x-pack/test/fleet_api_integration/apis/space_awareness/agents.ts +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/agents.ts @@ -10,7 +10,12 @@ import { CreateAgentPolicyResponse, GetAgentsResponse } from '@kbn/fleet-plugin/ import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { skipIfNoDockerRegistry } from '../../helpers'; import { SpaceTestApiClient } from './api_helper'; -import { cleanFleetIndices, createFleetAgent } from './helpers'; +import { + cleanFleetAgents, + cleanFleetIndices, + createFleetAgent, + makeAgentsUpgradeable, +} from './helpers'; import { setupTestSpaces, TEST_SPACE_1 } from './space_helpers'; export default function (providerContext: FtrProviderContext) { @@ -41,6 +46,7 @@ export default function (providerContext: FtrProviderContext) { setupTestSpaces(providerContext); let defaultSpacePolicy1: CreateAgentPolicyResponse; + let defaultSpacePolicy2: CreateAgentPolicyResponse; let spaceTest1Policy1: CreateAgentPolicyResponse; let spaceTest1Policy2: CreateAgentPolicyResponse; @@ -48,36 +54,50 @@ export default function (providerContext: FtrProviderContext) { let defaultSpaceAgent2: string; let testSpaceAgent1: string; let testSpaceAgent2: string; + let testSpaceAgent3: string; + + async function createAgents() { + const [ + _defaultSpaceAgent1, + _defaultSpaceAgent2, + _testSpaceAgent1, + _testSpaceAgent2, + _testSpaceAgent3, + ] = await Promise.all([ + createFleetAgent(esClient, defaultSpacePolicy1.item.id, 'default'), + createFleetAgent(esClient, defaultSpacePolicy2.item.id), + createFleetAgent(esClient, spaceTest1Policy1.item.id, TEST_SPACE_1), + createFleetAgent(esClient, spaceTest1Policy2.item.id, TEST_SPACE_1), + createFleetAgent(esClient, spaceTest1Policy1.item.id, TEST_SPACE_1), + ]); + defaultSpaceAgent1 = _defaultSpaceAgent1; + defaultSpaceAgent2 = _defaultSpaceAgent2; + testSpaceAgent1 = _testSpaceAgent1; + testSpaceAgent2 = _testSpaceAgent2; + testSpaceAgent3 = _testSpaceAgent3; + } before(async () => { await apiClient.postEnableSpaceAwareness(); - - const [_defaultSpacePolicy1, _spaceTest1Policy1, _spaceTest1Policy2] = await Promise.all([ - apiClient.createAgentPolicy(), - apiClient.createAgentPolicy(TEST_SPACE_1), - apiClient.createAgentPolicy(TEST_SPACE_1), - ]); + const [_defaultSpacePolicy1, _defaultSpacePolicy2, _spaceTest1Policy1, _spaceTest1Policy2] = + await Promise.all([ + apiClient.createAgentPolicy(), + apiClient.createAgentPolicy(), + apiClient.createAgentPolicy(TEST_SPACE_1), + apiClient.createAgentPolicy(TEST_SPACE_1), + ]); defaultSpacePolicy1 = _defaultSpacePolicy1; + defaultSpacePolicy2 = _defaultSpacePolicy2; spaceTest1Policy1 = _spaceTest1Policy1; spaceTest1Policy2 = _spaceTest1Policy2; - const [_defaultSpaceAgent1, _defaultSpaceAgent2, _testSpaceAgent1, _testSpaceAgent2] = - await Promise.all([ - createFleetAgent(esClient, defaultSpacePolicy1.item.id, 'default'), - createFleetAgent(esClient, defaultSpacePolicy1.item.id), - createFleetAgent(esClient, spaceTest1Policy1.item.id, TEST_SPACE_1), - createFleetAgent(esClient, spaceTest1Policy2.item.id, TEST_SPACE_1), - ]); - defaultSpaceAgent1 = _defaultSpaceAgent1; - defaultSpaceAgent2 = _defaultSpaceAgent2; - testSpaceAgent1 = _testSpaceAgent1; - testSpaceAgent2 = _testSpaceAgent2; + await createAgents(); }); - describe('GET /agents', () => { + describe('GET /agent', () => { it('should return agents in a specific space', async () => { const agents = await apiClient.getAgents(TEST_SPACE_1); - expect(agents.total).to.eql(2); + expect(agents.total).to.eql(3); const agentIds = agents.items?.map((item) => item.id); expect(agentIds).to.contain(testSpaceAgent1); expect(agentIds).to.contain(testSpaceAgent2); @@ -92,7 +112,7 @@ export default function (providerContext: FtrProviderContext) { }); }); - describe('GET /agents/{id}', () => { + describe('GET /agents/{agentId}', () => { it('should allow to retrieve agent in the same space', async () => { await apiClient.getAgent(testSpaceAgent1, TEST_SPACE_1); }); @@ -110,13 +130,13 @@ export default function (providerContext: FtrProviderContext) { }); }); - describe('PUT /agents/{id}', () => { - it('should allow to update an agent in the same space', async () => { + describe('PUT /agents/{agentId}', () => { + it('should allow updating an agent in the same space', async () => { await apiClient.updateAgent(testSpaceAgent1, { tags: ['foo'] }, TEST_SPACE_1); await apiClient.updateAgent(testSpaceAgent1, { tags: ['tag1'] }, TEST_SPACE_1); }); - it('should not allow to update an agent from a different space from the default space', async () => { + it('should not allow updating an agent from a different space', async () => { let err: Error | undefined; try { await apiClient.updateAgent(testSpaceAgent1, { tags: ['foo'] }); @@ -131,15 +151,15 @@ export default function (providerContext: FtrProviderContext) { describe('DELETE /agents/{id}', () => { it('should allow to delete an agent in the same space', async () => { - const testSpaceAgent3 = await createFleetAgent( + const testSpaceDeleteAgent = await createFleetAgent( esClient, spaceTest1Policy2.item.id, TEST_SPACE_1 ); - await apiClient.deleteAgent(testSpaceAgent3, TEST_SPACE_1); + await apiClient.deleteAgent(testSpaceDeleteAgent, TEST_SPACE_1); }); - it('should not allow to delete an agent from a different space from the default space', async () => { + it('should not allow deleting an agent from a different space', async () => { let err: Error | undefined; try { await apiClient.deleteAgent(testSpaceAgent1); @@ -229,5 +249,333 @@ export default function (providerContext: FtrProviderContext) { expect(agentInTestSpaceTags[testSpaceAgent2]).to.eql(['tag1']); }); }); + + describe('POST /agents/{agentId}/upgrade', () => { + beforeEach(async () => { + await cleanFleetAgents(esClient); + await createAgents(); + }); + + it('should allow upgrading an agent in the same space', async () => { + await makeAgentsUpgradeable(esClient, [testSpaceAgent1], '8.14.0'); + await apiClient.upgradeAgent(testSpaceAgent1, { version: '8.15.0' }, TEST_SPACE_1); + }); + + it('should forbid upgrading an agent from a different space', async () => { + await makeAgentsUpgradeable(esClient, [testSpaceAgent1], '8.14.0'); + const res = await supertest + .post(`/api/fleet/agents/${testSpaceAgent1}/upgrade`) + .set('kbn-xsrf', 'xxxx') + .send({ version: '8.15.0' }) + .expect(404); + expect(res.body.message).to.eql(`Agent ${testSpaceAgent1} not found`); + }); + }); + + describe('POST /agents/bulk_upgrade', () => { + beforeEach(async () => { + await cleanFleetAgents(esClient); + await createAgents(); + }); + + function getAgentStatus(agents: GetAgentsResponse) { + return agents.items?.reduce((acc, item) => { + acc[item.id] = item.status; + return acc; + }, {} as any); + } + + it('should only upgrade agents in the same space when passing a list of agent ids', async () => { + await makeAgentsUpgradeable( + esClient, + [defaultSpaceAgent1, defaultSpaceAgent2, testSpaceAgent1, testSpaceAgent2], + '8.14.0' + ); + + let agents = await apiClient.getAgents(); + let agentStatus = getAgentStatus(agents); + expect(agentStatus).to.eql({ + [defaultSpaceAgent1]: 'online', + [defaultSpaceAgent2]: 'online', + }); + + agents = await apiClient.getAgents(TEST_SPACE_1); + agentStatus = getAgentStatus(agents); + expect(agentStatus).to.eql({ + [testSpaceAgent1]: 'online', + [testSpaceAgent2]: 'online', + [testSpaceAgent3]: 'online', + }); + + await apiClient.bulkUpgradeAgents( + { + agents: [defaultSpaceAgent1, testSpaceAgent1], + version: '8.15.0', + skipRateLimitCheck: true, + }, + TEST_SPACE_1 + ); + + agents = await apiClient.getAgents(); + agentStatus = getAgentStatus(agents); + expect(agentStatus).to.eql({ + [defaultSpaceAgent1]: 'online', + [defaultSpaceAgent2]: 'online', + }); + + agents = await apiClient.getAgents(TEST_SPACE_1); + agentStatus = getAgentStatus(agents); + expect(agentStatus).to.eql({ + [testSpaceAgent1]: 'updating', + [testSpaceAgent2]: 'online', + [testSpaceAgent3]: 'online', + }); + }); + + it('should only upgrade agents in the same space when passing a kuery', async () => { + await makeAgentsUpgradeable( + esClient, + [defaultSpaceAgent1, defaultSpaceAgent2, testSpaceAgent1, testSpaceAgent2], + '8.14.0' + ); + + let agents = await apiClient.getAgents(); + let agentStatus = getAgentStatus(agents); + expect(agentStatus).to.eql({ + [defaultSpaceAgent1]: 'online', + [defaultSpaceAgent2]: 'online', + }); + + agents = await apiClient.getAgents(TEST_SPACE_1); + agentStatus = getAgentStatus(agents); + expect(agentStatus).to.eql({ + [testSpaceAgent1]: 'online', + [testSpaceAgent2]: 'online', + [testSpaceAgent3]: 'online', + }); + + await apiClient.bulkUpgradeAgents( + { + agents: 'status:online', + version: '8.15.0', + skipRateLimitCheck: true, + }, + TEST_SPACE_1 + ); + + agents = await apiClient.getAgents(); + agentStatus = getAgentStatus(agents); + expect(agentStatus).to.eql({ + [defaultSpaceAgent1]: 'online', + [defaultSpaceAgent2]: 'online', + }); + + agents = await apiClient.getAgents(TEST_SPACE_1); + agentStatus = getAgentStatus(agents); + expect(agentStatus).to.eql({ + [testSpaceAgent1]: 'updating', + [testSpaceAgent2]: 'updating', + [testSpaceAgent3]: 'updating', + }); + }); + }); + + describe('POST /agents/{agentId}/reassign', () => { + beforeEach(async () => { + await cleanFleetAgents(esClient); + await createAgents(); + }); + it('should allow reassigning an agent in the current space to a policy in the current space', async () => { + let agent = await apiClient.getAgent(defaultSpaceAgent1); + expect(agent.item.policy_id).to.eql(defaultSpacePolicy1.item.id); + await apiClient.reassignAgent(defaultSpaceAgent1, defaultSpacePolicy2.item.id); + agent = await apiClient.getAgent(defaultSpaceAgent1); + expect(agent.item.policy_id).to.eql(defaultSpacePolicy2.item.id); + + agent = await apiClient.getAgent(testSpaceAgent1, TEST_SPACE_1); + expect(agent.item.policy_id).to.eql(spaceTest1Policy1.item.id); + await apiClient.reassignAgent(testSpaceAgent1, spaceTest1Policy2.item.id, TEST_SPACE_1); + agent = await apiClient.getAgent(testSpaceAgent1, TEST_SPACE_1); + expect(agent.item.policy_id).to.eql(spaceTest1Policy2.item.id); + + await apiClient.reassignAgent(defaultSpaceAgent1, defaultSpacePolicy1.item.id); + await apiClient.reassignAgent(testSpaceAgent1, spaceTest1Policy1.item.id, TEST_SPACE_1); + }); + + it('should not allow reassigning an agent in a different space', async () => { + let err: Error | undefined; + try { + await apiClient.reassignAgent(testSpaceAgent1, defaultSpacePolicy2.item.id); + } catch (_err) { + err = _err; + } + + expect(err).to.be.an(Error); + expect(err?.message).to.match(/404 "Not Found"/); + }); + + it('should not allow reassigning an agent in the current space to a policy in a different space', async () => { + let err: Error | undefined; + try { + await apiClient.reassignAgent(defaultSpaceAgent1, spaceTest1Policy2.item.id); + } catch (_err) { + err = _err; + } + + expect(err).to.be.an(Error); + expect(err?.message).to.match(/404 "Not Found"/); + }); + }); + + describe('POST /agents/bulk_reassign', () => { + beforeEach(async () => { + await cleanFleetAgents(esClient); + await createAgents(); + }); + function getAgentPolicyIds(agents: GetAgentsResponse) { + return agents.items?.reduce((acc, item) => { + acc[item.id] = item.policy_id; + return acc; + }, {} as any); + } + + it('should return 404 if the policy is in another space', async () => { + let err: Error | undefined; + try { + await apiClient.bulkReassignAgents({ + agents: [defaultSpaceAgent1, testSpaceAgent1], + policy_id: spaceTest1Policy2.item.id, + }); + } catch (_err) { + err = _err; + } + + expect(err).to.be.an(Error); + expect(err?.message).to.match(/404 "Not Found"/); + }); + + it('should only reassign agents in the same space when passing a list of agent ids', async () => { + let agent = await apiClient.getAgent(defaultSpaceAgent1); + expect(agent.item.policy_id).to.eql(defaultSpacePolicy1.item.id); + agent = await apiClient.getAgent(testSpaceAgent1, TEST_SPACE_1); + expect(agent.item.policy_id).to.eql(spaceTest1Policy1.item.id); + + await apiClient.bulkReassignAgents( + { + agents: [defaultSpaceAgent1, testSpaceAgent1], + policy_id: spaceTest1Policy2.item.id, + }, + TEST_SPACE_1 + ); + + agent = await apiClient.getAgent(defaultSpaceAgent1); + expect(agent.item.policy_id).to.eql(defaultSpacePolicy1.item.id); + agent = await apiClient.getAgent(testSpaceAgent1, TEST_SPACE_1); + expect(agent.item.policy_id).to.eql(spaceTest1Policy2.item.id); + + await apiClient.reassignAgent(testSpaceAgent1, spaceTest1Policy1.item.id, TEST_SPACE_1); + }); + + it('should only reassign agents in the same space when passing a kuery', async () => { + let agents = await apiClient.getAgents(); + let agentPolicyIds = getAgentPolicyIds(agents); + expect(agentPolicyIds).to.eql({ + [defaultSpaceAgent1]: defaultSpacePolicy1.item.id, + [defaultSpaceAgent2]: defaultSpacePolicy2.item.id, + }); + agents = await apiClient.getAgents(TEST_SPACE_1); + agentPolicyIds = getAgentPolicyIds(agents); + expect(agentPolicyIds).to.eql({ + [testSpaceAgent1]: spaceTest1Policy1.item.id, + [testSpaceAgent2]: spaceTest1Policy2.item.id, + [testSpaceAgent3]: spaceTest1Policy1.item.id, + }); + + await apiClient.bulkReassignAgents( + { + agents: '*', + policy_id: spaceTest1Policy2.item.id, + }, + TEST_SPACE_1 + ); + + agents = await apiClient.getAgents(); + agentPolicyIds = getAgentPolicyIds(agents); + expect(agentPolicyIds).to.eql({ + [defaultSpaceAgent1]: defaultSpacePolicy1.item.id, + [defaultSpaceAgent2]: defaultSpacePolicy2.item.id, + }); + agents = await apiClient.getAgents(TEST_SPACE_1); + agentPolicyIds = getAgentPolicyIds(agents); + expect(agentPolicyIds).to.eql({ + [testSpaceAgent1]: spaceTest1Policy2.item.id, + [testSpaceAgent2]: spaceTest1Policy2.item.id, + [testSpaceAgent3]: spaceTest1Policy2.item.id, + }); + + await apiClient.reassignAgent(testSpaceAgent1, spaceTest1Policy1.item.id, TEST_SPACE_1); + await apiClient.reassignAgent(testSpaceAgent2, spaceTest1Policy1.item.id, TEST_SPACE_1); + }); + + it('should reassign agents in the same space by kuery in batches', async () => { + let agents = await apiClient.getAgents(); + let agentPolicyIds = getAgentPolicyIds(agents); + expect(agentPolicyIds).to.eql({ + [defaultSpaceAgent1]: defaultSpacePolicy1.item.id, + [defaultSpaceAgent2]: defaultSpacePolicy2.item.id, + }); + agents = await apiClient.getAgents(TEST_SPACE_1); + agentPolicyIds = getAgentPolicyIds(agents); + expect(agentPolicyIds).to.eql({ + [testSpaceAgent1]: spaceTest1Policy1.item.id, + [testSpaceAgent2]: spaceTest1Policy2.item.id, + [testSpaceAgent3]: spaceTest1Policy1.item.id, + }); + + const res = await apiClient.bulkReassignAgents( + { + agents: `not fleet-agents.policy_id:"${spaceTest1Policy2.item.id}"`, + policy_id: spaceTest1Policy2.item.id, + batchSize: 1, + }, + TEST_SPACE_1 + ); + + const verifyActionResult = async () => { + const { body: result } = await supertest + .get(`/s/${TEST_SPACE_1}/api/fleet/agents`) + .set('kbn-xsrf', 'xxx'); + expect(result.total).to.eql(3); + result.items.forEach((agent: any) => { + expect(agent.policy_id).to.eql(spaceTest1Policy2.item.id); + }); + }; + + await new Promise((resolve, reject) => { + let attempts = 0; + const intervalId = setInterval(async () => { + if (attempts > 20) { + clearInterval(intervalId); + reject(new Error('action timed out')); + } + ++attempts; + const { + body: { items: actionStatuses }, + } = await supertest + .get(`/s/${TEST_SPACE_1}/api/fleet/agents/action_status`) + .set('kbn-xsrf', 'xxx'); + + const action = actionStatuses.find((a: any) => a.actionId === res.actionId); + if (action && action.nbAgentsActioned === action.nbAgentsActionCreated) { + clearInterval(intervalId); + await verifyActionResult(); + resolve({}); + } + }, 1000); + }).catch((e) => { + throw e; + }); + }); + }); }); } diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts b/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts index 58f372ac0d7ee7..0695dd8868d4a4 100644 --- a/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts @@ -260,6 +260,38 @@ export class SpaceTestApiClient { return res; } + async reassignAgent(agentId: string, policyId: string, spaceId?: string) { + await this.supertest + .post(`${this.getBaseUrl(spaceId)}/api/fleet/agents/${agentId}/reassign`) + .set('kbn-xsrf', 'xxx') + .send({ + policy_id: policyId, + }) + .expect(200); + } + async bulkReassignAgents(data: any, spaceId?: string) { + const { body: res } = await this.supertest + .post(`${this.getBaseUrl(spaceId)}/api/fleet/agents/bulk_reassign`) + .set('kbn-xsrf', 'xxxx') + .send(data) + .expect(200); + + return res; + } + async upgradeAgent(agentId: string, data: any, spaceId?: string) { + await this.supertest + .post(`${this.getBaseUrl(spaceId)}/api/fleet/agents/${agentId}/upgrade`) + .set('kbn-xsrf', 'xxxx') + .send(data) + .expect(200); + } + async bulkUpgradeAgents(data: any, spaceId?: string) { + await this.supertest + .post(`${this.getBaseUrl(spaceId)}/api/fleet/agents/bulk_upgrade`) + .set('kbn-xsrf', 'xxxx') + .send(data) + .expect(200); + } async bulkUpdateAgentTags(data: any, spaceId?: string) { const { body: res } = await this.supertest .post(`${this.getBaseUrl(spaceId)}/api/fleet/agents/bulk_update_agent_tags`) @@ -366,7 +398,6 @@ export class SpaceTestApiClient { return res; } - async postNewAgentAction(agentId: string, spaceId?: string): Promise { const { body: res } = await this.supertest .post(`${this.getBaseUrl(spaceId)}/api/fleet/agents/${agentId}/actions`) @@ -376,6 +407,14 @@ export class SpaceTestApiClient { return res; } + + async cancelAction(actionId: string, spaceId?: string): Promise { + const { body: res } = await this.supertest + .post(`${this.getBaseUrl(spaceId)}/api/fleet/agents/actions/${actionId}/cancel`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + return res; + } // Enable space awareness async postEnableSpaceAwareness(spaceId?: string): Promise { const { body: res } = await this.supertest diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/helpers.ts b/x-pack/test/fleet_api_integration/apis/space_awareness/helpers.ts index c54291dc588a32..a82bf55c352a02 100644 --- a/x-pack/test/fleet_api_integration/apis/space_awareness/helpers.ts +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/helpers.ts @@ -15,6 +15,7 @@ import { AGENT_POLICY_INDEX, } from '@kbn/fleet-plugin/common'; import { ENROLLMENT_API_KEYS_INDEX } from '@kbn/fleet-plugin/common/constants'; +import { asyncForEach } from '@kbn/std'; const ES_INDEX_OPTIONS = { headers: { 'X-elastic-product-origin': 'fleet' } }; @@ -50,6 +51,15 @@ export async function cleanFleetIndices(esClient: Client) { ]); } +export async function cleanFleetAgents(esClient: Client) { + await esClient.deleteByQuery({ + index: AGENTS_INDEX, + q: '*', + ignore_unavailable: true, + refresh: true, + }); +} + export async function cleanFleetActionIndices(esClient: Client) { try { await Promise.all([ @@ -78,11 +88,7 @@ export async function cleanFleetActionIndices(esClient: Client) { } } -export const createFleetAgent = async ( - esClient: Client, - agentPolicyId: string, - spaceId?: string -) => { +export async function createFleetAgent(esClient: Client, agentPolicyId: string, spaceId?: string) { const agentResponse = await esClient.index({ index: '.fleet-agents', refresh: true, @@ -106,4 +112,19 @@ export const createFleetAgent = async ( }); return agentResponse._id; -}; +} + +export async function makeAgentsUpgradeable(esClient: Client, agentIds: string[], version: string) { + await asyncForEach(agentIds, async (agentId) => { + await esClient.update({ + id: agentId, + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + local_metadata: { elastic: { agent: { upgradeable: true, version } } }, + }, + }, + }); + }); +} From c2933dee94339f5eca939802f5c1f4a9326c7a70 Mon Sep 17 00:00:00 2001 From: seanrathier Date: Mon, 19 Aug 2024 10:22:45 -0400 Subject: [PATCH 04/16] [Cloud Security] [Agentless] [Serverless] Enable Serverless projects to transition to using Agentless API solution in Kibana (#190371) --- .../ftr_security_serverless_configs.yml | 1 + .../package_policy_input_panel.test.tsx | 8 +- .../hooks/setup_technology.test.ts | 78 +++++++-------- .../hooks/setup_technology.ts | 29 +++--- .../routes/package_policy/utils/index.ts | 4 +- .../services/agents/agentless_agent.test.ts | 2 +- .../server/services/agents/agentless_agent.ts | 8 +- .../server/services/preconfiguration.test.ts | 1 + .../fleet/server/services/preconfiguration.ts | 4 +- .../server/services/utils/agentless.test.ts | 52 +++++++--- .../fleet/server/services/utils/agentless.ts | 18 ++-- ...ig.cloud_security_posture.agentless_api.ts | 34 +++++++ .../agentless_api/create_agent.ts | 97 +++++++++++++++++++ .../agentless_api/index.ts | 15 +++ .../agentless_api/mock_agentless_api.ts | 28 ++++++ 15 files changed, 289 insertions(+), 90 deletions(-) create mode 100644 x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.agentless_api.ts create mode 100644 x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/create_agent.ts create mode 100644 x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/index.ts create mode 100644 x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/mock_agentless_api.ts diff --git a/.buildkite/ftr_security_serverless_configs.yml b/.buildkite/ftr_security_serverless_configs.yml index 4c3b037ce9f8a9..9f54a402a8bbfe 100644 --- a/.buildkite/ftr_security_serverless_configs.yml +++ b/.buildkite/ftr_security_serverless_configs.yml @@ -35,6 +35,7 @@ enabled: - x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.basic.ts - x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.essentials.ts - x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.agentless.ts + - x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.agentless_api.ts - x-pack/test_serverless/functional/test_suites/security/config.saved_objects_management.ts - x-pack/test_serverless/functional/test_suites/security/config.context_awareness.ts - x-pack/test_serverless/functional/test_suites/security/common_configs/config.group1.ts diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.test.tsx index c889dc862bf9e4..5accdf37e95e77 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.test.tsx @@ -363,8 +363,8 @@ describe('PackagePolicyInputPanel', () => { isAgentlessPackagePolicy: jest.fn(), isAgentlessAgentPolicy: jest.fn(), isAgentlessIntegration: jest.fn(), - isAgentlessCloudEnabled: true, - isAgentlessServerlessEnabled: false, + isAgentlessApiEnabled: true, + isDefaultAgentlessPolicyEnabled: false, }); }); @@ -398,8 +398,8 @@ describe('PackagePolicyInputPanel', () => { isAgentlessPackagePolicy: jest.fn(), isAgentlessAgentPolicy: jest.fn(), isAgentlessIntegration: jest.fn(), - isAgentlessCloudEnabled: true, - isAgentlessServerlessEnabled: false, + isAgentlessApiEnabled: true, + isDefaultAgentlessPolicyEnabled: false, }); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts index f32ea1cd007cee..be7884aad753d7 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts @@ -49,37 +49,18 @@ describe('useAgentless', () => { jest.clearAllMocks(); }); - it('should should not return return isAgentless when agentless is not enabled', () => { + it('should not return isAgentless when agentless is not enabled', () => { const { result } = renderHook(() => useAgentless()); expect(result.current.isAgentlessEnabled).toBeFalsy(); - expect(result.current.isAgentlessCloudEnabled).toBeFalsy(); - expect(result.current.isAgentlessServerlessEnabled).toBeFalsy(); - }); - it('should should return agentlessAPIUrl when agentless config is set', () => { - const agentlessAPIUrl = 'https://agentless.api.url'; - (useConfig as MockFn).mockReturnValue({ - agentless: { - api: { - url: agentlessAPIUrl, - }, - }, - } as any); - - const { result } = renderHook(() => useAgentless()); - - expect(result.current.isAgentlessEnabled).toBeFalsy(); - expect(result.current.isAgentlessCloudEnabled).toBeFalsy(); - expect(result.current.isAgentlessServerlessEnabled).toBeFalsy(); + expect(result.current.isAgentlessApiEnabled).toBeFalsy(); + expect(result.current.isDefaultAgentlessPolicyEnabled).toBeFalsy(); }); - it('should return isAgentlessEnabled as falsy if agentlessAPIUrl and experimental feature agentless is truthy without cloud or serverless', () => { - const agentlessAPIUrl = 'https://agentless.api.url'; + it('should return isAgentlessEnabled as falsy if agentless.enabled is true and experimental feature agentless is truthy without cloud or serverless', () => { (useConfig as MockFn).mockReturnValue({ agentless: { - api: { - url: agentlessAPIUrl, - }, + enabled: true, }, } as any); @@ -90,18 +71,14 @@ describe('useAgentless', () => { const { result } = renderHook(() => useAgentless()); expect(result.current.isAgentlessEnabled).toBeFalsy(); - expect(result.current.isAgentlessCloudEnabled).toBeFalsy(); - expect(result.current.isAgentlessServerlessEnabled).toBeFalsy(); + expect(result.current.isAgentlessApiEnabled).toBeFalsy(); + expect(result.current.isDefaultAgentlessPolicyEnabled).toBeFalsy(); }); - it('should return isAgentlessEnabled and isAgentlessCloudEnabled as truthy with isCloudEnabled', () => { - const agentlessAPIUrl = 'https://agentless.api.url'; + it('should return isAgentlessEnabled and isAgentlessApiEnabled as truthy with isCloudEnabled', () => { (useConfig as MockFn).mockReturnValue({ agentless: { enabled: true, - api: { - url: agentlessAPIUrl, - }, }, } as any); @@ -115,19 +92,10 @@ describe('useAgentless', () => { const { result } = renderHook(() => useAgentless()); expect(result.current.isAgentlessEnabled).toBeTruthy(); - expect(result.current.isAgentlessCloudEnabled).toBeTruthy(); - expect(result.current.isAgentlessServerlessEnabled).toBeFalsy(); + expect(result.current.isAgentlessApiEnabled).toBeTruthy(); + expect(result.current.isDefaultAgentlessPolicyEnabled).toBeFalsy(); }); - it('should return isAgentlessEnabled and isAgentlessServerlessEnabled as truthy with isServerlessEnabled', () => { - const agentlessAPIUrl = 'https://agentless.api.url'; - (useConfig as MockFn).mockReturnValue({ - agentless: { - api: { - url: agentlessAPIUrl, - }, - }, - } as any); - + it('should return isAgentlessEnabled and isDefaultAgentlessPolicyEnabled as truthy with isServerlessEnabled and experimental feature agentless is truthy', () => { mockedExperimentalFeaturesService.get.mockReturnValue({ agentless: true, } as any); @@ -142,8 +110,27 @@ describe('useAgentless', () => { const { result } = renderHook(() => useAgentless()); expect(result.current.isAgentlessEnabled).toBeTruthy(); - expect(result.current.isAgentlessCloudEnabled).toBeFalsy(); - expect(result.current.isAgentlessServerlessEnabled).toBeTruthy(); + expect(result.current.isAgentlessApiEnabled).toBeFalsy(); + expect(result.current.isDefaultAgentlessPolicyEnabled).toBeTruthy(); + }); + + it('should return isAgentlessEnabled as falsy and isDefaultAgentlessPolicyEnabled as falsy with isServerlessEnabled and experimental feature agentless is falsy', () => { + mockedExperimentalFeaturesService.get.mockReturnValue({ + agentless: false, + } as any); + + (useStartServices as MockFn).mockReturnValue({ + cloud: { + isServerlessEnabled: true, + isCloudEnabled: false, + }, + }); + + const { result } = renderHook(() => useAgentless()); + + expect(result.current.isAgentlessEnabled).toBeFalsy(); + expect(result.current.isAgentlessApiEnabled).toBeFalsy(); + expect(result.current.isDefaultAgentlessPolicyEnabled).toBeFalsy(); }); }); @@ -224,6 +211,7 @@ describe('useSetupTechnology', () => { it('should set agentless setup technology if agent policy supports agentless in edit page', async () => { (useConfig as MockFn).mockReturnValue({ agentless: { + enabled: true, api: { url: 'https://agentless.api.url', }, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts index 7fc159a2d43481..5cafedee7db7b2 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts @@ -29,10 +29,11 @@ export const useAgentless = () => { const isServerless = !!cloud?.isServerlessEnabled; const isCloud = !!cloud?.isCloudEnabled; - const isAgentlessCloudEnabled = isCloud && !!config.agentless?.enabled; - const isAgentlessServerlessEnabled = isServerless && agentlessExperimentalFeatureEnabled; + const isAgentlessApiEnabled = (isCloud || isServerless) && config.agentless?.enabled; + const isDefaultAgentlessPolicyEnabled = + !isAgentlessApiEnabled && isServerless && agentlessExperimentalFeatureEnabled; - const isAgentlessEnabled = isAgentlessCloudEnabled || isAgentlessServerlessEnabled; + const isAgentlessEnabled = isAgentlessApiEnabled || isDefaultAgentlessPolicyEnabled; const isAgentlessAgentPolicy = (agentPolicy: AgentPolicy | undefined) => { if (!agentPolicy) return false; @@ -62,8 +63,8 @@ export const useAgentless = () => { return isAgentlessEnabled && packagePolicy.policy_ids.includes(AGENTLESS_POLICY_ID); }; return { - isAgentlessCloudEnabled, - isAgentlessServerlessEnabled, + isAgentlessApiEnabled, + isDefaultAgentlessPolicyEnabled, isAgentlessEnabled, isAgentlessAgentPolicy, isAgentlessIntegration, @@ -90,7 +91,7 @@ export function useSetupTechnology({ isEditPage?: boolean; agentPolicies?: AgentPolicy[]; }) { - const { isAgentlessEnabled, isAgentlessCloudEnabled, isAgentlessServerlessEnabled } = + const { isAgentlessEnabled, isAgentlessApiEnabled, isDefaultAgentlessPolicyEnabled } = useAgentless(); // this is a placeholder for the new agent-BASED policy that will be used when the user switches from agentless to agent-based and back @@ -110,7 +111,7 @@ export function useSetupTechnology({ setSelectedSetupTechnology(SetupTechnology.AGENTLESS); return; } - if (isAgentlessCloudEnabled && selectedSetupTechnology === SetupTechnology.AGENTLESS) { + if (isAgentlessApiEnabled && selectedSetupTechnology === SetupTechnology.AGENTLESS) { const nextNewAgentlessPolicy = { ...newAgentlessPolicy, name: getAgentlessAgentPolicyNameFromPackagePolicyName(packagePolicy.name), @@ -122,7 +123,7 @@ export function useSetupTechnology({ } } }, [ - isAgentlessCloudEnabled, + isAgentlessApiEnabled, isEditPage, newAgentlessPolicy, packagePolicy.name, @@ -145,10 +146,10 @@ export function useSetupTechnology({ } }; - if (isAgentlessServerlessEnabled) { + if (isDefaultAgentlessPolicyEnabled) { fetchAgentlessPolicy(); } - }, [isAgentlessServerlessEnabled]); + }, [isDefaultAgentlessPolicyEnabled]); const handleSetupTechnologyChange = useCallback( (setupTechnology: SetupTechnology) => { @@ -157,14 +158,14 @@ export function useSetupTechnology({ } if (setupTechnology === SetupTechnology.AGENTLESS) { - if (isAgentlessCloudEnabled) { + if (isAgentlessApiEnabled) { setNewAgentPolicy(newAgentlessPolicy as NewAgentPolicy); setSelectedPolicyTab(SelectedPolicyTab.NEW); updateAgentPolicies([newAgentlessPolicy] as AgentPolicy[]); } // tech debt: remove this when Serverless uses the Agentless API // https://github.com/elastic/security-team/issues/9781 - if (isAgentlessServerlessEnabled) { + if (isDefaultAgentlessPolicyEnabled) { setNewAgentPolicy(newAgentlessPolicy as AgentPolicy); updateAgentPolicies([newAgentlessPolicy] as AgentPolicy[]); setSelectedPolicyTab(SelectedPolicyTab.EXISTING); @@ -183,8 +184,8 @@ export function useSetupTechnology({ [ isAgentlessEnabled, selectedSetupTechnology, - isAgentlessCloudEnabled, - isAgentlessServerlessEnabled, + isAgentlessApiEnabled, + isDefaultAgentlessPolicyEnabled, setNewAgentPolicy, newAgentlessPolicy, setSelectedPolicyTab, diff --git a/x-pack/plugins/fleet/server/routes/package_policy/utils/index.ts b/x-pack/plugins/fleet/server/routes/package_policy/utils/index.ts index 032dab4a07acc7..518d8fc3f74c89 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/utils/index.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/utils/index.ts @@ -11,7 +11,7 @@ import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-ser import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; -import { isAgentlessCloudEnabled } from '../../../services/utils/agentless'; +import { isAgentlessApiEnabled } from '../../../services/utils/agentless'; import { getAgentlessAgentPolicyNameFromPackagePolicyName } from '../../../../common/services/agentless_policy_helper'; @@ -65,7 +65,7 @@ export async function renameAgentlessAgentPolicy( packagePolicy: PackagePolicy, name: string ) { - if (!isAgentlessCloudEnabled()) { + if (!isAgentlessApiEnabled()) { return; } // If agentless is enabled for cloud, we need to rename the agent policy diff --git a/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts b/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts index dc057fd962c9b5..00a1ff4ff61cf9 100644 --- a/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts @@ -111,7 +111,7 @@ describe('Agentless Agent service', () => { namespace: 'default', supports_agentless: true, } as AgentPolicy) - ).rejects.toThrowError(new AgentlessAgentCreateError('Agentless agent not supported')); + ).rejects.toThrowError(new AgentlessAgentCreateError('missing agentless configuration')); }); it('should throw AgentlessAgentCreateError if agentless configuration is not found', async () => { diff --git a/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts b/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts index 21d3a6c8df73dc..627bdf38b8fe22 100644 --- a/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts +++ b/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts @@ -22,7 +22,7 @@ import { appContextService } from '../app_context'; import { listEnrollmentApiKeys } from '../api_keys'; import { listFleetServerHosts } from '../fleet_server_host'; -import { prependAgentlessApiBasePathToEndpoint } from '../utils/agentless'; +import { prependAgentlessApiBasePathToEndpoint, isAgentlessApiEnabled } from '../utils/agentless'; class AgentlessAgentService { public async createAgentlessAgent( @@ -33,8 +33,10 @@ class AgentlessAgentService { const logger = appContextService.getLogger(); logger.debug(`Creating agentless agent ${agentlessAgentPolicy.id}`); - if (!appContextService.getCloud()?.isCloudEnabled) { - logger.error('Creating agentless agent not supported in non-cloud environments'); + if (!isAgentlessApiEnabled) { + logger.error( + 'Creating agentless agent not supported in non-cloud or non-serverless environments' + ); throw new AgentlessAgentCreateError('Agentless agent not supported'); } if (!agentlessAgentPolicy.supports_agentless) { diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts index 802edd93e05435..36b6d4fdbeb17a 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts @@ -309,6 +309,7 @@ jest.mock('./app_context', () => ({ getExperimentalFeatures: jest.fn().mockReturnValue({ agentless: false, }), + getConfig: jest.fn(), getInternalUserSOClientForSpaceId: jest.fn(), }, })); diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts index b12adbe4a2437c..853961f2fd77a2 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts @@ -43,7 +43,7 @@ import { type InputsOverride, packagePolicyService } from './package_policy'; import { preconfigurePackageInputs } from './package_policy'; import { appContextService } from './app_context'; import type { UpgradeManagedPackagePoliciesResult } from './managed_package_policies'; -import { isAgentlessServerlessEnabled } from './utils/agentless'; +import { isDefaultAgentlessPolicyEnabled } from './utils/agentless'; interface PreconfigurationResult { policies: Array<{ id: string; updated_at: string }>; @@ -164,7 +164,7 @@ export async function ensurePreconfiguredPackagesAndPolicies( } if ( - !isAgentlessServerlessEnabled() && + !isDefaultAgentlessPolicyEnabled() && preconfiguredAgentPolicy?.supports_agentless !== undefined ) { throw new FleetError( diff --git a/x-pack/plugins/fleet/server/services/utils/agentless.test.ts b/x-pack/plugins/fleet/server/services/utils/agentless.test.ts index 4a1bbdd5f7d845..5bf5116128d94c 100644 --- a/x-pack/plugins/fleet/server/services/utils/agentless.test.ts +++ b/x-pack/plugins/fleet/server/services/utils/agentless.test.ts @@ -10,9 +10,9 @@ import { securityMock } from '@kbn/security-plugin/server/mocks'; import { appContextService } from '../app_context'; import { - isAgentlessCloudEnabled, + isAgentlessApiEnabled, isAgentlessEnabled, - isAgentlessServerlessEnabled, + isDefaultAgentlessPolicyEnabled, prependAgentlessApiBasePathToEndpoint, } from './agentless'; @@ -23,9 +23,10 @@ mockedAppContextService.getSecuritySetup.mockImplementation(() => ({ ...securityMock.createSetup(), })); -describe('isAgentlessCloudEnabled', () => { +describe('isAgentlessApiEnabled', () => { afterEach(() => { jest.clearAllMocks(); + mockedAppContextService.getConfig.mockReset(); }); it('should return false if cloud is not enabled', () => { jest.spyOn(appContextService, 'getConfig').mockReturnValue({ @@ -35,7 +36,7 @@ describe('isAgentlessCloudEnabled', () => { } as any); jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: false } as any); - expect(isAgentlessCloudEnabled()).toBe(false); + expect(isAgentlessApiEnabled()).toBe(false); }); it('should return false if cloud is enabled but agentless is not', () => { @@ -46,7 +47,7 @@ describe('isAgentlessCloudEnabled', () => { } as any); jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); - expect(isAgentlessCloudEnabled()).toBe(false); + expect(isAgentlessApiEnabled()).toBe(false); }); it('should return true if cloud is enabled and agentless is enabled', () => { @@ -57,13 +58,14 @@ describe('isAgentlessCloudEnabled', () => { } as any); jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); - expect(isAgentlessCloudEnabled()).toBe(true); + expect(isAgentlessApiEnabled()).toBe(true); }); }); -describe('isAgentlessServerlessEnabled', () => { +describe('isDefaultAgentlessPolicyEnabled', () => { afterEach(() => { jest.clearAllMocks(); + mockedAppContextService.getConfig.mockReset(); }); it('should return false if serverless is not enabled', () => { @@ -74,7 +76,7 @@ describe('isAgentlessServerlessEnabled', () => { .spyOn(appContextService, 'getCloud') .mockReturnValue({ isServerlessEnabled: false } as any); - expect(isAgentlessServerlessEnabled()).toBe(false); + expect(isDefaultAgentlessPolicyEnabled()).toBe(false); }); it('should return false if serverless is enabled but agentless is not', () => { @@ -83,7 +85,7 @@ describe('isAgentlessServerlessEnabled', () => { .mockReturnValue({ agentless: false } as any); jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isServerlessEnabled: true } as any); - expect(isAgentlessServerlessEnabled()).toBe(false); + expect(isDefaultAgentlessPolicyEnabled()).toBe(false); }); it('should return true if serverless is enabled and agentless is enabled', () => { @@ -92,13 +94,14 @@ describe('isAgentlessServerlessEnabled', () => { .mockReturnValue({ agentless: true } as any); jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isServerlessEnabled: true } as any); - expect(isAgentlessServerlessEnabled()).toBe(true); + expect(isDefaultAgentlessPolicyEnabled()).toBe(true); }); }); describe('isAgentlessEnabled', () => { afterEach(() => { jest.clearAllMocks(); + mockedAppContextService.getConfig.mockReset(); }); it('should return false if cloud and serverless are not enabled', () => { @@ -138,8 +141,8 @@ describe('isAgentlessEnabled', () => { it('should return true if cloud is enabled and agentless is enabled', () => { jest - .spyOn(appContextService, 'getExperimentalFeatures') - .mockReturnValue({ agentless: true } as any); + .spyOn(appContextService, 'getConfig') + .mockReturnValue({ agentless: { enabled: true } } as any); jest .spyOn(appContextService, 'getCloud') .mockReturnValue({ isCloudEnabled: true, isServerlessEnabled: false } as any); @@ -163,7 +166,10 @@ describe('prependAgentlessApiBasePathToEndpoint', () => { jest.clearAllMocks(); }); - it('should prepend the agentless api base path to the endpoint', () => { + it('should prepend the agentless api base path to the endpoint with ess if in cloud', () => { + jest + .spyOn(appContextService, 'getCloud') + .mockReturnValue({ isCloudEnabled: true, isServerlessEnabled: false } as any); const agentlessConfig = { api: { url: 'https://agentless-api.com', @@ -176,7 +182,27 @@ describe('prependAgentlessApiBasePathToEndpoint', () => { ); }); + it('should prepend the agentless api base path to the endpoint with serverless if in serverless', () => { + jest + .spyOn(appContextService, 'getCloud') + .mockReturnValue({ isCloudEnabled: false, isServerlessEnabled: true } as any); + const agentlessConfig = { + api: { + url: 'https://agentless-api.com', + }, + } as any; + const endpoint = '/deployments'; + + expect(prependAgentlessApiBasePathToEndpoint(agentlessConfig, endpoint)).toBe( + 'https://agentless-api.com/api/v1/serverless/deployments' + ); + }); + it('should prepend the agentless api base path to the endpoint with a dynamic path', () => { + jest + .spyOn(appContextService, 'getCloud') + .mockReturnValue({ isCloudEnabled: true, isServerlessEnabled: false } as any); + const agentlessConfig = { api: { url: 'https://agentless-api.com', diff --git a/x-pack/plugins/fleet/server/services/utils/agentless.ts b/x-pack/plugins/fleet/server/services/utils/agentless.ts index d54ea2bb3d00b5..5c544b1907b251 100644 --- a/x-pack/plugins/fleet/server/services/utils/agentless.ts +++ b/x-pack/plugins/fleet/server/services/utils/agentless.ts @@ -8,21 +8,23 @@ import { appContextService } from '..'; import type { FleetConfigType } from '../../config'; -export const isAgentlessCloudEnabled = () => { +export const isAgentlessApiEnabled = () => { const cloudSetup = appContextService.getCloud(); - return Boolean(cloudSetup?.isCloudEnabled && appContextService.getConfig()?.agentless?.enabled); + const isHosted = cloudSetup?.isCloudEnabled || cloudSetup?.isServerlessEnabled; + return Boolean(isHosted && appContextService.getConfig()?.agentless?.enabled); }; -export const isAgentlessServerlessEnabled = () => { +export const isDefaultAgentlessPolicyEnabled = () => { const cloudSetup = appContextService.getCloud(); return Boolean( cloudSetup?.isServerlessEnabled && appContextService.getExperimentalFeatures().agentless ); }; export const isAgentlessEnabled = () => { - return isAgentlessCloudEnabled() || isAgentlessServerlessEnabled(); + return isAgentlessApiEnabled() || isDefaultAgentlessPolicyEnabled(); }; -const AGENTLESS_API_BASE_PATH = '/api/v1/ess'; +const AGENTLESS_ESS_API_BASE_PATH = '/api/v1/ess'; +const AGENTLESS_SERVERLESS_API_BASE_PATH = '/api/v1/serverless'; type AgentlessApiEndpoints = '/deployments' | `/deployments/${string}`; @@ -30,5 +32,9 @@ export const prependAgentlessApiBasePathToEndpoint = ( agentlessConfig: FleetConfigType['agentless'], endpoint: AgentlessApiEndpoints ) => { - return `${agentlessConfig.api.url}${AGENTLESS_API_BASE_PATH}${endpoint}`; + const cloudSetup = appContextService.getCloud(); + const endpointPrefix = cloudSetup?.isServerlessEnabled + ? AGENTLESS_SERVERLESS_API_BASE_PATH + : AGENTLESS_ESS_API_BASE_PATH; + return `${agentlessConfig.api.url}${endpointPrefix}${endpoint}`; }; diff --git a/x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.agentless_api.ts b/x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.agentless_api.ts new file mode 100644 index 00000000000000..0f37f224197ef5 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.agentless_api.ts @@ -0,0 +1,34 @@ +/* + * 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 { CLOUD_CREDENTIALS_PACKAGE_VERSION } from '@kbn/cloud-security-posture-plugin/common/constants'; +import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; +import { createTestConfig } from '../../config.base'; + +export default createTestConfig({ + serverlessProject: 'security', + junit: { + reportName: 'Serverless Security Cloud Security Agentless API Onboarding Functional Tests', + }, + kbnServerArgs: [ + `--xpack.fleet.packages.0.name=cloud_security_posture`, + `--xpack.fleet.packages.0.version=${CLOUD_CREDENTIALS_PACKAGE_VERSION}`, + + `--xpack.fleet.agents.fleet_server.hosts=["https://ftr.kibana:8220"]`, + `--xpack.fleet.internal.fleetServerStandalone=true`, + + // Agentless Configuration based on Serverless Security Dev Yaml - config/serverless.security.dev.yml + `--xpack.fleet.agentless.enabled=true`, + `--xpack.fleet.agentless.api.url=http://localhost:8089`, + `--xpack.fleet.agentless.api.tls.certificate=${KBN_CERT_PATH}`, + `--xpack.fleet.agentless.api.tls.key=${KBN_KEY_PATH}`, + `--xpack.fleet.agentless.api.tls.ca=${CA_CERT_PATH}`, + `--xpack.cloud.serverless.project_id=some_fake_project_id`, + ], + // load tests in the index file + testFiles: [require.resolve('./ftr/cloud_security_posture/agentless_api')], +}); diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/create_agent.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/create_agent.ts new file mode 100644 index 00000000000000..d2d797a5c107d7 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/create_agent.ts @@ -0,0 +1,97 @@ +/* + * 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 { CLOUD_CREDENTIALS_PACKAGE_VERSION } from '@kbn/cloud-security-posture-plugin/common/constants'; +import * as http from 'http'; +import expect from '@kbn/expect'; +import { setupMockServer } from './mock_agentless_api'; +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const mockAgentlessApiService = setupMockServer(); + const pageObjects = getPageObjects([ + 'svlCommonPage', + 'cspSecurity', + 'security', + 'header', + 'cisAddIntegration', + ]); + + const CIS_AWS_OPTION_TEST_ID = 'cisAwsTestId'; + + const AWS_SINGLE_ACCOUNT_TEST_ID = 'awsSingleTestId'; + + describe('Agentless API Serverless', function () { + let mockApiServer: http.Server; + let cisIntegration: typeof pageObjects.cisAddIntegration; + + before(async () => { + mockApiServer = mockAgentlessApiService.listen(8089); // Start the usage api mock server on port 8089 + await pageObjects.svlCommonPage.loginAsAdmin(); + cisIntegration = pageObjects.cisAddIntegration; + }); + + after(async () => { + mockApiServer.close(); + }); + + it(`should create agentless-agent`, async () => { + const integrationPolicyName = `cloud_security_posture-${new Date().toISOString()}`; + await cisIntegration.navigateToAddIntegrationCspmWithVersionPage( + CLOUD_CREDENTIALS_PACKAGE_VERSION + ); + + await cisIntegration.clickOptionButton(CIS_AWS_OPTION_TEST_ID); + await cisIntegration.clickOptionButton(AWS_SINGLE_ACCOUNT_TEST_ID); + + await cisIntegration.inputIntegrationName(integrationPolicyName); + + await cisIntegration.selectSetupTechnology('agentless'); + await cisIntegration.selectAwsCredentials('direct'); + + await pageObjects.header.waitUntilLoadingHasFinished(); + + await cisIntegration.clickSaveButton(); + await pageObjects.header.waitUntilLoadingHasFinished(); + + await cisIntegration.navigateToIntegrationCspList(); + await pageObjects.header.waitUntilLoadingHasFinished(); + + expect(await cisIntegration.getFirstCspmIntegrationPageIntegration()).to.be( + integrationPolicyName + ); + expect(await cisIntegration.getFirstCspmIntegrationPageAgent()).to.be( + `Agentless policy for ${integrationPolicyName}` + ); + }); + + it(`should create default agent-based agent`, async () => { + const integrationPolicyName = `cloud_security_posture-${new Date().toISOString()}`; + + await cisIntegration.navigateToAddIntegrationCspmWithVersionPage( + CLOUD_CREDENTIALS_PACKAGE_VERSION + ); + + await cisIntegration.clickOptionButton(CIS_AWS_OPTION_TEST_ID); + await cisIntegration.clickOptionButton(AWS_SINGLE_ACCOUNT_TEST_ID); + + await cisIntegration.inputIntegrationName(integrationPolicyName); + + await cisIntegration.clickSaveButton(); + await pageObjects.header.waitUntilLoadingHasFinished(); + + const agentPolicyName = await cisIntegration.getAgentBasedPolicyValue(); + + await cisIntegration.navigateToIntegrationCspList(); + await pageObjects.header.waitUntilLoadingHasFinished(); + + expect(await cisIntegration.getFirstCspmIntegrationPageIntegration()).to.be( + integrationPolicyName + ); + expect(await cisIntegration.getFirstCspmIntegrationPageAgent()).to.be(agentPolicyName); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/index.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/index.ts new file mode 100644 index 00000000000000..44aea818827d7b --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { FtrProviderContext } from '../../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('cloud_security_posture', function () { + this.tags(['cloud_security_posture_agentless']); + loadTestFile(require.resolve('./create_agent')); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/mock_agentless_api.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/mock_agentless_api.ts new file mode 100644 index 00000000000000..8688db0fc018f0 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/mock_agentless_api.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 { createServer } from '@mswjs/http-middleware'; + +import { http, HttpResponse, StrictResponse } from 'msw'; + +export const setupMockServer = () => { + const server = createServer(deploymentHandler); + return server; +}; + +interface AgentlessApiResponse { + status: number; +} + +const deploymentHandler = http.post( + 'api/v1/serverless/deployments', + async ({ request }): Promise> => { + return HttpResponse.json({ + status: 200, + }); + } +); From 35c0671414ea173ef982a0df6e0332e44ee3436f Mon Sep 17 00:00:00 2001 From: Philippe Oberti Date: Mon, 19 Aug 2024 17:00:45 +0200 Subject: [PATCH 05/16] [Security Solution][Entity details] - move prevalence related hooks to flyout folder (#190109) --- .../components/analyzer_preview.test.tsx | 4 +- .../right/components/analyzer_preview.tsx | 4 +- .../analyzer_preview_container.test.tsx | 4 +- .../components/insights_section.test.tsx | 4 +- .../visualizations_section.test.tsx | 4 +- .../right/mocks/mock_analyzer_data.ts | 2 +- .../right/utils/analyzer_helpers.ts | 2 +- ...se_alert_document_analyzer_schema.test.tsx | 102 ++++++++++++ .../use_alert_document_analyzer_schema.ts | 95 +++++++++++ .../hooks/use_alert_prevalence.test.tsx | 151 +++++++++++++++++ .../shared/hooks}/use_alert_prevalence.ts | 77 ++++++--- ...lert_prevalence_from_process_tree.test.tsx | 154 ++++++++++++++++++ .../use_alert_prevalence_from_process_tree.ts | 143 ++++++++-------- ..._fetch_related_alerts_by_ancestry.test.tsx | 4 +- .../use_fetch_related_alerts_by_ancestry.ts | 2 +- ...lated_alerts_by_same_source_event.test.tsx | 4 +- ...tch_related_alerts_by_same_source_event.ts | 4 +- ...e_fetch_related_alerts_by_session.test.tsx | 4 +- .../use_fetch_related_alerts_by_session.ts | 4 +- 19 files changed, 655 insertions(+), 113 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_document_analyzer_schema.test.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_document_analyzer_schema.ts create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence.test.tsx rename x-pack/plugins/security_solution/public/{common/containers/alerts => flyout/document_details/shared/hooks}/use_alert_prevalence.ts (66%) create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence_from_process_tree.test.tsx rename x-pack/plugins/security_solution/public/{common/containers/alerts => flyout/document_details/shared/hooks}/use_alert_prevalence_from_process_tree.ts (57%) diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.test.tsx index 67d7438e8bb68c..724eaf979d8fd0 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.test.tsx @@ -8,7 +8,7 @@ import { render } from '@testing-library/react'; import React from 'react'; import { TestProviders } from '../../../../common/mock'; -import { useAlertPrevalenceFromProcessTree } from '../../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; +import { useAlertPrevalenceFromProcessTree } from '../../shared/hooks/use_alert_prevalence_from_process_tree'; import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters'; import { mockContextValue } from '../../shared/mocks/mock_context'; import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser'; @@ -17,7 +17,7 @@ import { AnalyzerPreview } from './analyzer_preview'; import { ANALYZER_PREVIEW_TEST_ID } from './test_ids'; import * as mock from '../mocks/mock_analyzer_data'; -jest.mock('../../../../common/containers/alerts/use_alert_prevalence_from_process_tree', () => ({ +jest.mock('../../shared/hooks/use_alert_prevalence_from_process_tree', () => ({ useAlertPrevalenceFromProcessTree: jest.fn(), })); const mockUseAlertPrevalenceFromProcessTree = useAlertPrevalenceFromProcessTree as jest.Mock; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.tsx index efae023e0d0929..bbdcc4f8e3d6b9 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.tsx @@ -13,8 +13,8 @@ import { ANALYZER_PREVIEW_TEST_ID, ANALYZER_PREVIEW_LOADING_TEST_ID } from './te import { getTreeNodes } from '../utils/analyzer_helpers'; import { ANCESTOR_ID, RULE_INDICES } from '../../shared/constants/field_names'; import { useDocumentDetailsContext } from '../../shared/context'; -import { useAlertPrevalenceFromProcessTree } from '../../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; -import type { StatsNode } from '../../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; +import { useAlertPrevalenceFromProcessTree } from '../../shared/hooks/use_alert_prevalence_from_process_tree'; +import type { StatsNode } from '../../shared/hooks/use_alert_prevalence_from_process_tree'; import { isActiveTimeline } from '../../../../helpers'; import { getField } from '../../shared/utils'; import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview_container.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview_container.test.tsx index 5ce6fcebae76b2..7dae9400358c53 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview_container.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview_container.test.tsx @@ -13,7 +13,7 @@ import { mockContextValue } from '../../shared/mocks/mock_context'; import { AnalyzerPreviewContainer } from './analyzer_preview_container'; import { useIsInvestigateInResolverActionEnabled } from '../../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver'; import { ANALYZER_PREVIEW_TEST_ID } from './test_ids'; -import { useAlertPrevalenceFromProcessTree } from '../../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; +import { useAlertPrevalenceFromProcessTree } from '../../shared/hooks/use_alert_prevalence_from_process_tree'; import * as mock from '../mocks/mock_analyzer_data'; import { EXPANDABLE_PANEL_CONTENT_TEST_ID, @@ -28,7 +28,7 @@ import { useInvestigateInTimeline } from '../../../../detections/components/aler jest.mock( '../../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver' ); -jest.mock('../../../../common/containers/alerts/use_alert_prevalence_from_process_tree'); +jest.mock('../../shared/hooks/use_alert_prevalence_from_process_tree'); jest.mock( '../../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline' ); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_section.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_section.test.tsx index eb1af2a74b8df0..96dff8150e6547 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_section.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_section.test.tsx @@ -25,13 +25,13 @@ import { usePrevalence } from '../../shared/hooks/use_prevalence'; import { mockGetFieldsData } from '../../shared/mocks/mock_get_fields_data'; import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser'; import { InsightsSection } from './insights_section'; -import { useAlertPrevalence } from '../../../../common/containers/alerts/use_alert_prevalence'; +import { useAlertPrevalence } from '../../shared/hooks/use_alert_prevalence'; import { useRiskScore } from '../../../../entity_analytics/api/hooks/use_risk_score'; import { useExpandSection } from '../hooks/use_expand_section'; import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters'; import { useTourContext } from '../../../../common/components/guided_onboarding_tour'; -jest.mock('../../../../common/containers/alerts/use_alert_prevalence'); +jest.mock('../../shared/hooks/use_alert_prevalence'); const mockDispatch = jest.fn(); jest.mock('react-redux', () => { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx index 36fe53aa41deab..f204c18f9036ac 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx @@ -18,7 +18,7 @@ import { VisualizationsSection } from './visualizations_section'; import { mockContextValue } from '../../shared/mocks/mock_context'; import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser'; import { DocumentDetailsContext } from '../../shared/context'; -import { useAlertPrevalenceFromProcessTree } from '../../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; +import { useAlertPrevalenceFromProcessTree } from '../../shared/hooks/use_alert_prevalence_from_process_tree'; import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters'; import { TestProvider } from '@kbn/expandable-flyout/src/test/provider'; import { useExpandSection } from '../hooks/use_expand_section'; @@ -26,7 +26,7 @@ import { useInvestigateInTimeline } from '../../../../detections/components/aler import { useIsInvestigateInResolverActionEnabled } from '../../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver'; jest.mock('../hooks/use_expand_section'); -jest.mock('../../../../common/containers/alerts/use_alert_prevalence_from_process_tree', () => ({ +jest.mock('../../shared/hooks/use_alert_prevalence_from_process_tree', () => ({ useAlertPrevalenceFromProcessTree: jest.fn(), })); const mockUseAlertPrevalenceFromProcessTree = useAlertPrevalenceFromProcessTree as jest.Mock; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/mocks/mock_analyzer_data.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/mocks/mock_analyzer_data.ts index fbd7dea83f79dd..e0d35b796e76f0 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/mocks/mock_analyzer_data.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/mocks/mock_analyzer_data.ts @@ -7,7 +7,7 @@ import React from 'react'; import { EuiToken } from '@elastic/eui'; import type { Node } from '@elastic/eui/src/components/tree_view/tree_view'; -import type { StatsNode } from '../../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; +import type { StatsNode } from '../../shared/hooks/use_alert_prevalence_from_process_tree'; export const mockStatsNode: StatsNode = { id: '70e19mhyda', diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/utils/analyzer_helpers.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/utils/analyzer_helpers.ts index 15492f7e41377c..5db8665bc3bbc4 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/utils/analyzer_helpers.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/utils/analyzer_helpers.ts @@ -7,7 +7,7 @@ import React from 'react'; import type { Node } from '@elastic/eui/src/components/tree_view/tree_view'; import { EuiToken } from '@elastic/eui'; -import type { StatsNode } from '../../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; +import type { StatsNode } from '../../shared/hooks/use_alert_prevalence_from_process_tree'; /** * Helper function to recursively create ancestor tree nodes diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_document_analyzer_schema.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_document_analyzer_schema.test.tsx new file mode 100644 index 00000000000000..3c31720b53f99e --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_document_analyzer_schema.test.tsx @@ -0,0 +1,102 @@ +/* + * 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 type { RenderHookResult } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react-hooks'; +import { useQuery } from '@tanstack/react-query'; +import type { + UseAlertDocumentAnalyzerSchemaParams, + UseAlertDocumentAnalyzerSchemaResult, +} from './use_alert_document_analyzer_schema'; +import { useAlertDocumentAnalyzerSchema } from './use_alert_document_analyzer_schema'; +import { useHttp } from '../../../../common/lib/kibana'; + +jest.mock('../../../../common/lib/kibana'); +jest.mock('@tanstack/react-query'); + +describe('useAlertPrevalenceFromProcessTree', () => { + let hookResult: RenderHookResult< + UseAlertDocumentAnalyzerSchemaParams, + UseAlertDocumentAnalyzerSchemaResult + >; + + beforeEach(() => { + (useHttp as jest.Mock).mockReturnValue({ + get: jest.fn(), + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return all properties when loading', () => { + (useQuery as jest.Mock).mockReturnValue({ + isLoading: true, + data: [], + }); + + hookResult = renderHook(() => + useAlertDocumentAnalyzerSchema({ + documentId: 'documentId', + indices: [], + }) + ); + + expect(hookResult.result.current.loading).toEqual(true); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.id).toEqual(null); + expect(hookResult.result.current.schema).toEqual(null); + expect(hookResult.result.current.agentId).toEqual(null); + }); + + it('should return all properties with data', () => { + (useQuery as jest.Mock).mockReturnValue({ + isLoading: false, + data: [ + { + schema: {}, + id: 'id', + agentId: 'agentId', + }, + ], + }); + + hookResult = renderHook(() => + useAlertDocumentAnalyzerSchema({ + documentId: 'documentId', + indices: [], + }) + ); + + expect(hookResult.result.current.loading).toEqual(false); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.id).toEqual('id'); + expect(hookResult.result.current.schema).toEqual({}); + expect(hookResult.result.current.agentId).toEqual('agentId'); + }); + + it('should return error when no data', () => { + (useQuery as jest.Mock).mockReturnValue({ + isLoading: false, + data: [], + }); + + hookResult = renderHook(() => + useAlertDocumentAnalyzerSchema({ + documentId: 'documentId', + indices: [], + }) + ); + + expect(hookResult.result.current.loading).toEqual(false); + expect(hookResult.result.current.error).toEqual(true); + expect(hookResult.result.current.id).toEqual(null); + expect(hookResult.result.current.schema).toEqual(null); + expect(hookResult.result.current.agentId).toEqual(null); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_document_analyzer_schema.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_document_analyzer_schema.ts new file mode 100644 index 00000000000000..63cf63398bd16f --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_document_analyzer_schema.ts @@ -0,0 +1,95 @@ +/* + * 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 { useQuery } from '@tanstack/react-query'; +import { useHttp } from '../../../../common/lib/kibana'; + +interface EntityResponse { + id: string; + name: string; + schema: object; + agentId: string; +} + +export interface UseAlertDocumentAnalyzerSchemaParams { + /** + * The document ID of the alert to analyze + */ + documentId: string; + /** + * The indices to search for alerts + */ + indices: string[]; +} + +export interface UseAlertDocumentAnalyzerSchemaResult { + /** + * True if the request is still loading + */ + loading: boolean; + /** + * True if there was an error + */ + error: boolean; + /** + * The id returned by the API + */ + id: string | null; + /** + * The schema returned by the API + */ + schema: object | null; + /** + * The agent ID value returned byt the API + */ + agentId: string | null; +} + +export function useAlertDocumentAnalyzerSchema({ + documentId, + indices, +}: UseAlertDocumentAnalyzerSchemaParams): UseAlertDocumentAnalyzerSchemaResult { + const http = useHttp(); + + const query = useQuery(['getAlertPrevalenceSchema', documentId], () => { + return http.get(`/api/endpoint/resolver/entity`, { + query: { + _id: documentId, + indices, + }, + }); + }); + + if (query.isLoading) { + return { + loading: true, + error: false, + id: null, + schema: null, + agentId: null, + }; + } else if (query.data && query.data.length > 0) { + const { + data: [{ schema, id, agentId }], + } = query; + return { + loading: false, + error: false, + id, + schema, + agentId, + }; + } else { + return { + loading: false, + error: true, + id: null, + schema: null, + agentId: null, + }; + } +} diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence.test.tsx new file mode 100644 index 00000000000000..231e0e54194414 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence.test.tsx @@ -0,0 +1,151 @@ +/* + * 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 type { RenderHookResult } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react-hooks'; +import { ALERT_PREVALENCE_AGG, useAlertPrevalence } from './use_alert_prevalence'; +import type { UseAlertPrevalenceParams, UserAlertPrevalenceResult } from './use_alert_prevalence'; +import { useGlobalTime } from '../../../../common/containers/use_global_time'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +import { useQueryAlerts } from '../../../../detections/containers/detection_engine/alerts/use_query'; + +jest.mock('../../../../common/containers/use_global_time'); +jest.mock('../../../../common/hooks/use_selector'); +jest.mock('../../../../detections/containers/detection_engine/alerts/use_query'); + +describe('useAlertPrevalence', () => { + let hookResult: RenderHookResult; + + beforeEach(() => { + (useDeepEqualSelector as jest.Mock).mockReturnValue({ + from: 'from', + to: 'to', + }); + (useGlobalTime as jest.Mock).mockReturnValue({ + from: 'from', + to: 'to', + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return all properties', () => { + (useQueryAlerts as jest.Mock).mockReturnValue({ + loading: true, + data: undefined, + setQuery: jest.fn(), + }); + + hookResult = renderHook(() => + useAlertPrevalence({ + field: 'field', + value: 'value', + indexName: 'index', + isActiveTimelines: true, + includeAlertIds: false, + ignoreTimerange: false, + }) + ); + + expect(hookResult.result.current.loading).toEqual(true); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.alertIds).toEqual(undefined); + expect(hookResult.result.current.count).toEqual(undefined); + }); + + it('should return error true if loading is done and no data', () => { + (useQueryAlerts as jest.Mock).mockReturnValue({ + loading: false, + data: undefined, + setQuery: jest.fn(), + }); + + hookResult = renderHook(() => + useAlertPrevalence({ + field: 'field', + value: 'value', + indexName: 'index', + isActiveTimelines: true, + includeAlertIds: false, + ignoreTimerange: false, + }) + ); + + expect(hookResult.result.current.loading).toEqual(false); + expect(hookResult.result.current.error).toEqual(true); + expect(hookResult.result.current.alertIds).toEqual(undefined); + expect(hookResult.result.current.count).toEqual(undefined); + }); + + it('should return correct count from aggregation', () => { + (useQueryAlerts as jest.Mock).mockReturnValue({ + loading: false, + data: { + aggregations: { + [ALERT_PREVALENCE_AGG]: { + buckets: [{ doc_count: 1 }], + }, + }, + hits: { + hits: [], + }, + }, + setQuery: jest.fn(), + }); + + hookResult = renderHook(() => + useAlertPrevalence({ + field: 'field', + value: 'value', + indexName: 'index', + isActiveTimelines: true, + includeAlertIds: false, + ignoreTimerange: false, + }) + ); + + expect(hookResult.result.current.loading).toEqual(false); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.alertIds).toEqual([]); + expect(hookResult.result.current.count).toEqual(1); + }); + + it('should return alertIds if includeAlertIds is true', () => { + (useQueryAlerts as jest.Mock).mockReturnValue({ + loading: false, + data: { + aggregations: { + [ALERT_PREVALENCE_AGG]: { + buckets: [{ doc_count: 1 }], + }, + }, + hits: { + hits: [{ _id: 'id' }], + }, + }, + setQuery: jest.fn(), + }); + + hookResult = renderHook(() => + useAlertPrevalence({ + field: 'field', + value: 'value', + indexName: 'index', + isActiveTimelines: true, + includeAlertIds: true, + ignoreTimerange: false, + }) + ); + + expect(hookResult.result.current.loading).toEqual(false); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.alertIds).toEqual(['id']); + expect(hookResult.result.current.count).toEqual(1); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence.ts similarity index 66% rename from x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.ts rename to x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence.ts index cc3ff5507ec409..a68a462c0ec0af 100644 --- a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence.ts @@ -7,41 +7,80 @@ import { useEffect, useMemo, useState } from 'react'; -import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../common/constants'; -import { useGlobalTime } from '../use_global_time'; -import type { GenericBuckets } from '../../../../common/search_strategy'; -import { useQueryAlerts } from '../../../detections/containers/detection_engine/alerts/use_query'; -import { ALERTS_QUERY_NAMES } from '../../../detections/containers/detection_engine/alerts/constants'; -import { useDeepEqualSelector } from '../../hooks/use_selector'; -import { inputsSelectors } from '../../store'; - -const ALERT_PREVALENCE_AGG = 'countOfAlertsWithSameFieldAndValue'; +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../common/constants'; +import { useGlobalTime } from '../../../../common/containers/use_global_time'; +import type { GenericBuckets } from '../../../../../common/search_strategy'; +import { useQueryAlerts } from '../../../../detections/containers/detection_engine/alerts/use_query'; +import { ALERTS_QUERY_NAMES } from '../../../../detections/containers/detection_engine/alerts/constants'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +import { inputsSelectors } from '../../../../common/store'; + +export const ALERT_PREVALENCE_AGG = 'countOfAlertsWithSameFieldAndValue'; +interface AlertPrevalenceAggregation { + [ALERT_PREVALENCE_AGG]: { + buckets: GenericBuckets[]; + }; +} -interface UseAlertPrevalenceOptions { +export interface UseAlertPrevalenceParams { + /** + * The field to search for + */ field: string; + /** + * The value to search for + */ value: string | string[] | undefined | null; + /** + * The index to search in + */ + indexName: string | null; + /** + * Whether to use the timeline time or the global time + */ isActiveTimelines: boolean; - signalIndexName: string | null; + /** + * Whether to include the alert ids in the response + */ includeAlertIds?: boolean; + /** + * Whether to ignore the timeline time and use the global time + */ ignoreTimerange?: boolean; } -interface UserAlertPrevalenceResult { +export interface UserAlertPrevalenceResult { + /** + * Whether the query is loading + */ loading: boolean; + /** + * The count of the prevalence aggregation + */ count: undefined | number; + /** + * Whether there was an error with the query + */ error: boolean; + /** + * The alert ids sorted by timestamp + */ alertIds?: string[]; } -// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462 +/** + * Hook to get the prevalence of alerts with the field and value pair. + * By default, includeAlertIds is false, which means the call only returns the aggregation and not all the alerts themselves. If includeAlertIds is true, it will also return the alertIds sorted by timestamp. + * By default, includeAlertIds is false, which means we're fetching with the global time from and to values. If isActiveTimelines is true, we're getting the timeline time. + */ export const useAlertPrevalence = ({ field, value, + indexName, isActiveTimelines, - signalIndexName, includeAlertIds = false, ignoreTimerange = false, -}: UseAlertPrevalenceOptions): UserAlertPrevalenceResult => { +}: UseAlertPrevalenceParams): UserAlertPrevalenceResult => { const timelineTime = useDeepEqualSelector((state) => inputsSelectors.timelineTimeRangeSelector(state) ); @@ -57,7 +96,7 @@ export const useAlertPrevalence = ({ const { loading, data, setQuery } = useQueryAlerts<{ _id: string }, AlertPrevalenceAggregation>({ query: initialQuery, - indexName: signalIndexName, + indexName, queryName: ALERTS_QUERY_NAMES.PREVALENCE, }); @@ -165,9 +204,3 @@ const generateAlertPrevalenceQuery = ( runtime_mappings: {}, }; }; - -export interface AlertPrevalenceAggregation { - [ALERT_PREVALENCE_AGG]: { - buckets: GenericBuckets[]; - }; -} diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence_from_process_tree.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence_from_process_tree.test.tsx new file mode 100644 index 00000000000000..94b7cfa6235077 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence_from_process_tree.test.tsx @@ -0,0 +1,154 @@ +/* + * 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 type { RenderHookResult } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react-hooks'; +import type { + UseAlertPrevalenceFromProcessTreeParams, + UserAlertPrevalenceFromProcessTreeResult, +} from './use_alert_prevalence_from_process_tree'; +import { useAlertPrevalenceFromProcessTree } from './use_alert_prevalence_from_process_tree'; +import { useHttp } from '../../../../common/lib/kibana'; +import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters'; +import { useQuery } from '@tanstack/react-query'; +import { useAlertDocumentAnalyzerSchema } from './use_alert_document_analyzer_schema'; +import { mockStatsNode } from '../../right/mocks/mock_analyzer_data'; + +jest.mock('../../../../common/lib/kibana'); +jest.mock('../../../../timelines/containers/use_timeline_data_filters'); +jest.mock('./use_alert_document_analyzer_schema'); +jest.mock('@tanstack/react-query'); + +describe('useAlertPrevalenceFromProcessTree', () => { + let hookResult: RenderHookResult< + UseAlertPrevalenceFromProcessTreeParams, + UserAlertPrevalenceFromProcessTreeResult + >; + + beforeEach(() => { + (useHttp as jest.Mock).mockReturnValue({ + post: jest.fn(), + }); + (useTimelineDataFilters as jest.Mock).mockReturnValue({ + selectedPatterns: [], + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return all properties when query is loading', () => { + (useQuery as jest.Mock).mockReturnValue({ + isLoading: true, + data: {}, + }); + (useAlertDocumentAnalyzerSchema as jest.Mock).mockReturnValue({ + loading: false, + error: false, + id: null, + schema: null, + agentId: null, + }); + + hookResult = renderHook(() => + useAlertPrevalenceFromProcessTree({ + documentId: 'documentId', + isActiveTimeline: true, + indices: [], + }) + ); + + expect(hookResult.result.current.loading).toEqual(true); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.alertIds).toEqual(undefined); + expect(hookResult.result.current.statsNodes).toEqual(undefined); + }); + + it('should return all properties when analyzer query is loading', () => { + (useQuery as jest.Mock).mockReturnValue({ + isLoading: false, + data: {}, + }); + (useAlertDocumentAnalyzerSchema as jest.Mock).mockReturnValue({ + loading: true, + error: false, + id: null, + schema: null, + agentId: null, + }); + + hookResult = renderHook(() => + useAlertPrevalenceFromProcessTree({ + documentId: 'documentId', + isActiveTimeline: true, + indices: [], + }) + ); + + expect(hookResult.result.current.loading).toEqual(true); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.alertIds).toEqual(undefined); + expect(hookResult.result.current.statsNodes).toEqual(undefined); + }); + + it('should return all properties data exists', () => { + (useQuery as jest.Mock).mockReturnValue({ + isLoading: false, + data: { + alertIds: ['alertIds'], + statsNodes: [mockStatsNode], + }, + }); + (useAlertDocumentAnalyzerSchema as jest.Mock).mockReturnValue({ + loading: false, + error: false, + id: null, + schema: null, + agentId: null, + }); + + hookResult = renderHook(() => + useAlertPrevalenceFromProcessTree({ + documentId: 'documentId', + isActiveTimeline: true, + indices: [], + }) + ); + + expect(hookResult.result.current.loading).toEqual(false); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.alertIds).toEqual(['alertIds']); + expect(hookResult.result.current.statsNodes).toEqual([mockStatsNode]); + }); + + it('should return all properties data undefined', () => { + (useQuery as jest.Mock).mockReturnValue({ + isLoading: false, + }); + (useAlertDocumentAnalyzerSchema as jest.Mock).mockReturnValue({ + loading: false, + error: false, + id: null, + schema: null, + agentId: null, + }); + + hookResult = renderHook(() => + useAlertPrevalenceFromProcessTree({ + documentId: 'documentId', + isActiveTimeline: true, + indices: [], + }) + ); + + expect(hookResult.result.current.loading).toEqual(false); + expect(hookResult.result.current.error).toEqual(true); + expect(hookResult.result.current.alertIds).toEqual(undefined); + expect(hookResult.result.current.statsNodes).toEqual(undefined); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence_from_process_tree.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence_from_process_tree.ts similarity index 57% rename from x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence_from_process_tree.ts rename to x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence_from_process_tree.ts index 4e6747384fe34c..f9c27f6e2ccb43 100644 --- a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence_from_process_tree.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence_from_process_tree.ts @@ -4,114 +4,120 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { useQuery } from '@tanstack/react-query'; -import { useHttp } from '../../lib/kibana'; -import { useTimelineDataFilters } from '../../../timelines/containers/use_timeline_data_filters'; -export const DETECTIONS_ALERTS_COUNT_ID = 'detections-alerts-count'; +import { useQuery } from '@tanstack/react-query'; +import { useMemo } from 'react'; +import { useAlertDocumentAnalyzerSchema } from './use_alert_document_analyzer_schema'; +import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters'; +import { useHttp } from '../../../../common/lib/kibana'; export interface StatsNode { + /** + * The data of the node + */ data: object; + /** + * The ID of the node + */ id: string; + /** + * The name of the node + */ name: string; + /** + * The parent ID of the node + */ parent?: string; stats: { + /** + * The total number of alerts + */ total: number; + /** + * The total number of alerts by category + */ byCategory: { alerts?: number; }; }; } -interface UserAlertPrevalenceFromProcessTreeResult { - loading: boolean; - alertIds: undefined | string[]; - statsNodes: undefined | StatsNode[]; - count?: number; - error: boolean; -} - interface ProcessTreeAlertPrevalenceResponse { + /** + * The alert IDs found in the process tree + */ alertIds: string[] | undefined; + /** + * The stats nodes found in the process tree + */ statsNodes: StatsNode[] | undefined; } -interface EntityResponse { - id: string; - name: string; - schema: object; - agentId: string; +interface TreeResponse { + /** + * The alert IDs found in the process tree + */ + alertIds: string[]; + /** + * The stats nodes found in the process tree + */ + statsNodes: StatsNode[]; } -interface UseAlertPrevalenceFromProcessTree { +export interface UseAlertPrevalenceFromProcessTreeParams { + /** + * The document ID of the alert to analyze + */ documentId: string; + /** + * Whether or not the timeline is active + */ isActiveTimeline: boolean; + /** + * The indices to search for alerts + */ indices: string[]; } -interface UseAlertDocumentAnalyzerSchema { - documentId: string; - indices: string[]; -} - -interface TreeResponse { - statsNodes: StatsNode[]; - alertIds: string[]; -} - -function useAlertDocumentAnalyzerSchema({ documentId, indices }: UseAlertDocumentAnalyzerSchema) { - const http = useHttp(); - const query = useQuery(['getAlertPrevalenceSchema', documentId], () => { - return http.get(`/api/endpoint/resolver/entity`, { - query: { - _id: documentId, - indices, - }, - }); - }); - if (query.isLoading) { - return { - loading: true, - error: false, - id: null, - schema: null, - agentId: null, - }; - } else if (query.data && query.data.length > 0) { - const { - data: [{ schema, id, agentId }], - } = query; - return { - loading: false, - error: false, - id, - schema, - agentId, - }; - } else { - return { - loading: false, - error: true, - id: null, - schema: null, - agentId: null, - }; - } +export interface UserAlertPrevalenceFromProcessTreeResult { + /** + * Whether or not the query is loading + */ + loading: boolean; + /** + * The alert IDs found in the process tree + */ + alertIds: undefined | string[]; + /** + * The stats nodes found in the process tree + */ + statsNodes: undefined | StatsNode[]; + /** + * Whether or not the query errored + */ + error: boolean; } +/** + * Fetches the alert prevalence from the process tree + */ export function useAlertPrevalenceFromProcessTree({ documentId, isActiveTimeline, indices, -}: UseAlertPrevalenceFromProcessTree): UserAlertPrevalenceFromProcessTreeResult { +}: UseAlertPrevalenceFromProcessTreeParams): UserAlertPrevalenceFromProcessTreeResult { const http = useHttp(); const { selectedPatterns } = useTimelineDataFilters(isActiveTimeline); - const alertAndOriginalIndices = [...new Set(selectedPatterns.concat(indices))]; + const alertAndOriginalIndices = useMemo( + () => [...new Set(selectedPatterns.concat(indices))], + [indices, selectedPatterns] + ); const { loading, id, schema, agentId } = useAlertDocumentAnalyzerSchema({ documentId, indices: alertAndOriginalIndices, }); + const query = useQuery( ['getAlertPrevalenceFromProcessTree', id], () => { @@ -129,6 +135,7 @@ export function useAlertPrevalenceFromProcessTree({ }, { enabled: schema !== null && id !== null } ); + if (query.isLoading || loading) { return { loading: true, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_ancestry.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_ancestry.test.tsx index 9291b5e9a0c1a8..4d65339c6b41a8 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_ancestry.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_ancestry.test.tsx @@ -12,9 +12,9 @@ import type { UseFetchRelatedAlertsByAncestryResult, } from './use_fetch_related_alerts_by_ancestry'; import { useFetchRelatedAlertsByAncestry } from './use_fetch_related_alerts_by_ancestry'; -import { useAlertPrevalenceFromProcessTree } from '../../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; +import { useAlertPrevalenceFromProcessTree } from './use_alert_prevalence_from_process_tree'; -jest.mock('../../../../common/containers/alerts/use_alert_prevalence_from_process_tree'); +jest.mock('./use_alert_prevalence_from_process_tree'); const documentId = 'documentId'; const indices = ['index1']; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_ancestry.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_ancestry.ts index b44349a06eec92..826290a3dd3e9e 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_ancestry.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_ancestry.ts @@ -6,7 +6,7 @@ */ import { useMemo } from 'react'; -import { useAlertPrevalenceFromProcessTree } from '../../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; +import { useAlertPrevalenceFromProcessTree } from './use_alert_prevalence_from_process_tree'; import { isActiveTimeline } from '../../../../helpers'; export interface UseFetchRelatedAlertsByAncestryParams { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_same_source_event.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_same_source_event.test.tsx index 4aaab73af12965..ff74774068adf1 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_same_source_event.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_same_source_event.test.tsx @@ -12,9 +12,9 @@ import type { UseFetchRelatedAlertsBySameSourceEventResult, } from './use_fetch_related_alerts_by_same_source_event'; import { useFetchRelatedAlertsBySameSourceEvent } from './use_fetch_related_alerts_by_same_source_event'; -import { useAlertPrevalence } from '../../../../common/containers/alerts/use_alert_prevalence'; +import { useAlertPrevalence } from './use_alert_prevalence'; -jest.mock('../../../../common/containers/alerts/use_alert_prevalence'); +jest.mock('./use_alert_prevalence'); const originalEventId = 'originalEventId'; const scopeId = 'scopeId'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_same_source_event.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_same_source_event.ts index 1946cef3e7de4d..209bcb0c04051f 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_same_source_event.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_same_source_event.ts @@ -7,7 +7,7 @@ import { useMemo } from 'react'; import { ANCESTOR_ID } from '../constants/field_names'; -import { useAlertPrevalence } from '../../../../common/containers/alerts/use_alert_prevalence'; +import { useAlertPrevalence } from './use_alert_prevalence'; import { isActiveTimeline } from '../../../../helpers'; export interface UseFetchRelatedAlertsBySameSourceEventParams { @@ -50,7 +50,7 @@ export const useFetchRelatedAlertsBySameSourceEvent = ({ field: ANCESTOR_ID, value: originalEventId, isActiveTimelines: isActiveTimeline(scopeId), - signalIndexName: null, + indexName: null, includeAlertIds: true, }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_session.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_session.test.tsx index 6f6f2ea73158f1..b38ef44178f9f6 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_session.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_session.test.tsx @@ -13,9 +13,9 @@ import type { UseFetchRelatedAlertsBySessionResult, } from './use_fetch_related_alerts_by_session'; import { useFetchRelatedAlertsBySession } from './use_fetch_related_alerts_by_session'; -import { useAlertPrevalence } from '../../../../common/containers/alerts/use_alert_prevalence'; +import { useAlertPrevalence } from './use_alert_prevalence'; -jest.mock('../../../../common/containers/alerts/use_alert_prevalence'); +jest.mock('./use_alert_prevalence'); const entityId = 'entityId'; const scopeId = 'scopeId'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_session.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_session.ts index 2c70714d07d5b4..606c3523f60be7 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_session.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_session.ts @@ -6,7 +6,7 @@ */ import { useMemo } from 'react'; -import { useAlertPrevalence } from '../../../../common/containers/alerts/use_alert_prevalence'; +import { useAlertPrevalence } from './use_alert_prevalence'; import { isActiveTimeline } from '../../../../helpers'; import { ENTRY_LEADER_ENTITY_ID } from '../constants/field_names'; @@ -50,7 +50,7 @@ export const useFetchRelatedAlertsBySession = ({ field: ENTRY_LEADER_ENTITY_ID, value: entityId, isActiveTimelines: isActiveTimeline(scopeId), - signalIndexName: null, + indexName: null, includeAlertIds: true, ignoreTimerange: true, }); From 04503bffe9110196d591d7886f7a62ea7ab61982 Mon Sep 17 00:00:00 2001 From: Philippe Oberti Date: Mon, 19 Aug 2024 17:01:07 +0200 Subject: [PATCH 06/16] [Security Solution][Entity details] - move osquery, response and investigation guide related hooks and components to flyout folder (#190110) --- .../components/event_details/translations.ts | 32 ---------- .../markdown_editor/plugins/insight/index.tsx | 2 +- .../plugins/osquery/renderer.tsx | 2 +- .../left/components/investigation_guide.tsx | 2 +- .../investigation_guide_view.test.tsx | 2 +- .../components}/investigation_guide_view.tsx | 18 ++++-- .../left/components/response_details.tsx | 2 +- .../hooks/use_response_actions_view.test.ts | 61 +++++++++++++++++++ .../left/hooks/use_response_actions_view.tsx} | 60 ++++++++++-------- .../translations/translations/fr-FR.json | 5 -- .../translations/translations/ja-JP.json | 5 -- .../translations/translations/zh-CN.json | 5 -- 12 files changed, 114 insertions(+), 82 deletions(-) rename x-pack/plugins/security_solution/public/{common/components/event_details => flyout/document_details/left/components}/investigation_guide_view.test.tsx (92%) rename x-pack/plugins/security_solution/public/{common/components/event_details => flyout/document_details/left/components}/investigation_guide_view.tsx (81%) create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_response_actions_view.test.ts rename x-pack/plugins/security_solution/public/{common/components/event_details/response_actions_view.tsx => flyout/document_details/left/hooks/use_response_actions_view.tsx} (75%) diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts index d4d129f57ae3a4..2362284b3f6964 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts @@ -7,28 +7,10 @@ import { i18n } from '@kbn/i18n'; -export const INVESTIGATION_GUIDE = i18n.translate( - 'xpack.securitySolution.alertDetails.overview.investigationGuide', - { - defaultMessage: 'Investigation guide', - } -); - export const TABLE = i18n.translate('xpack.securitySolution.eventDetails.table', { defaultMessage: 'Table', }); -export const OSQUERY_VIEW = i18n.translate('xpack.securitySolution.eventDetails.osqueryView', { - defaultMessage: 'Osquery Results', -}); - -export const RESPONSE_ACTIONS_VIEW = i18n.translate( - 'xpack.securitySolution.eventDetails.responseActionsView', - { - defaultMessage: 'Response Results', - } -); - export const DESCRIPTION = i18n.translate('xpack.securitySolution.eventDetails.description', { defaultMessage: 'Description', }); @@ -48,20 +30,6 @@ export const RULE_TYPE = i18n.translate('xpack.securitySolution.detections.alert defaultMessage: 'Rule type', }); -export const MULTI_FIELD_TOOLTIP = i18n.translate( - 'xpack.securitySolution.eventDetails.multiFieldTooltipContent', - { - defaultMessage: 'Multi-fields can have multiple values per field', - } -); - -export const MULTI_FIELD_BADGE = i18n.translate( - 'xpack.securitySolution.eventDetails.multiFieldBadge', - { - defaultMessage: 'multi-field', - } -); - export const ACTIONS = i18n.translate('xpack.securitySolution.eventDetails.table.actions', { defaultMessage: 'Actions', }); diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx index 6eae6b723d5410..791bace753ff83 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx @@ -42,7 +42,7 @@ import { useAppToasts } from '../../../../hooks/use_app_toasts'; import { useKibana } from '../../../../lib/kibana'; import { useInsightQuery } from './use_insight_query'; import { useInsightDataProviders, type Provider } from './use_insight_data_providers'; -import { BasicAlertDataContext } from '../../../event_details/investigation_guide_view'; +import { BasicAlertDataContext } from '../../../../../flyout/document_details/left/components/investigation_guide_view'; import { InvestigateInTimelineButton } from '../../../event_details/table/investigate_in_timeline_button'; import { getTimeRangeSettings, diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/renderer.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/renderer.tsx index 04963e70f9cfaa..198f64bb252372 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/renderer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/renderer.tsx @@ -13,7 +13,7 @@ import styled from 'styled-components'; import { EuiButton, EuiToolTip } from '@elastic/eui'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; import { useUpsellingMessage } from '../../../../hooks/use_upselling'; -import { BasicAlertDataContext } from '../../../event_details/investigation_guide_view'; +import { BasicAlertDataContext } from '../../../../../flyout/document_details/left/components/investigation_guide_view'; import { expandDottedObject } from '../../../../../../common/utils/expand_dotted'; import OsqueryLogo from './osquery_icon/osquery.svg'; import { OsqueryFlyout } from '../../../../../detections/components/osquery/osquery_flyout'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/investigation_guide.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/investigation_guide.tsx index 0bf6ca92b28fa0..ee1bebdb336ce5 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/investigation_guide.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/investigation_guide.tsx @@ -10,7 +10,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { useInvestigationGuide } from '../../shared/hooks/use_investigation_guide'; import { useDocumentDetailsContext } from '../../shared/context'; import { INVESTIGATION_GUIDE_TEST_ID, INVESTIGATION_GUIDE_LOADING_TEST_ID } from './test_ids'; -import { InvestigationGuideView } from '../../../../common/components/event_details/investigation_guide_view'; +import { InvestigationGuideView } from './investigation_guide_view'; import { FlyoutLoading } from '../../../shared/components/flyout_loading'; /** diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/investigation_guide_view.test.tsx similarity index 92% rename from x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.test.tsx rename to x-pack/plugins/security_solution/public/flyout/document_details/left/components/investigation_guide_view.test.tsx index 355ad1f9129d7f..bc7de71c564175 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/investigation_guide_view.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { InvestigationGuideView } from './investigation_guide_view'; -import type { UseBasicDataFromDetailsDataResult } from '../../../flyout/document_details/shared/hooks/use_basic_data_from_details_data'; +import type { UseBasicDataFromDetailsDataResult } from '../../shared/hooks/use_basic_data_from_details_data'; const defaultProps = { basicData: { diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/investigation_guide_view.tsx similarity index 81% rename from x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx rename to x-pack/plugins/security_solution/public/flyout/document_details/left/components/investigation_guide_view.tsx index b3015bafe4536f..3d61c223fd47ff 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/investigation_guide_view.tsx @@ -8,10 +8,17 @@ import { EuiSpacer, EuiTitle, EuiText } from '@elastic/eui'; import React, { createContext } from 'react'; import styled from 'styled-components'; -import type { UseBasicDataFromDetailsDataResult } from '../../../flyout/document_details/shared/hooks/use_basic_data_from_details_data'; -import * as i18n from './translations'; -import { MarkdownRenderer } from '../markdown_editor'; -import { LineClamp } from '../line_clamp'; +import { i18n } from '@kbn/i18n'; +import type { UseBasicDataFromDetailsDataResult } from '../../shared/hooks/use_basic_data_from_details_data'; +import { LineClamp } from '../../../../common/components/line_clamp'; +import { MarkdownRenderer } from '../../../../common/components/markdown_editor'; + +const INVESTIGATION_GUIDE = i18n.translate( + 'xpack.securitySolution.flyout.left.investigationGuide', + { + defaultMessage: 'Investigation guide', + } +); export const Indent = styled.div` padding: 0 8px; @@ -43,7 +50,6 @@ interface InvestigationGuideViewProps { /** * Investigation guide that shows the markdown text of rule.note */ -// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462 const InvestigationGuideViewComponent: React.FC = ({ basicData, ruleNote, @@ -56,7 +62,7 @@ const InvestigationGuideViewComponent: React.FC = ( <> -
{i18n.INVESTIGATION_GUIDE}
+
{INVESTIGATION_GUIDE}
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/response_details.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/response_details.tsx index 5081bdad9c17fc..a26e636749786b 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/response_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/response_details.tsx @@ -11,7 +11,7 @@ import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n-react'; import { RESPONSE_DETAILS_TEST_ID } from './test_ids'; import { useDocumentDetailsContext } from '../../shared/context'; -import { useResponseActionsView } from '../../../../common/components/event_details/response_actions_view'; +import { useResponseActionsView } from '../hooks/use_response_actions_view'; const ExtendedFlyoutWrapper = styled.div` figure { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_response_actions_view.test.ts b/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_response_actions_view.test.ts new file mode 100644 index 00000000000000..cafac9f3a0b9fe --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_response_actions_view.test.ts @@ -0,0 +1,61 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { useResponseActionsView } from './use_response_actions_view'; +import { mockSearchHit } from '../../shared/mocks/mock_search_hit'; +import { mockDataAsNestedObject } from '../../shared/mocks/mock_data_as_nested_object'; +import { useGetAutomatedActionList } from '../../../../management/hooks/response_actions/use_get_automated_action_list'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; + +const ecsData = mockDataAsNestedObject; +const rawEventData = mockSearchHit; + +jest.mock('../../../../common/hooks/use_experimental_features'); +jest.mock('../../../../management/hooks/response_actions/use_get_automated_action_list'); + +describe('useResponseActionsView', () => { + it('should return the normal component', () => { + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); + (useGetAutomatedActionList as jest.Mock).mockReturnValue({ + data: [], + isFetched: true, + }); + + const { result } = renderHook(() => + useResponseActionsView({ + ecsData, + rawEventData, + }) + ); + + expect(result.current.id).toEqual('response-actions-results-view'); + expect(result.current.name).toEqual('Response Results'); + expect(result.current.append).toBeDefined(); + expect(result.current.content).toBeDefined(); + }); + + it('returns early return if rawEventData is undefined', () => { + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); + (useGetAutomatedActionList as jest.Mock).mockReturnValue({ + data: [], + isFetched: true, + }); + + const { result } = renderHook(() => + useResponseActionsView({ + ecsData, + rawEventData: undefined, + }) + ); + + expect(result.current.id).toEqual('response-actions-results-view'); + expect(result.current.name).toEqual('Response Results'); + expect(result.current.append).not.toBeDefined(); + expect(result.current.content).toBeDefined(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/response_actions_view.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_response_actions_view.tsx similarity index 75% rename from x-pack/plugins/security_solution/public/common/components/event_details/response_actions_view.tsx rename to x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_response_actions_view.tsx index 33760b7ab42424..b6966b529d3d6d 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/response_actions_view.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_response_actions_view.tsx @@ -5,23 +5,29 @@ * 2.0. */ -import React, { useMemo, useState, useEffect } from 'react'; -import styled from 'styled-components'; +import React, { useState, useEffect } from 'react'; +import styled from '@emotion/styled'; import type { EuiTabbedContentTab } from '@elastic/eui'; import { EuiLink, EuiNotificationBadge, EuiSpacer } from '@elastic/eui'; import type { Ecs } from '@kbn/cases-plugin/common'; import { FormattedMessage } from '@kbn/i18n-react'; -import { RESPONSE_NO_DATA_TEST_ID } from '../../../flyout/document_details/left/components/test_ids'; -import type { SearchHit } from '../../../../common/search_strategy'; +import { i18n } from '@kbn/i18n'; +import { RESPONSE_NO_DATA_TEST_ID } from '../components/test_ids'; +import type { SearchHit } from '../../../../../common/search_strategy'; import type { ExpandedEventFieldsObject, RawEventData, -} from '../../../../common/types/response_actions'; -import { ResponseActionsResults } from '../response_actions/response_actions_results'; -import { expandDottedObject } from '../../../../common/utils/expand_dotted'; -import { useGetAutomatedActionList } from '../../../management/hooks/response_actions/use_get_automated_action_list'; -import { EventsViewType } from './event_details'; -import * as i18n from './translations'; +} from '../../../../../common/types/response_actions'; +import { ResponseActionsResults } from '../../../../common/components/response_actions/response_actions_results'; +import { expandDottedObject } from '../../../../../common/utils/expand_dotted'; +import { useGetAutomatedActionList } from '../../../../management/hooks/response_actions/use_get_automated_action_list'; + +const RESPONSE_ACTIONS_VIEW = i18n.translate( + 'xpack.securitySolution.flyout.response.responseActionsView', + { + defaultMessage: 'Response Results', + } +); const TabContentWrapper = styled.div` height: 100%; @@ -56,23 +62,29 @@ const EmptyResponseActions = () => { ); }; -// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462 +const viewData = { + id: 'response-actions-results-view', + name: RESPONSE_ACTIONS_VIEW, +}; + +export interface UseResponseActionsViewParams { + /** + * An object with top level fields from the ECS object + */ + ecsData?: Ecs | null; + /** + * The actual raw document object + */ + rawEventData: SearchHit | undefined; +} + +/** + * + */ export const useResponseActionsView = ({ rawEventData, ecsData, -}: { - ecsData?: Ecs | null; - rawEventData: SearchHit | undefined; -}): EuiTabbedContentTab | undefined => { - // can not be moved outside of the component, because then EventsViewType throws runtime error regarding not being initialized yet - const viewData = useMemo( - () => ({ - id: EventsViewType.responseActionsView, - 'data-test-subj': 'responseActionsViewTab', - name: i18n.RESPONSE_ACTIONS_VIEW, - }), - [] - ); +}: UseResponseActionsViewParams): EuiTabbedContentTab => { const expandedEventFieldsObject = rawEventData ? (expandDottedObject((rawEventData as RawEventData).fields) as ExpandedEventFieldsObject) : undefined; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 2fa4a66e8b37b7..cc0f7067d89e07 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -35559,7 +35559,6 @@ "xpack.securitySolution.alertCountByRuleByStatus.tooltipTitle": "Nom de règle", "xpack.securitySolution.alertDetails.overview.hostRiskDataTitle": "Données de risque de {riskEntity}", "xpack.securitySolution.alertDetails.overview.insights.suppressedAlertsCountTechnicalPreview": "Version d'évaluation technique", - "xpack.securitySolution.alertDetails.overview.investigationGuide": "Guide d'investigation", "xpack.securitySolution.alertDetails.summary.readLess": "Lire moins", "xpack.securitySolution.alertDetails.summary.readMore": "En savoir plus", "xpack.securitySolution.alerts.badge.readOnly.tooltip": "Impossible de mettre à jour les alertes", @@ -38924,14 +38923,10 @@ "xpack.securitySolution.event.summary.threat_indicator.showMatches": "Afficher les {count} alertes de correspondance d'indicateur", "xpack.securitySolution.eventDetails.alertReason": "Raison d'alerte", "xpack.securitySolution.eventDetails.description": "Description", - "xpack.securitySolution.eventDetails.multiFieldBadge": "champ multiple", - "xpack.securitySolution.eventDetails.multiFieldTooltipContent": "Les champs multiples peuvent avoir plusieurs valeurs.", - "xpack.securitySolution.eventDetails.osqueryView": "Résultats Osquery", "xpack.securitySolution.eventDetails.responseActions.endpoint.executed": "a exécuté la commande {command}", "xpack.securitySolution.eventDetails.responseActions.endpoint.failed": "n'a pas pu exécuter la commande {command}", "xpack.securitySolution.eventDetails.responseActions.endpoint.pending": "exécute la commande {command}", "xpack.securitySolution.eventDetails.responseActions.endpoint.tried": "a tenté d'exécuter la commande {command}", - "xpack.securitySolution.eventDetails.responseActionsView": "Résultats de la réponse", "xpack.securitySolution.eventDetails.summaryView": "résumé", "xpack.securitySolution.eventDetails.table": "Tableau", "xpack.securitySolution.eventDetails.table.actions": "Actions", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c26fe66d039313..519c9f2a428a92 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -35543,7 +35543,6 @@ "xpack.securitySolution.alertCountByRuleByStatus.tooltipTitle": "ルール名", "xpack.securitySolution.alertDetails.overview.hostRiskDataTitle": "{riskEntity}リスクデータ", "xpack.securitySolution.alertDetails.overview.insights.suppressedAlertsCountTechnicalPreview": "テクニカルプレビュー", - "xpack.securitySolution.alertDetails.overview.investigationGuide": "調査ガイド", "xpack.securitySolution.alertDetails.summary.readLess": "表示を減らす", "xpack.securitySolution.alertDetails.summary.readMore": "続きを読む", "xpack.securitySolution.alerts.badge.readOnly.tooltip": "アラートを更新できません", @@ -38905,14 +38904,10 @@ "xpack.securitySolution.event.summary.threat_indicator.showMatches": "すべての{count}件のインジケーター一致アラートを表示", "xpack.securitySolution.eventDetails.alertReason": "アラートの理由", "xpack.securitySolution.eventDetails.description": "説明", - "xpack.securitySolution.eventDetails.multiFieldBadge": "複数フィールド", - "xpack.securitySolution.eventDetails.multiFieldTooltipContent": "複数フィールドにはフィールドごとに複数の値を入力できます", - "xpack.securitySolution.eventDetails.osqueryView": "Osquery結果", "xpack.securitySolution.eventDetails.responseActions.endpoint.executed": "{command}コマンドを実行しました", "xpack.securitySolution.eventDetails.responseActions.endpoint.failed": "{command}コマンドを実行できませんでした", "xpack.securitySolution.eventDetails.responseActions.endpoint.pending": "{command}コマンドを実行しています", "xpack.securitySolution.eventDetails.responseActions.endpoint.tried": "{command}コマンドを実行しようとしました", - "xpack.securitySolution.eventDetails.responseActionsView": "対応の結果", "xpack.securitySolution.eventDetails.summaryView": "まとめ", "xpack.securitySolution.eventDetails.table": "表", "xpack.securitySolution.eventDetails.table.actions": "アクション", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c4f2618a499d60..328008ebf39526 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -35584,7 +35584,6 @@ "xpack.securitySolution.alertCountByRuleByStatus.tooltipTitle": "规则名称", "xpack.securitySolution.alertDetails.overview.hostRiskDataTitle": "{riskEntity}风险数据", "xpack.securitySolution.alertDetails.overview.insights.suppressedAlertsCountTechnicalPreview": "技术预览", - "xpack.securitySolution.alertDetails.overview.investigationGuide": "调查指南", "xpack.securitySolution.alertDetails.summary.readLess": "阅读更少内容", "xpack.securitySolution.alertDetails.summary.readMore": "阅读更多内容", "xpack.securitySolution.alerts.badge.readOnly.tooltip": "无法更新告警", @@ -38949,14 +38948,10 @@ "xpack.securitySolution.event.summary.threat_indicator.showMatches": "显示所有 {count} 个指标匹配告警", "xpack.securitySolution.eventDetails.alertReason": "告警原因", "xpack.securitySolution.eventDetails.description": "描述", - "xpack.securitySolution.eventDetails.multiFieldBadge": "多字段", - "xpack.securitySolution.eventDetails.multiFieldTooltipContent": "多字段的每个字段可以有多个值", - "xpack.securitySolution.eventDetails.osqueryView": "Osquery 结果", "xpack.securitySolution.eventDetails.responseActions.endpoint.executed": "已执行 {command} 命令", "xpack.securitySolution.eventDetails.responseActions.endpoint.failed": "无法执行 {command} 命令", "xpack.securitySolution.eventDetails.responseActions.endpoint.pending": "正在执行 {command} 命令", "xpack.securitySolution.eventDetails.responseActions.endpoint.tried": "已尝试执行 {command} 命令", - "xpack.securitySolution.eventDetails.responseActionsView": "响应结果", "xpack.securitySolution.eventDetails.summaryView": "摘要", "xpack.securitySolution.eventDetails.table": "表", "xpack.securitySolution.eventDetails.table.actions": "操作", From 4eeb35d21b9d983582c25be8936591acd8a29246 Mon Sep 17 00:00:00 2001 From: Ryan Keairns Date: Mon, 19 Aug 2024 08:28:29 -0700 Subject: [PATCH 07/16] Slim down popover panels (#190472) ## Summary These couple of popover panels contain more padding than desired or intended by the design system. **Before** _Discover alerts popover_ _Nav deployments popover_ **After** _Discover alerts popover_ _Nav deployments popover_ ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [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 - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### Risk Matrix Delete this section if it is not applicable to this PR. Before closing this PR, invite QA, stakeholders, and other developers to identify risks that should be tested prior to the change/feature release. When forming the risk matrix, consider some of the following examples and how they may potentially impact the change: | Risk | Probability | Severity | Mitigation/Notes | |---------------------------|-------------|----------|-------------------------| | Multiple Spaces—unexpected behavior in non-default Kibana Space. | Low | High | Integration tests will verify that all features are still supported in non-default Kibana Space and when user switches between spaces. | | Multiple nodes—Elasticsearch polling might have race conditions when multiple Kibana nodes are polling for the same tasks. | High | Low | Tasks are idempotent, so executing them multiple times will not result in logical error, but will degrade performance. To test for this case we add plenty of unit tests around this logic and document manual testing procedure. | | Code should gracefully handle cases when feature X or plugin Y are disabled. | Medium | High | Unit tests will verify that any feature flag or plugin combination still results in our service operational. | | [See more potential risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) | ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../src/project_navigation/breadcrumbs.tsx | 6 ++++-- .../main/components/top_nav/open_alerts_popover.tsx | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/breadcrumbs.tsx b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/breadcrumbs.tsx index fb1043d239523f..ac80702ab99ec0 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/breadcrumbs.tsx +++ b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/breadcrumbs.tsx @@ -144,6 +144,7 @@ function buildRootCrumb({ color="text" iconType="gear" data-test-subj="manageDeploymentBtn" + size="s" > {i18n.translate('core.ui.primaryNav.cloud.breadCrumbDropdown.manageDeploymentLabel', { defaultMessage: 'Manage this deployment', @@ -157,6 +158,7 @@ function buildRootCrumb({ color="text" iconType="spaces" data-test-subj="viewDeploymentsBtn" + size="s" > {cloudLinks.deployments.title} @@ -164,9 +166,9 @@ function buildRootCrumb({ ), popoverProps: { - panelPaddingSize: 'm', + panelPaddingSize: 's', zIndex: 6000, - panelStyle: { width: 260 }, + panelStyle: { maxWidth: 240 }, panelProps: { 'data-test-subj': 'deploymentLinksPanel', }, diff --git a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx index 097278ac9cd66e..7b97db9b633184 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx @@ -183,8 +183,9 @@ export function AlertsPopover({ button={anchorElement} closePopover={onClose} isOpen={!alertFlyoutVisible} + panelPaddingSize="s" > - + ); From 439c7fa84c45b3c632193ce0ffd16b437ea21e08 Mon Sep 17 00:00:00 2001 From: Kyle Pollich Date: Mon, 19 Aug 2024 11:35:25 -0400 Subject: [PATCH 08/16] [Fleet] Replace all references to unsafe YML load/dump methods in Fleet codebase (#190659) ## Summary Replaces any unsafe YML operations with their safe alternatives. `load` -> `safeLoad` `dump` -> `safeDump` --- .../custom_integrations/assets/dataset/ingest_pipeline.ts | 4 ++-- .../packages/custom_integrations/assets/dataset/manifest.ts | 4 ++-- .../epm/packages/custom_integrations/assets/manifest.ts | 4 ++-- .../plugins/fleet/server/services/epm/packages/utils.test.ts | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/dataset/ingest_pipeline.ts b/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/dataset/ingest_pipeline.ts index 0021f395158e66..4d7313cacff723 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/dataset/ingest_pipeline.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/dataset/ingest_pipeline.ts @@ -5,7 +5,7 @@ * 2.0. */ -import * as yaml from 'js-yaml'; +import { safeDump } from 'js-yaml'; // NOTE: The install methods will take care of adding a reference to a @custom pipeline. We don't need to add one here. export const createDefaultPipeline = (dataset: string, type: string) => { @@ -25,5 +25,5 @@ export const createDefaultPipeline = (dataset: string, type: string) => { managed: true, }, }; - return yaml.dump(pipeline); + return safeDump(pipeline); }; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/dataset/manifest.ts b/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/dataset/manifest.ts index 75b34867f6d07c..efca290e31092d 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/dataset/manifest.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/dataset/manifest.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import * as yaml from 'js-yaml'; +import { safeDump } from 'js-yaml'; import { convertStringToTitle } from '../../utils'; import type { AssetOptions } from '../generate'; @@ -17,5 +17,5 @@ export const createDatasetManifest = (dataset: string, assetOptions: AssetOption title: convertStringToTitle(dataset), type, }; - return yaml.dump(manifest); + return safeDump(manifest); }; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/manifest.ts b/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/manifest.ts index cf308f03db7dd6..4c27ad6c45343f 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/manifest.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/manifest.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import * as yaml from 'js-yaml'; +import { safeDump } from 'js-yaml'; import type { AssetOptions } from './generate'; @@ -34,5 +34,5 @@ export const createManifest = (assetOptions: AssetOptions) => { }, }; - return yaml.dump(manifest); + return safeDump(manifest); }; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/utils.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/utils.test.ts index 166687a836fb15..8de21942d75522 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/utils.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/utils.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { dump } from 'js-yaml'; +import { safeDump } from 'js-yaml'; import type { AssetsMap } from '../../../../common/types'; @@ -14,7 +14,7 @@ import type { RegistryDataStream } from '../../../../common'; import { resolveDataStreamFields } from './utils'; describe('resolveDataStreamFields', () => { - const statusAssetYml = dump([ + const statusAssetYml = safeDump([ { name: 'apache.status', type: 'group', From 6d1426acd8823a48a0ffa03ad6635cf853f936a2 Mon Sep 17 00:00:00 2001 From: Philippe Oberti Date: Mon, 19 Aug 2024 17:59:43 +0200 Subject: [PATCH 09/16] [Security Solution][Entity details] - move code to get url link to flyout folder (#190111) --- .../right/components/header_actions.test.tsx | 10 ++--- .../right/components/header_actions.tsx | 8 ++-- .../right/hooks/use_flyout_is_expandable.ts | 2 +- .../right/hooks/use_get_flyout_link.test.tsx | 37 ++++++++++++++++++ .../right/hooks/use_get_flyout_link.ts} | 39 +++++++++++++------ .../right/hooks/use_process_data.ts | 2 +- .../right/hooks/use_session_preview.test.tsx | 2 +- .../right/hooks/use_session_preview.ts | 2 +- .../document_details/shared/context.tsx | 2 +- .../shared/hooks/use_event_details.test.tsx | 28 +++++++++++-- .../shared/hooks/use_event_details.ts | 23 +++++++++-- .../shared/hooks/use_get_fields_data.test.tsx | 36 +++++++++++++++++ .../shared}/hooks/use_get_fields_data.ts | 37 ++++++++++++++---- .../use_show_related_alerts_by_ancestry.ts | 2 +- ...how_related_alerts_by_same_source_event.ts | 2 +- .../use_show_related_alerts_by_session.ts | 2 +- .../shared/hooks/use_show_related_cases.ts | 2 +- .../hooks/use_show_suppressed_alerts.ts | 2 +- .../shared/mocks/mock_get_fields_data.ts | 2 +- 19 files changed, 192 insertions(+), 48 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_get_flyout_link.test.tsx rename x-pack/plugins/security_solution/public/{timelines/components/side_panel/event_details/use_get_alert_details_flyout_link.ts => flyout/document_details/right/hooks/use_get_flyout_link.ts} (64%) create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_get_fields_data.test.tsx rename x-pack/plugins/security_solution/public/{common => flyout/document_details/shared}/hooks/use_get_fields_data.ts (86%) diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.test.tsx index 26d2c9d1f63d47..a3aa8e410eee6e 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.test.tsx @@ -14,13 +14,11 @@ import { useAssistant } from '../hooks/use_assistant'; import { mockGetFieldsData } from '../../shared/mocks/mock_get_fields_data'; import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser'; import { TestProvidersComponent } from '../../../../common/mock'; -import { useGetAlertDetailsFlyoutLink } from '../../../../timelines/components/side_panel/event_details/use_get_alert_details_flyout_link'; +import { useGetFlyoutLink } from '../hooks/use_get_flyout_link'; jest.mock('../../../../common/lib/kibana'); jest.mock('../hooks/use_assistant'); -jest.mock( - '../../../../timelines/components/side_panel/event_details/use_get_alert_details_flyout_link' -); +jest.mock('../hooks/use_get_flyout_link'); jest.mock('@elastic/eui', () => ({ ...jest.requireActual('@elastic/eui'), @@ -53,7 +51,7 @@ describe('', () => { beforeEach(() => { window.location.search = '?'; - jest.mocked(useGetAlertDetailsFlyoutLink).mockReturnValue(alertUrl); + jest.mocked(useGetFlyoutLink).mockReturnValue(alertUrl); jest.mocked(useAssistant).mockReturnValue({ showAssistant: true, promptContextId: '' }); }); @@ -65,7 +63,7 @@ describe('', () => { }); it('should not render share button in the title if alert is missing url info', () => { - jest.mocked(useGetAlertDetailsFlyoutLink).mockReturnValue(null); + jest.mocked(useGetFlyoutLink).mockReturnValue(null); const { queryByTestId } = renderHeaderActions(mockContextValue); expect(queryByTestId(SHARE_BUTTON_TEST_ID)).not.toBeInTheDocument(); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.tsx index f90d67f87f37e9..078f273ec28fab 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.tsx @@ -10,7 +10,7 @@ import React, { memo } from 'react'; import { EuiButtonIcon, EuiCopy, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { NewChatByTitle } from '@kbn/elastic-assistant'; -import { useGetAlertDetailsFlyoutLink } from '../../../../timelines/components/side_panel/event_details/use_get_alert_details_flyout_link'; +import { useGetFlyoutLink } from '../hooks/use_get_flyout_link'; import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data'; import { useAssistant } from '../hooks/use_assistant'; import { @@ -27,9 +27,9 @@ export const HeaderActions: VFC = memo(() => { const { dataFormattedForFieldBrowser, eventId, indexName } = useDocumentDetailsContext(); const { isAlert, timestamp } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser); - const alertDetailsLink = useGetAlertDetailsFlyoutLink({ - _id: eventId, - _index: indexName, + const alertDetailsLink = useGetFlyoutLink({ + eventId, + indexName, timestamp, }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_flyout_is_expandable.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_flyout_is_expandable.ts index f1f762fa9abdb9..8098acef40d27e 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_flyout_is_expandable.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_flyout_is_expandable.ts @@ -8,7 +8,7 @@ import { useMemo } from 'react'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; import { getField, getFieldArray } from '../../shared/utils'; -import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data'; +import type { GetFieldsData } from '../../shared/hooks/use_get_fields_data'; import { getRowRenderer } from '../../../../timelines/components/timeline/body/renderers/get_row_renderer'; import { defaultRowRenderers } from '../../../../timelines/components/timeline/body/renderers'; import { isEcsAllowedValue } from '../utils/event_utils'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_get_flyout_link.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_get_flyout_link.test.tsx new file mode 100644 index 00000000000000..2db21334e59f3d --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_get_flyout_link.test.tsx @@ -0,0 +1,37 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { useGetFlyoutLink } from './use_get_flyout_link'; +import { useGetAppUrl } from '@kbn/security-solution-navigation'; +import { ALERT_DETAILS_REDIRECT_PATH } from '../../../../../common/constants'; + +jest.mock('@kbn/security-solution-navigation'); + +const eventId = 'eventId'; +const indexName = 'indexName'; +const timestamp = 'timestamp'; + +describe('useGetFlyoutLink', () => { + it('should return url', () => { + (useGetAppUrl as jest.Mock).mockReturnValue({ + getAppUrl: (data: { path: string }) => data.path, + }); + + const hookResult = renderHook(() => + useGetFlyoutLink({ + eventId, + indexName, + timestamp, + }) + ); + + const origin = 'http://localhost'; + const path = `${ALERT_DETAILS_REDIRECT_PATH}/${eventId}?index=${indexName}×tamp=${timestamp}`; + expect(hookResult.result.current).toBe(`${origin}${path}`); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/use_get_alert_details_flyout_link.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_get_flyout_link.ts similarity index 64% rename from x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/use_get_alert_details_flyout_link.ts rename to x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_get_flyout_link.ts index 5838099820dd93..b4f50f1e89a3a4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/use_get_alert_details_flyout_link.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_get_flyout_link.ts @@ -11,19 +11,36 @@ import { DEFAULT_PREVIEW_INDEX } from '../../../../../common/constants'; import { buildAlertDetailPath } from '../../../../../common/utils/alert_detail_path'; import { useAppUrl } from '../../../../common/lib/kibana/hooks'; -// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462 -export const useGetAlertDetailsFlyoutLink = ({ - _id, - _index, - timestamp, -}: { - _id: string; - _index: string; +export interface UseGetFlyoutLinkProps { + /** + * Id of the document + */ + eventId: string; + /** + * Name of the index used in the parent's page + */ + indexName: string; + /** + * Timestamp of the document + */ timestamp: string; -}) => { +} + +/** + * Hook to get the link to the alert details page + */ +export const useGetFlyoutLink = ({ + eventId, + indexName, + timestamp, +}: UseGetFlyoutLinkProps): string | null => { const { getAppUrl } = useAppUrl(); - const alertDetailPath = buildAlertDetailPath({ alertId: _id, index: _index, timestamp }); - const isPreviewAlert = _index.includes(DEFAULT_PREVIEW_INDEX); + const alertDetailPath = buildAlertDetailPath({ + alertId: eventId, + index: indexName, + timestamp, + }); + const isPreviewAlert = indexName.includes(DEFAULT_PREVIEW_INDEX); // getAppUrl accounts for the users selected space const alertDetailsLink = useMemo(() => { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_process_data.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_process_data.ts index 8f02f371a53194..bb4e2be802a18e 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_process_data.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_process_data.ts @@ -7,7 +7,7 @@ import { useMemo } from 'react'; import { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils'; -import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data'; +import type { GetFieldsData } from '../../shared/hooks/use_get_fields_data'; import { getField } from '../../shared/utils'; import { useDocumentDetailsContext } from '../../shared/context'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.test.tsx index 64e10766ad21bd..0f6f233772626f 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.test.tsx @@ -10,7 +10,7 @@ import { renderHook } from '@testing-library/react-hooks'; import type { UseSessionPreviewParams } from './use_session_preview'; import { useSessionPreview } from './use_session_preview'; import type { SessionViewConfig } from '@kbn/securitysolution-data-table/common/types'; -import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data'; +import type { GetFieldsData } from '../../shared/hooks/use_get_fields_data'; import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser'; import { mockFieldData, mockGetFieldsData } from '../../shared/mocks/mock_get_fields_data'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.ts index 95c79e6815bfd4..4b2132d265871e 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.ts @@ -7,7 +7,7 @@ import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; import type { SessionViewConfig } from '@kbn/securitysolution-data-table/common/types'; -import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data'; +import type { GetFieldsData } from '../../shared/hooks/use_get_fields_data'; import { getField } from '../../shared/utils'; import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/context.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/context.tsx index bdfae953303c6d..12e2ad4f2a0b68 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/context.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/context.tsx @@ -15,7 +15,7 @@ import { FlyoutLoading } from '../../shared/components/flyout_loading'; import type { SearchHit } from '../../../../common/search_strategy'; import { useBasicDataFromDetailsData } from './hooks/use_basic_data_from_details_data'; import type { DocumentDetailsProps } from './types'; -import type { GetFieldsData } from '../../../common/hooks/use_get_fields_data'; +import type { GetFieldsData } from './hooks/use_get_fields_data'; import { useRuleWithFallback } from '../../../detection_engine/rule_management/logic/use_rule_with_fallback'; export interface DocumentDetailsContext { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_event_details.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_event_details.test.tsx index 7eb2c76573a275..de1020bac4d007 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_event_details.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_event_details.test.tsx @@ -8,22 +8,42 @@ import type { RenderHookResult } from '@testing-library/react-hooks'; import { renderHook } from '@testing-library/react-hooks'; import type { UseEventDetailsParams, UseEventDetailsResult } from './use_event_details'; -import { useEventDetails } from './use_event_details'; +import { getAlertIndexAlias, useEventDetails } from './use_event_details'; import { useSpaceId } from '../../../../common/hooks/use_space_id'; import { useRouteSpy } from '../../../../common/utils/route/use_route_spy'; import { useSourcererDataView } from '../../../../sourcerer/containers'; import { useTimelineEventsDetails } from '../../../../timelines/containers/details'; -import { useGetFieldsData } from '../../../../common/hooks/use_get_fields_data'; +import { useGetFieldsData } from './use_get_fields_data'; jest.mock('../../../../common/hooks/use_space_id'); jest.mock('../../../../common/utils/route/use_route_spy'); jest.mock('../../../../sourcerer/containers'); jest.mock('../../../../timelines/containers/details'); -jest.mock('../../../../common/hooks/use_get_fields_data'); +jest.mock('./use_get_fields_data'); const eventId = 'eventId'; const indexName = 'indexName'; +describe('getAlertIndexAlias', () => { + it('should handle default alert index', () => { + expect(getAlertIndexAlias('.internal.alerts-security.alerts')).toEqual( + '.alerts-security.alerts-default' + ); + }); + + it('should handle default preview index', () => { + expect(getAlertIndexAlias('.internal.preview.alerts-security.alerts')).toEqual( + '.preview.alerts-security.alerts-default' + ); + }); + + it('should handle non default space id', () => { + expect(getAlertIndexAlias('.internal.preview.alerts-security.alerts', 'test')).toEqual( + '.preview.alerts-security.alerts-test' + ); + }); +}); + describe('useEventDetails', () => { let hookResult: RenderHookResult; @@ -35,7 +55,7 @@ describe('useEventDetails', () => { indexPattern: {}, }); (useTimelineEventsDetails as jest.Mock).mockReturnValue([false, [], {}, {}, jest.fn()]); - jest.mocked(useGetFieldsData).mockReturnValue((field: string) => field); + jest.mocked(useGetFieldsData).mockReturnValue({ getFieldsData: (field: string) => field }); hookResult = renderHook(() => useEventDetails({ eventId, indexName })); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_event_details.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_event_details.ts index b039cc9573f35b..40acb8690ce640 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_event_details.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_event_details.ts @@ -9,16 +9,31 @@ import type { BrowserFields, TimelineEventsDetailsItem } from '@kbn/timelines-pl import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; import { SecurityPageName } from '@kbn/security-solution-navigation'; import type { DataViewBase } from '@kbn/es-query'; +import { DEFAULT_ALERTS_INDEX, DEFAULT_PREVIEW_INDEX } from '../../../../../common/constants'; import type { RunTimeMappings } from '../../../../../common/api/search_strategy'; import { useSpaceId } from '../../../../common/hooks/use_space_id'; -import { getAlertIndexAlias } from '../../../../timelines/components/side_panel/event_details/helpers'; import { useRouteSpy } from '../../../../common/utils/route/use_route_spy'; import { SourcererScopeName } from '../../../../sourcerer/store/model'; import { useSourcererDataView } from '../../../../sourcerer/containers'; import { useTimelineEventsDetails } from '../../../../timelines/containers/details'; -import { useGetFieldsData } from '../../../../common/hooks/use_get_fields_data'; import type { SearchHit } from '../../../../../common/search_strategy'; -import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data'; +import type { GetFieldsData } from './use_get_fields_data'; +import { useGetFieldsData } from './use_get_fields_data'; + +/** + * The referenced alert _index in the flyout uses the `.internal.` such as `.internal.alerts-security.alerts-spaceId` in the alert page flyout and .internal.preview.alerts-security.alerts-spaceId` in the rule creation preview flyout, + * but we always want to use their respective aliase indices rather than accessing their backing .internal. indices. + */ +export const getAlertIndexAlias = ( + index: string, + spaceId: string = 'default' +): string | undefined => { + if (index.startsWith(`.internal${DEFAULT_ALERTS_INDEX}`)) { + return `${DEFAULT_ALERTS_INDEX}-${spaceId}`; + } else if (index.startsWith(`.internal${DEFAULT_PREVIEW_INDEX}`)) { + return `${DEFAULT_PREVIEW_INDEX}-${spaceId}`; + } +}; export interface UseEventDetailsParams { /** @@ -90,7 +105,7 @@ export const useEventDetails = ({ runtimeMappings: sourcererDataView?.sourcererDataView?.runtimeFieldMap as RunTimeMappings, skip: !eventId, }); - const getFieldsData = useGetFieldsData(searchHit?.fields); + const { getFieldsData } = useGetFieldsData({ fieldsData: searchHit?.fields }); return { browserFields: sourcererDataView.browserFields, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_get_fields_data.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_get_fields_data.test.tsx new file mode 100644 index 00000000000000..fcf370b4bca1a0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_get_fields_data.test.tsx @@ -0,0 +1,36 @@ +/* + * 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 type { RenderHookResult } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react-hooks'; +import { mockSearchHit } from '../mocks/mock_search_hit'; +import type { UseGetFieldsDataParams, UseGetFieldsDataResult } from './use_get_fields_data'; +import { useGetFieldsData } from './use_get_fields_data'; + +const fieldsData = { + ...mockSearchHit.fields, + field: ['value'], +}; + +describe('useGetFieldsData', () => { + let hookResult: RenderHookResult; + + it('should return the value for a field', () => { + hookResult = renderHook(() => useGetFieldsData({ fieldsData })); + + const getFieldsData = hookResult.result.current.getFieldsData; + expect(getFieldsData('field')).toEqual(['value']); + expect(getFieldsData('wrong_field')).toEqual(undefined); + }); + + it('should handle undefined', () => { + hookResult = renderHook(() => useGetFieldsData({ fieldsData: undefined })); + + const getFieldsData = hookResult.result.current.getFieldsData; + expect(getFieldsData('field')).toEqual(undefined); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_get_fields_data.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_get_fields_data.ts similarity index 86% rename from x-pack/plugins/security_solution/public/common/hooks/use_get_fields_data.ts rename to x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_get_fields_data.ts index 12f6c5fbd0cbb3..3e055e3bc4f63d 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_get_fields_data.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_get_fields_data.ts @@ -7,7 +7,7 @@ import { useCallback, useMemo } from 'react'; import { getOr } from 'lodash/fp'; -import type { SearchHit } from '../../../common/search_strategy'; +import type { SearchHit } from '../../../../../common/search_strategy'; /** * Since the fields api may return a string array as well as an object array @@ -37,7 +37,6 @@ const getAllDotIndicesInReverse = (dotField: string): number[] => { /** * We get the dot paths so we can look up each path to see if any of the nested fields exist * */ - const getAllPotentialDotPaths = (dotField: string): string[][] => { const reverseDotIndices = getAllDotIndicesInReverse(dotField); @@ -49,6 +48,9 @@ const getAllPotentialDotPaths = (dotField: string): string[][] => { return pathTuples; }; +/** + * We get the nested value + */ const getNestedValue = (startPath: string, endPath: string, data: Record) => { const foundPrimaryPath = data[startPath]; if (Array.isArray(foundPrimaryPath)) { @@ -63,7 +65,7 @@ const getNestedValue = (startPath: string, endPath: string, data: Record GetFieldsDataValue; +export type GetFieldsData = (field: string) => string | string[] | null | undefined; + +export interface UseGetFieldsDataParams { + /** + * All fields from the searchHit result + */ + fieldsData: SearchHit['fields'] | undefined; +} + +export interface UseGetFieldsDataResult { + /** + * Retrieves the value for the provided field (reading from the searchHit result) + */ + getFieldsData: GetFieldsData; +} -// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462 -export const useGetFieldsData = (fieldsData: SearchHit['fields'] | undefined): GetFieldsData => { +/** + * Hook that returns a function to retrieve the values for a field (reading from the searchHit result) + */ +export const useGetFieldsData = ({ + fieldsData, +}: UseGetFieldsDataParams): UseGetFieldsDataResult => { // TODO: Move cache to top level container such as redux or context. Make it store type agnostic if possible // TODO: Handle updates where data is re-requested and the cache is reset. const cachedOriginalData = useMemo(() => fieldsData, [fieldsData]); @@ -111,7 +130,7 @@ export const useGetFieldsData = (fieldsData: SearchHit['fields'] | undefined): G [cachedExpensiveNestedValues] ); - return useCallback( + const getFieldsData = useCallback( (field: string) => { let fieldsValue; // Get an expensive value from the cache if it exists, otherwise search for the value @@ -133,4 +152,6 @@ export const useGetFieldsData = (fieldsData: SearchHit['fields'] | undefined): G }, [cacheNestedValues, cachedExpensiveNestedValues, cachedOriginalData] ); + + return { getFieldsData }; }; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_ancestry.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_ancestry.ts index 12172621b4df2f..69c0ae29893575 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_ancestry.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_ancestry.ts @@ -6,7 +6,7 @@ */ import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; -import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data'; +import type { GetFieldsData } from './use_get_fields_data'; import { useIsInvestigateInResolverActionEnabled } from '../../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver'; import { useLicense } from '../../../../common/hooks/use_license'; import { ANCESTOR_ID } from '../constants/field_names'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_same_source_event.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_same_source_event.ts index 2f76c74b329d1e..cbee250120a00a 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_same_source_event.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_same_source_event.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data'; +import type { GetFieldsData } from './use_get_fields_data'; import { ANCESTOR_ID } from '../constants/field_names'; import { getField } from '../utils'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_session.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_session.ts index 81ce4bdb0475ca..de7af4975da982 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_session.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_session.ts @@ -6,7 +6,7 @@ */ import { ENTRY_LEADER_ENTITY_ID } from '../constants/field_names'; -import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data'; +import type { GetFieldsData } from './use_get_fields_data'; import { getField } from '../utils'; export interface UseShowRelatedAlertsBySessionParams { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_cases.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_cases.ts index 4a739dd930e1d0..ba40f74417c5e9 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_cases.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_cases.ts @@ -7,7 +7,7 @@ import { APP_ID } from '../../../../../common'; import { useKibana } from '../../../../common/lib/kibana/kibana_react'; -import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data'; +import type { GetFieldsData } from './use_get_fields_data'; import { getField } from '../utils'; export interface UseShowRelatedCasesParams { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_suppressed_alerts.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_suppressed_alerts.ts index f459d83e5f3d43..df0abc1809f205 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_suppressed_alerts.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_suppressed_alerts.ts @@ -6,7 +6,7 @@ */ import { ALERT_SUPPRESSION_DOCS_COUNT } from '@kbn/rule-data-utils'; -import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data'; +import type { GetFieldsData } from './use_get_fields_data'; export interface ShowSuppressedAlertsParams { /** diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/mocks/mock_get_fields_data.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/mocks/mock_get_fields_data.ts index 4db7cf262580ac..bf8b8cbeae422e 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/mocks/mock_get_fields_data.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/mocks/mock_get_fields_data.ts @@ -12,7 +12,7 @@ import { ALERT_SUPPRESSION_DOCS_COUNT, } from '@kbn/rule-data-utils'; import { EventKind } from '../constants/event_kinds'; -import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data'; +import type { GetFieldsData } from '../hooks/use_get_fields_data'; export const mockFieldData: Record = { [ALERT_SEVERITY]: ['low'], From b82c49f825a2ba7cd8c28d33fc18fa18fefc39bb Mon Sep 17 00:00:00 2001 From: Kevin Lacabane Date: Mon, 19 Aug 2024 18:01:22 +0200 Subject: [PATCH 10/16] [kbn-data-forge] fix mongodb duplicate component name (#190660) Noticed this error during data forge resources cleanup caused by duplicated name ``` info Deleteing components for logs-mongodb@template (mongodb_8.0.0_base,mongodb_8.0.0_log,mongodb_8.0.0_host,mongodb_8.0.0_host) ERROR Failed to delete {"options":{"redaction":{"type":"replace","additionalKeys":[]}},"name":"ResponseError","meta":{"body":{"error":{"root_cause":[{"type":"resource_not_found_exception","reason":"mongodb_8.0.0_host"}],"type":"resource_not_found_exception","reason":"mongodb_8.0.0_host"}, ``` --- .../src/data_sources/fake_stack/mongodb/ecs/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/mongodb/ecs/index.ts b/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/mongodb/ecs/index.ts index 758c9f5b8e2ccd..f1a994a37a24c5 100644 --- a/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/mongodb/ecs/index.ts +++ b/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/mongodb/ecs/index.ts @@ -21,7 +21,7 @@ const components = [ { name: `${MONGODB}_${ECS_VERSION}_base`, template: base }, { name: `${MONGODB}_${ECS_VERSION}_log`, template: log }, { name: `${MONGODB}_${ECS_VERSION}_host`, template: host }, - { name: `${MONGODB}_${ECS_VERSION}_host`, template: mongodb }, + { name: `${MONGODB}_${ECS_VERSION}_mongodb`, template: mongodb }, ]; export const indexTemplate: IndexTemplateDef = { From f64392f32fc74efed2ab6dac8e05c6655f72073d Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 19 Aug 2024 17:12:09 +0100 Subject: [PATCH 11/16] skip flaky suite (#189791) --- .../timeline/tabs/query/query_tab_unified_components.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx index 1644982533a9e1..81be06e52e44f5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx @@ -292,7 +292,8 @@ describe('query tab with unified timeline', () => { ); }); - describe('pagination', () => { + // FLAKY: https://github.com/elastic/kibana/issues/189791 + describe.skip('pagination', () => { beforeEach(() => { // should return all the records instead just 3 // as the case in the default mock From a9e8d4a37a44c3ae9c3a70e90e67f399f68e5ec6 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 19 Aug 2024 17:13:20 +0100 Subject: [PATCH 12/16] skip flaky suite (#189792) --- .../timeline/tabs/query/query_tab_unified_components.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx index 81be06e52e44f5..4ee2c1565acd1b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx @@ -596,7 +596,8 @@ describe('query tab with unified timeline', () => { ); }); - describe('left controls', () => { + // FLAKY: https://github.com/elastic/kibana/issues/189792 + describe.skip('left controls', () => { it( 'should clear all sorting', async () => { From 6920cc13de846b48ec3cd3cb5775188c053fca29 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 19 Aug 2024 17:13:45 +0100 Subject: [PATCH 13/16] skip flaky suite (#189793) --- .../timeline/tabs/query/query_tab_unified_components.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx index 4ee2c1565acd1b..db808346fa4e32 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx @@ -597,6 +597,7 @@ describe('query tab with unified timeline', () => { }); // FLAKY: https://github.com/elastic/kibana/issues/189792 + // FLAKY: https://github.com/elastic/kibana/issues/189793 describe.skip('left controls', () => { it( 'should clear all sorting', From 754595b12396f20c0a547b15c889b355f97e7799 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 19 Aug 2024 17:14:20 +0100 Subject: [PATCH 14/16] skip flaky suite (#189794) --- .../timeline/tabs/query/query_tab_unified_components.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx index db808346fa4e32..8b7b3742f0f270 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx @@ -835,7 +835,8 @@ describe('query tab with unified timeline', () => { }); describe('Leading actions - notes', () => { - describe('securitySolutionNotesEnabled = true', () => { + // FLAKY: https://github.com/elastic/kibana/issues/189794 + describe.skip('securitySolutionNotesEnabled = true', () => { beforeEach(() => { (useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation( jest.fn((feature: keyof ExperimentalFeatures) => { From 4e31c9b9767431f460a7e5aa118a2c1cab68ebd7 Mon Sep 17 00:00:00 2001 From: Tomasz Ciecierski Date: Mon, 19 Aug 2024 18:48:29 +0200 Subject: [PATCH 15/16] [EDR Workflows] Add Telemetry to JAMF Analyzer schema (#190704) --- .../routes/resolver/entity/utils/supported_schemas.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity/utils/supported_schemas.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity/utils/supported_schemas.ts index fa50d587b99d49..62fbd7127f86e9 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity/utils/supported_schemas.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity/utils/supported_schemas.ts @@ -46,7 +46,12 @@ export const getSupportedSchemas = ( ? ['crowdstrike.falcon', 'crowdstrike.fdr', 'crowdstrike.alert'] : []), ...(jamfDataInAnalyzerEnabled - ? ['jamf_protect.alerts', 'jamf_protect.web-threat-events', 'jamf_protect.web-traffic-events'] + ? [ + 'jamf_protect.telemetry', + 'jamf_protect.alerts', + 'jamf_protect.web-threat-events', + 'jamf_protect.web-traffic-events', + ] : []), ]; From d0c13491227a166ed45e0831958f3946e8bdc6c8 Mon Sep 17 00:00:00 2001 From: Jordan <51442161+JordanSh@users.noreply.github.com> Date: Mon, 19 Aug 2024 20:13:34 +0300 Subject: [PATCH 16/16] [Cloud Security] Removing license gate keeping and displaying the table when there are findings (#190285) --- .../common/types_old.ts | 1 + ...ts => use_is_subscription_status_valid.ts} | 3 +- .../components/cloud_posture_page.test.tsx | 106 +------- .../public/components/cloud_posture_page.tsx | 23 -- .../policy_template_form.test.tsx | 65 ++++- .../fleet_extensions/policy_template_form.tsx | 256 ++++++++++-------- .../components/subscription_not_allowed.tsx | 9 +- .../public/components/test_subjects.ts | 2 + .../pages/benchmarks/benchmarks.test.tsx | 10 +- .../compliance_dashboard.test.tsx | 11 +- .../configurations/configurations.test.tsx | 33 ++- .../pages/configurations/configurations.tsx | 10 +- .../public/pages/rules/rules.test.tsx | 10 +- .../vulnerabilities/vulnerabilties.test.tsx | 10 +- .../vulnerability_dashboard.test.tsx | 10 +- .../cloud_security_posture/public/types.ts | 1 + .../routes/status/status.handlers.mock.ts | 41 +++ .../server/routes/status/status.ts | 46 ++++ .../status/status_indexed.ts | 49 ++++ 19 files changed, 396 insertions(+), 300 deletions(-) rename x-pack/plugins/cloud_security_posture/public/common/hooks/{use_subscription_status.ts => use_is_subscription_status_valid.ts} (94%) diff --git a/x-pack/plugins/cloud_security_posture/common/types_old.ts b/x-pack/plugins/cloud_security_posture/common/types_old.ts index f77ac4678a526a..19e18902b7ea1d 100644 --- a/x-pack/plugins/cloud_security_posture/common/types_old.ts +++ b/x-pack/plugins/cloud_security_posture/common/types_old.ts @@ -133,6 +133,7 @@ export interface BaseCspSetupStatus { vuln_mgmt: BaseCspSetupBothPolicy; isPluginInitialized: boolean; installedPackageVersion?: string | undefined; + hasMisconfigurationsFindings?: boolean; } export type CspSetupStatus = BaseCspSetupStatus; diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_subscription_status.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_is_subscription_status_valid.ts similarity index 94% rename from x-pack/plugins/cloud_security_posture/public/common/hooks/use_subscription_status.ts rename to x-pack/plugins/cloud_security_posture/public/common/hooks/use_is_subscription_status_valid.ts index f8bda84dbcb656..99ded40b04f632 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_subscription_status.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_is_subscription_status_valid.ts @@ -12,9 +12,10 @@ import { useKibana } from './use_kibana'; const SUBSCRIPTION_QUERY_KEY = 'csp_subscription_query_key'; -export const useSubscriptionStatus = () => { +export const useIsSubscriptionStatusValid = () => { const { licensing } = useKibana().services; const { isCloudEnabled } = useContext(SetupContext); + return useQuery([SUBSCRIPTION_QUERY_KEY], async () => { const license = await licensing.refresh(); return isSubscriptionAllowed(isCloudEnabled, license); diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.test.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.test.tsx index 05fedd4d8adec5..22fc6b4ae65f87 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import { useSubscriptionStatus } from '../common/hooks/use_subscription_status'; import Chance from 'chance'; import { DEFAULT_NO_DATA_TEST_SUBJECT, @@ -13,7 +12,6 @@ import { isCommonError, LOADING_STATE_TEST_SUBJECT, PACKAGE_NOT_INSTALLED_TEST_SUBJECT, - SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT, } from './cloud_posture_page'; import { createReactQueryResponse } from '../test/fixtures/react_query'; import { TestProvider } from '../test/test_provider'; @@ -23,27 +21,17 @@ import React, { ComponentProps } from 'react'; import { UseQueryResult } from '@tanstack/react-query'; import { CloudPosturePage } from './cloud_posture_page'; import { NoDataPage } from '@kbn/kibana-react-plugin/public'; -import { useLicenseManagementLocatorApi } from '../common/api/use_license_management_locator_api'; const chance = new Chance(); jest.mock('../common/api/use_setup_status_api'); jest.mock('../common/api/use_license_management_locator_api'); -jest.mock('../common/hooks/use_subscription_status'); +jest.mock('../common/hooks/use_is_subscription_status_valid'); jest.mock('../common/navigation/use_csp_integration_link'); describe('', () => { beforeEach(() => { jest.resetAllMocks(); - - (useSubscriptionStatus as jest.Mock).mockImplementation(() => - createReactQueryResponse({ - status: 'success', - data: true, - }) - ); - - (useLicenseManagementLocatorApi as jest.Mock).mockImplementation(undefined); }); const renderCloudPosturePage = ( @@ -72,101 +60,16 @@ describe('', () => { ); }; - it('renders with license url locator', () => { - (useSubscriptionStatus as jest.Mock).mockImplementation(() => - createReactQueryResponse({ - status: 'success', - data: false, - }) - ); - - (useLicenseManagementLocatorApi as jest.Mock).mockImplementation(() => 'http://license-url'); - - renderCloudPosturePage(); - - expect(screen.getByTestId('has_locator')).toBeInTheDocument(); - }); - - it('renders no license url locator', () => { - (useSubscriptionStatus as jest.Mock).mockImplementation(() => - createReactQueryResponse({ - status: 'success', - data: false, - }) - ); - - (useLicenseManagementLocatorApi as jest.Mock).mockImplementation(undefined); - - renderCloudPosturePage(); - expect(screen.getByTestId('no_locator')).toBeInTheDocument(); - }); - it('renders children if setup status is indexed', () => { const children = chance.sentence(); renderCloudPosturePage({ children }); expect(screen.getByText(children)).toBeInTheDocument(); expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); - expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument(); expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument(); }); - it('renders default loading state when the subscription query is loading', () => { - (useSubscriptionStatus as jest.Mock).mockImplementation( - () => - createReactQueryResponse({ - status: 'loading', - }) as unknown as UseQueryResult - ); - - const children = chance.sentence(); - renderCloudPosturePage({ children }); - - expect(screen.getByTestId(LOADING_STATE_TEST_SUBJECT)).toBeInTheDocument(); - expect(screen.queryByText(children)).not.toBeInTheDocument(); - expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); - expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument(); - expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument(); - }); - - it('renders default error state when the subscription query has an error', () => { - (useSubscriptionStatus as jest.Mock).mockImplementation( - () => - createReactQueryResponse({ - status: 'error', - error: new Error('error'), - }) as unknown as UseQueryResult - ); - - const children = chance.sentence(); - renderCloudPosturePage({ children }); - - expect(screen.getByTestId(ERROR_STATE_TEST_SUBJECT)).toBeInTheDocument(); - expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); - expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument(); - expect(screen.queryByText(children)).not.toBeInTheDocument(); - expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument(); - }); - - it('renders subscription not allowed prompt if subscription is not installed', () => { - (useSubscriptionStatus as jest.Mock).mockImplementation(() => - createReactQueryResponse({ - status: 'success', - data: false, - }) - ); - - const children = chance.sentence(); - renderCloudPosturePage({ children }); - - expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument(); - expect(screen.queryByText(children)).not.toBeInTheDocument(); - expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); - expect(screen.getByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).toBeInTheDocument(); - expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); - }); - it('renders default loading text when query isLoading', () => { const query = createReactQueryResponse({ status: 'loading', @@ -176,7 +79,6 @@ describe('', () => { renderCloudPosturePage({ children, query }); expect(screen.getByTestId(LOADING_STATE_TEST_SUBJECT)).toBeInTheDocument(); - expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument(); expect(screen.queryByText(children)).not.toBeInTheDocument(); expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument(); @@ -191,7 +93,6 @@ describe('', () => { renderCloudPosturePage({ children, query }); expect(screen.getByTestId(LOADING_STATE_TEST_SUBJECT)).toBeInTheDocument(); - expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument(); expect(screen.queryByText(children)).not.toBeInTheDocument(); expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument(); @@ -220,7 +121,6 @@ describe('', () => { expect(screen.getByText(text, { exact: false })).toBeInTheDocument() ); expect(screen.getByTestId(ERROR_STATE_TEST_SUBJECT)).toBeInTheDocument(); - expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument(); expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); expect(screen.queryByText(children)).not.toBeInTheDocument(); expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument(); @@ -253,7 +153,6 @@ describe('', () => { [error, statusCode].forEach((text) => expect(screen.queryByText(text)).not.toBeInTheDocument()); expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); - expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument(); expect(screen.queryByText(children)).not.toBeInTheDocument(); expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument(); }); @@ -275,7 +174,6 @@ describe('', () => { expect(screen.getByText(loading)).toBeInTheDocument(); expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); - expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument(); expect(screen.queryByText(children)).not.toBeInTheDocument(); expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument(); }); @@ -291,7 +189,6 @@ describe('', () => { expect(screen.getByTestId(DEFAULT_NO_DATA_TEST_SUBJECT)).toBeInTheDocument(); expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); - expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument(); expect(screen.queryByText(children)).not.toBeInTheDocument(); expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument(); @@ -320,7 +217,6 @@ describe('', () => { expect(screen.getByText(pageTitle)).toBeInTheDocument(); expect(screen.getAllByText(solution, { exact: false })[0]).toBeInTheDocument(); expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); - expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument(); expect(screen.queryByText(children)).not.toBeInTheDocument(); expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument(); diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.tsx index b2f1d892da9073..34ea821ed26203 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.tsx @@ -11,8 +11,6 @@ import { EuiEmptyPrompt } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { NoDataPage, NoDataPageProps } from '@kbn/kibana-react-plugin/public'; import { css } from '@emotion/react'; -import { SubscriptionNotAllowed } from './subscription_not_allowed'; -import { useSubscriptionStatus } from '../common/hooks/use_subscription_status'; import { FullSizeCenteredPage } from './full_size_centered_page'; import { CspLoadingState } from './csp_loading_state'; @@ -22,7 +20,6 @@ export const PACKAGE_NOT_INSTALLED_TEST_SUBJECT = 'cloud_posture_page_package_no export const CSPM_INTEGRATION_NOT_INSTALLED_TEST_SUBJECT = 'cloud_posture_page_cspm_not_installed'; export const KSPM_INTEGRATION_NOT_INSTALLED_TEST_SUBJECT = 'cloud_posture_page_kspm_not_installed'; export const DEFAULT_NO_DATA_TEST_SUBJECT = 'cloud_posture_page_no_data'; -export const SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT = 'cloud_posture_page_subscription_not_allowed'; interface CommonError { body: { @@ -150,12 +147,6 @@ export const defaultNoDataRenderer = () => ( ); -const subscriptionNotAllowedRenderer = () => ( - - - -); - interface CloudPosturePageProps { children: React.ReactNode; query?: UseQueryResult; @@ -171,21 +162,7 @@ export const CloudPosturePage = ({ errorRender = defaultErrorRenderer, noDataRenderer = defaultNoDataRenderer, }: CloudPosturePageProps) => { - const subscriptionStatus = useSubscriptionStatus(); - const render = () => { - if (subscriptionStatus.isError) { - return defaultErrorRenderer(subscriptionStatus.error); - } - - if (subscriptionStatus.isLoading) { - return defaultLoadingRenderer(); - } - - if (!subscriptionStatus.data) { - return subscriptionNotAllowedRenderer(); - } - if (!query) { return children; } diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx index 116170b2ac29c9..76134c4d41df02 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ import React from 'react'; -import { render, waitFor, within } from '@testing-library/react'; +import { render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { CspPolicyTemplateForm, @@ -53,9 +53,12 @@ import { GCP_CREDENTIALS_TYPE_OPTIONS_TEST_SUBJ, SETUP_TECHNOLOGY_SELECTOR_ACCORDION_TEST_SUBJ, SETUP_TECHNOLOGY_SELECTOR_TEST_SUBJ, + SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT, } from '../test_subjects'; import { ExperimentalFeaturesService } from '@kbn/fleet-plugin/public/services'; import { createFleetTestRendererMock } from '@kbn/fleet-plugin/public/mock'; +import { useIsSubscriptionStatusValid } from '../../common/hooks/use_is_subscription_status_valid'; +import { useLicenseManagementLocatorApi } from '../../common/api/use_license_management_locator_api'; // mock useParams jest.mock('react-router-dom', () => ({ @@ -66,6 +69,8 @@ jest.mock('react-router-dom', () => ({ })); jest.mock('../../common/api/use_setup_status_api'); jest.mock('../../common/api/use_package_policy_list'); +jest.mock('../../common/hooks/use_is_subscription_status_valid'); +jest.mock('../../common/api/use_license_management_locator_api'); jest.mock('@kbn/fleet-plugin/public/services/experimental_features'); const onChange = jest.fn(); @@ -85,9 +90,11 @@ describe('', () => { (useParams as jest.Mock).mockReturnValue({ integration: undefined, }); + mockedExperimentalFeaturesService.get.mockReturnValue({ secretsStorage: true, } as any); + (usePackagePolicyList as jest.Mock).mockImplementation((packageName) => createReactQueryResponseWithRefetch({ status: 'success', @@ -96,13 +103,22 @@ describe('', () => { }, }) ); + onChange.mockClear(); + (useCspSetupStatusApi as jest.Mock).mockImplementation(() => createReactQueryResponseWithRefetch({ status: 'success', data: { status: 'indexed', installedPackageVersion: '1.2.13' }, }) ); + + (useIsSubscriptionStatusValid as jest.Mock).mockImplementation(() => + createReactQueryResponse({ + status: 'success', + data: true, + }) + ); }); const WrappedComponent = ({ @@ -145,6 +161,53 @@ describe('', () => { ); }; + it('shows license block if subscription is not allowed', () => { + (useIsSubscriptionStatusValid as jest.Mock).mockImplementation(() => + createReactQueryResponse({ + status: 'success', + data: false, + }) + ); + + const policy = getMockPolicyK8s(); + const { rerender } = render(); + + rerender(); + expect(screen.getByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).toBeInTheDocument(); + }); + + it('license block renders with license url locator', () => { + (useIsSubscriptionStatusValid as jest.Mock).mockImplementation(() => + createReactQueryResponse({ + status: 'success', + data: false, + }) + ); + (useLicenseManagementLocatorApi as jest.Mock).mockImplementation(() => 'http://license-url'); + + const policy = getMockPolicyK8s(); + const { rerender } = render(); + + rerender(); + expect(screen.getByTestId('has_locator')).toBeInTheDocument(); + }); + + it('license block renders without license url locator', () => { + (useIsSubscriptionStatusValid as jest.Mock).mockImplementation(() => + createReactQueryResponse({ + status: 'success', + data: false, + }) + ); + (useLicenseManagementLocatorApi as jest.Mock).mockImplementation(undefined); + + const policy = getMockPolicyK8s(); + const { rerender } = render(); + + rerender(); + expect(screen.getByTestId('no_locator')).toBeInTheDocument(); + }); + it('updates package policy namespace to default when it changes', () => { const policy = getMockPolicyK8s(); const { rerender } = render(); diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx index 6a7d73868ced66..f70fb3a18f6900 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx @@ -30,6 +30,8 @@ import type { import { PackageInfo, PackagePolicy } from '@kbn/fleet-plugin/common'; import { useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; +import { useIsSubscriptionStatusValid } from '../../common/hooks/use_is_subscription_status_valid'; +import { SubscriptionNotAllowed } from '../subscription_not_allowed'; import { CspRadioGroupProps, RadioGroup } from './csp_boxed_radio_group'; import { assert } from '../../../common/utils/helpers'; import type { CloudSecurityPolicyTemplate, PostureInput } from '../../../common/types_old'; @@ -67,6 +69,7 @@ import { SetupTechnologySelector } from './setup_technology_selector/setup_techn import { useSetupTechnology } from './setup_technology_selector/use_setup_technology'; import { AZURE_CREDENTIALS_TYPE } from './azure_credentials_form/azure_credentials_form'; import { AWS_CREDENTIALS_TYPE } from './aws_credentials_form/aws_credentials_form'; +import { useKibana } from '../../common/hooks/use_kibana'; const DEFAULT_INPUT_TYPE = { kspm: CLOUDBEAT_VANILLA, @@ -537,6 +540,125 @@ const IntegrationSettings = ({ onChange, fields }: IntegrationInfoFieldsProps) = ); +const useEnsureDefaultNamespace = ({ + newPolicy, + input, + updatePolicy, +}: { + newPolicy: NewPackagePolicy; + input: NewPackagePolicyPostureInput; + updatePolicy: (policy: NewPackagePolicy) => void; +}) => { + useEffect(() => { + if (newPolicy.namespace === POSTURE_NAMESPACE) return; + + const policy = { ...getPosturePolicy(newPolicy, input.type), namespace: POSTURE_NAMESPACE }; + updatePolicy(policy); + }, [newPolicy, input, updatePolicy]); +}; + +const usePolicyTemplateInitialName = ({ + isEditPage, + isLoading, + integration, + newPolicy, + packagePolicyList, + updatePolicy, + setCanFetchIntegration, +}: { + isEditPage: boolean; + isLoading: boolean; + integration: CloudSecurityPolicyTemplate | undefined; + newPolicy: NewPackagePolicy; + packagePolicyList: PackagePolicy[] | undefined; + updatePolicy: (policy: NewPackagePolicy) => void; + setCanFetchIntegration: (canFetch: boolean) => void; +}) => { + useEffect(() => { + if (!integration) return; + if (isEditPage) return; + if (isLoading) return; + + const packagePolicyListByIntegration = packagePolicyList?.filter( + (policy) => policy?.vars?.posture?.value === integration + ); + + const currentIntegrationName = getMaxPackageName(integration, packagePolicyListByIntegration); + + if (newPolicy.name === currentIntegrationName) { + return; + } + + updatePolicy({ + ...newPolicy, + name: currentIntegrationName, + }); + setCanFetchIntegration(false); + // since this useEffect should only run on initial mount updatePolicy and newPolicy shouldn't re-trigger it + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isLoading, integration, isEditPage, packagePolicyList]); +}; + +const getSelectedOption = ( + options: NewPackagePolicyInput[], + policyTemplate: string = CSPM_POLICY_TEMPLATE +) => { + // Looks for the enabled deployment (aka input). By default, all inputs are disabled. + // Initial state when all inputs are disabled is to choose the first available of the relevant policyTemplate + // Default selected policy template is CSPM + const selectedOption = + options.find((i) => i.enabled) || + options.find((i) => i.policy_template === policyTemplate) || + options[0]; + + assert(selectedOption, 'Failed to determine selected option'); // We can't provide a default input without knowing the policy template + assert(isPostureInput(selectedOption), 'Unknown option: ' + selectedOption.type); + + return selectedOption; +}; + +/** + * Update CloudFormation template and stack name in the Agent Policy + * based on the selected policy template + */ +const useCloudFormationTemplate = ({ + packageInfo, + newPolicy, + updatePolicy, +}: { + packageInfo: PackageInfo; + newPolicy: NewPackagePolicy; + updatePolicy: (policy: NewPackagePolicy) => void; +}) => { + useEffect(() => { + const templateUrl = getVulnMgmtCloudFormationDefaultValue(packageInfo); + + // If the template is not available, do not update the policy + if (templateUrl === '') return; + + const checkCurrentTemplate = newPolicy?.inputs?.find( + (i: any) => i.type === CLOUDBEAT_VULN_MGMT_AWS + )?.config?.cloud_formation_template_url?.value; + + // If the template is already set, do not update the policy + if (checkCurrentTemplate === templateUrl) return; + + updatePolicy?.({ + ...newPolicy, + inputs: newPolicy.inputs.map((input) => { + if (input.type === CLOUDBEAT_VULN_MGMT_AWS) { + return { + ...input, + config: { cloud_formation_template_url: { value: templateUrl } }, + }; + } + return input; + }), + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [newPolicy?.vars?.cloud_formation_template_url, newPolicy, packageInfo]); +}; + export const CspPolicyTemplateForm = memo( ({ newPolicy, @@ -553,7 +675,11 @@ export const CspPolicyTemplateForm = memo