diff --git a/.buildkite/scripts/steps/serverless/deploy.sh b/.buildkite/scripts/steps/serverless/deploy.sh index 0c6f52b6f1982..325aadf187b5b 100644 --- a/.buildkite/scripts/steps/serverless/deploy.sh +++ b/.buildkite/scripts/steps/serverless/deploy.sh @@ -160,9 +160,12 @@ EOF is_pr_with_label "ci:project-deploy-elasticsearch" && deploy "elasticsearch" if is_pr_with_label "ci:project-deploy-observability" ; then - create_github_issue_oblt_test_environments - echo "--- Deploy observability with Kibana CI" - deploy "observability" + # Only deploy observability if the PR is targeting main + if [[ "$BUILDKITE_PULL_REQUEST_BASE_BRANCH" == "main" ]]; then + create_github_issue_oblt_test_environments + echo "--- Deploy observability with Kibana CI" + deploy "observability" + fi fi is_pr_with_label "ci:project-deploy-security" && deploy "security" diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8e93aeee0dae1..98ff703cf0968 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -407,6 +407,7 @@ packages/kbn-eslint-plugin-imports @elastic/kibana-operations packages/kbn-eslint-plugin-telemetry @elastic/obs-knowledge-team examples/eso_model_version_example @elastic/kibana-security x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin @elastic/kibana-security +src/plugins/esql @elastic/kibana-esql packages/kbn-esql-ast @elastic/kibana-esql examples/esql_ast_inspector @elastic/kibana-esql src/plugins/esql_datagrid @elastic/kibana-esql @@ -422,7 +423,7 @@ x-pack/plugins/event_log @elastic/response-ops packages/kbn-expandable-flyout @elastic/security-threat-hunting-investigations packages/kbn-expect @elastic/kibana-operations @elastic/appex-qa x-pack/examples/exploratory_view_example @elastic/obs-ux-infra_services-team -x-pack/plugins/observability_solution/exploratory_view @elastic/obs-ux-infra_services-team +x-pack/plugins/observability_solution/exploratory_view @elastic/obs-ux-management-team src/plugins/expression_error @elastic/kibana-presentation src/plugins/chart_expressions/expression_gauge @elastic/kibana-visualizations src/plugins/chart_expressions/expression_heatmap @elastic/kibana-visualizations @@ -658,6 +659,7 @@ packages/react/kibana_context/root @elastic/appex-sharedux packages/react/kibana_context/styled @elastic/appex-sharedux packages/react/kibana_context/theme @elastic/appex-sharedux packages/react/kibana_mount @elastic/appex-sharedux +packages/kbn-recently-accessed @elastic/appex-sharedux x-pack/plugins/remote_clusters @elastic/kibana-management test/plugin_functional/plugins/rendering_plugin @elastic/kibana-core packages/kbn-repo-file-maps @elastic/kibana-operations @@ -848,7 +850,7 @@ test/server_integration/plugins/status_plugin_b @elastic/kibana-core packages/kbn-std @elastic/kibana-core packages/kbn-stdio-dev-helpers @elastic/kibana-operations packages/kbn-storybook @elastic/kibana-operations -x-pack/plugins/observability_solution/synthetics @elastic/obs-ux-infra_services-team +x-pack/plugins/observability_solution/synthetics @elastic/obs-ux-management-team x-pack/test/alerting_api_integration/common/plugins/task_manager_fixture @elastic/response-ops x-pack/test/plugin_api_perf/plugins/task_manager_performance @elastic/response-ops x-pack/plugins/task_manager @elastic/response-ops @@ -865,7 +867,6 @@ packages/kbn-test-jest-helpers @elastic/kibana-operations @elastic/appex-qa packages/kbn-test-subj-selector @elastic/kibana-operations @elastic/appex-qa x-pack/examples/testing_embedded_lens @elastic/kibana-visualizations packages/kbn-text-based-editor @elastic/kibana-esql -src/plugins/text_based_languages @elastic/kibana-esql x-pack/examples/third_party_lens_navigation_prompt @elastic/kibana-visualizations x-pack/examples/third_party_vis_lens_example @elastic/kibana-visualizations x-pack/plugins/threat_intelligence @elastic/security-threat-hunting-investigations @@ -904,7 +905,7 @@ src/plugins/unified_search @elastic/kibana-visualizations packages/kbn-unsaved-changes-badge @elastic/kibana-data-discovery packages/kbn-unsaved-changes-prompt @elastic/kibana-management x-pack/plugins/upgrade_assistant @elastic/kibana-management -x-pack/plugins/observability_solution/uptime @elastic/obs-ux-infra_services-team +x-pack/plugins/observability_solution/uptime @elastic/obs-ux-management-team x-pack/plugins/drilldowns/url_drilldown @elastic/appex-sharedux src/plugins/url_forwarding @elastic/kibana-visualizations src/plugins/usage_collection @elastic/kibana-core @@ -1142,15 +1143,15 @@ x-pack/test/observability_ai_assistant_functional @elastic/obs-ai-assistant #CC# /x-pack/plugins/observability_solution/observability/ @elastic/apm-ui # Uptime -/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/uptime/ @elastic/obs-ux-infra_services-team -/x-pack/test/functional/apps/uptime @elastic/obs-ux-infra_services-team -/x-pack/test/functional/es_archives/uptime @elastic/obs-ux-infra_services-team -/x-pack/test/functional/services/uptime @elastic/obs-ux-infra_services-team -/x-pack/test/api_integration/apis/uptime @elastic/obs-ux-infra_services-team -/x-pack/test/api_integration/apis/synthetics @elastic/obs-ux-infra_services-team -/x-pack/test/alerting_api_integration/observability/synthetics_rule.ts @elastic/obs-ux-infra_services-team +/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/uptime/ @elastic/obs-ux-management-team +/x-pack/test/functional/apps/uptime @elastic/obs-ux-management-team +/x-pack/test/functional/es_archives/uptime @elastic/obs-ux-management-team +/x-pack/test/functional/services/uptime @elastic/obs-ux-management-team +/x-pack/test/api_integration/apis/uptime @elastic/obs-ux-management-team +/x-pack/test/api_integration/apis/synthetics @elastic/obs-ux-management-team +/x-pack/test/alerting_api_integration/observability/synthetics_rule.ts @elastic/obs-ux-management-team /x-pack/test/alerting_api_integration/observability/index.ts @elastic/obs-ux-management-team -/x-pack/test_serverless/api_integration/test_suites/observability/synthetics @elastic/obs-ux-infra_services-team +/x-pack/test_serverless/api_integration/test_suites/observability/synthetics @elastic/obs-ux-management-team # Logs /x-pack/test/api_integration/apis/logs_ui @elastic/obs-ux-logs-team @@ -1734,9 +1735,9 @@ packages/react @elastic/appex-sharedux x-pack/plugins/actions/server/saved_objects/index.ts @elastic/response-ops @elastic/kibana-security x-pack/plugins/alerting/server/saved_objects/index.ts @elastic/response-ops @elastic/kibana-security x-pack/plugins/fleet/server/saved_objects/index.ts @elastic/fleet @elastic/kibana-security -x-pack/plugins/observability_solution/synthetics/server/saved_objects/saved_objects.ts @elastic/obs-ux-infra_services-team @elastic/kibana-security -x-pack/plugins/observability_solution/synthetics/server/saved_objects/synthetics_monitor.ts @elastic/obs-ux-infra_services-team @elastic/kibana-security -x-pack/plugins/observability_solution/synthetics/server/saved_objects/synthetics_param.ts @elastic/obs-ux-infra_services-team @elastic/kibana-security +x-pack/plugins/observability_solution/synthetics/server/saved_objects/saved_objects.ts @elastic/obs-ux-management-team @elastic/kibana-security +x-pack/plugins/observability_solution/synthetics/server/saved_objects/synthetics_monitor.ts @elastic/obs-ux-management-team @elastic/kibana-security +x-pack/plugins/observability_solution/synthetics/server/saved_objects/synthetics_param.ts @elastic/obs-ux-management-team @elastic/kibana-security # Specialised GitHub workflows for the Observability robots /.github/workflows/deploy-my-kibana.yml @elastic/observablt-robots @elastic/kibana-operations diff --git a/.i18nrc.json b/.i18nrc.json index 63adc5c5aae2b..bab7cdc68d81d 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -116,7 +116,7 @@ "serverlessPackages": "packages/serverless", "coloring": "packages/kbn-coloring/src", "languageDocumentationPopover": "packages/kbn-language-documentation-popover/src", - "textBasedLanguages": "src/plugins/text_based_languages", + "esql": "src/plugins/esql", "esqlDataGrid": "src/plugins/esql_datagrid", "statusPage": "src/legacy/core_plugins/status_page", "telemetry": ["src/plugins/telemetry", "src/plugins/telemetry_management_section"], diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index e4f161ac8f4e5..0f20d331118cc 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -102,6 +102,10 @@ This API doesn't support angular, for registering angular dev tools, bootstrap a |Embeddables are React components that manage their own state, can be serialized and deserialized, and return an API that can be used to interact with them imperatively. +|{kib-repo}blob/{branch}/src/plugins/esql/README.md[esql] +|The editor accepts the following properties: + + |{kib-repo}blob/{branch}/src/plugins/esql_datagrid/README.md[esqlDataGrid] |Contains a Discover-like table specifically for ES|QL queries: @@ -328,10 +332,6 @@ generating deep links to other apps using locators, and creating short URLs. |This plugin adds the Advanced Settings section for the Usage and Security Data collection (aka Telemetry). -|{kib-repo}blob/{branch}/src/plugins/text_based_languages/README.md[textBasedLanguages] -|The editor accepts the following properties: - - |<> |UI Actions plugins provides API to manage *triggers* and *actions*. diff --git a/package.json b/package.json index e9a8d26a23c8e..1370f8012f3f3 100644 --- a/package.json +++ b/package.json @@ -454,6 +454,7 @@ "@kbn/es-ui-shared-plugin": "link:src/plugins/es_ui_shared", "@kbn/eso-model-version-example": "link:examples/eso_model_version_example", "@kbn/eso-plugin": "link:x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin", + "@kbn/esql": "link:src/plugins/esql", "@kbn/esql-ast": "link:packages/kbn-esql-ast", "@kbn/esql-ast-inspector-plugin": "link:examples/esql_ast_inspector", "@kbn/esql-datagrid": "link:src/plugins/esql_datagrid", @@ -674,6 +675,7 @@ "@kbn/react-kibana-context-styled": "link:packages/react/kibana_context/styled", "@kbn/react-kibana-context-theme": "link:packages/react/kibana_context/theme", "@kbn/react-kibana-mount": "link:packages/react/kibana_mount", + "@kbn/recently-accessed": "link:packages/kbn-recently-accessed", "@kbn/remote-clusters-plugin": "link:x-pack/plugins/remote_clusters", "@kbn/rendering-plugin": "link:test/plugin_functional/plugins/rendering_plugin", "@kbn/repo-info": "link:packages/kbn-repo-info", @@ -864,7 +866,6 @@ "@kbn/test-feature-usage-plugin": "link:x-pack/test/licensing_plugin/plugins/test_feature_usage", "@kbn/testing-embedded-lens-plugin": "link:x-pack/examples/testing_embedded_lens", "@kbn/text-based-editor": "link:packages/kbn-text-based-editor", - "@kbn/text-based-languages": "link:src/plugins/text_based_languages", "@kbn/third-party-lens-navigation-prompt-plugin": "link:x-pack/examples/third_party_lens_navigation_prompt", "@kbn/third-party-vis-lens-example-plugin": "link:x-pack/examples/third_party_vis_lens_example", "@kbn/threat-intelligence-plugin": "link:x-pack/plugins/threat_intelligence", diff --git a/packages/content-management/table_list_view/src/table_list_view.tsx b/packages/content-management/table_list_view/src/table_list_view.tsx index a5150c959e4f5..06c2566936d5a 100644 --- a/packages/content-management/table_list_view/src/table_list_view.tsx +++ b/packages/content-management/table_list_view/src/table_list_view.tsx @@ -38,6 +38,7 @@ export type TableListViewProps & { title: string; description?: string; @@ -75,6 +76,7 @@ export const TableListView = ({ additionalRightSideActions, withoutPageTemplateWrapper, createdByEnabled, + recentlyAccessed, }: TableListViewProps) => { const PageTemplate = withoutPageTemplateWrapper ? (React.Fragment as unknown as typeof KibanaPageTemplate) @@ -124,6 +126,7 @@ export const TableListView = ({ onFetchSuccess={onFetchSuccess} setPageDataTestSubject={setPageDataTestSubject} createdByEnabled={createdByEnabled} + recentlyAccessed={recentlyAccessed} /> diff --git a/packages/content-management/table_list_view_table/src/components/table.tsx b/packages/content-management/table_list_view_table/src/components/table.tsx index dc3061e1e4802..058c12eba5bf3 100644 --- a/packages/content-management/table_list_view_table/src/components/table.tsx +++ b/packages/content-management/table_list_view_table/src/components/table.tsx @@ -13,7 +13,6 @@ import { EuiButton, EuiInMemoryTable, CriteriaWithPagination, - PropertySort, SearchFilterConfig, Direction, Query, @@ -59,6 +58,7 @@ interface Props extends State, TagManageme tableCaption: string; tableColumns: Array>; hasUpdatedAtMetadata: boolean; + hasRecentlyAccessedMetadata: boolean; deleteItems: TableListViewTableProps['deleteItems']; tableItemsRowActions: TableItemsRowActions; renderCreateButton: () => React.ReactElement | undefined; @@ -81,6 +81,7 @@ export function Table({ tableSort, tableFilter, hasUpdatedAtMetadata, + hasRecentlyAccessedMetadata, entityName, entityNamePlural, tagsToTableItemMap, @@ -174,12 +175,13 @@ export function Table({ ); }, }; - }, [hasUpdatedAtMetadata, onSortChange, tableSort]); + }, [hasUpdatedAtMetadata, onSortChange, tableSort, hasRecentlyAccessedMetadata]); const tagFilterPanel = useMemo(() => { if (!isTaggingEnabled()) return null; @@ -278,6 +280,11 @@ export function Table({ return { allUsers: Array.from(users), showNoUserOption: _showNoUserOption }; }, [createdByEnabled, items]); + const sorting = + tableSort.field === 'accessedAt' // "accessedAt" is a special case with a custom sorting + ? true // by passing "true" we disable the EuiInMemoryTable sorting and handle it ourselves, but sorting is still enabled + : { sort: tableSort }; + return ( ({ selection={selection} search={search} executeQueryOptions={{ enabled: false }} - sorting={tableSort ? { sort: tableSort as PropertySort } : undefined} + sorting={sorting} onChange={onTableChange} data-test-subj="itemsInMemTable" rowHeader="attributes.title" diff --git a/packages/content-management/table_list_view_table/src/components/table_sort_select.test.tsx b/packages/content-management/table_list_view_table/src/components/table_sort_select.test.tsx new file mode 100644 index 0000000000000..e2c62a46c0e71 --- /dev/null +++ b/packages/content-management/table_list_view_table/src/components/table_sort_select.test.tsx @@ -0,0 +1,64 @@ +/* + * 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 { sortByRecentlyAccessed } from './table_sort_select'; +import { UserContentCommonSchema } from '@kbn/content-management-table-list-view-common'; + +describe('sortByRecentlyAccessed', () => { + const items: UserContentCommonSchema[] = [ + { + id: 'item-1', + type: 'dashboard', + updatedAt: '2020-01-01T00:00:00Z', + attributes: { + title: 'Item 1', + }, + references: [], + }, + { + id: 'item-2', + type: 'dashboard', + updatedAt: '2020-01-02T00:00:00Z', + attributes: { + title: 'Item 2', + }, + createdBy: 'u_1', + references: [], + }, + { + id: 'item-3', + type: 'dashboard', + updatedAt: '2020-01-03T00:00:00Z', + attributes: { + title: 'Item 3', + }, + createdBy: 'u_2', + references: [], + }, + { + id: 'item-4', + type: 'dashboard', + updatedAt: '2020-01-04T00:00:00Z', + attributes: { + title: 'Item 4', + }, + references: [], + managed: true, + }, + ]; + + test('sort by last updated', () => { + const sortedItems = sortByRecentlyAccessed(items, []); + expect(sortedItems.map((item) => item.id)).toEqual(['item-4', 'item-3', 'item-2', 'item-1']); + }); + + test('pulls recently accessed to the top', () => { + const sortedItems = sortByRecentlyAccessed(items, [{ id: 'item-1' }, { id: 'item-2' }]); + expect(sortedItems.map((item) => item.id)).toEqual(['item-1', 'item-2', 'item-4', 'item-3']); + }); +}); diff --git a/packages/content-management/table_list_view_table/src/components/table_sort_select.tsx b/packages/content-management/table_list_view_table/src/components/table_sort_select.tsx index c9e06a29e9c8c..1b5d9080f8205 100644 --- a/packages/content-management/table_list_view_table/src/components/table_sort_select.tsx +++ b/packages/content-management/table_list_view_table/src/components/table_sort_select.tsx @@ -16,8 +16,10 @@ import { Direction, EuiText, useEuiTheme, + EuiIconTip, } from '@elastic/eui'; import { css } from '@emotion/react'; +import { UserContentCommonSchema } from '@kbn/content-management-table-list-view-common'; import { State } from '../table_list_view_table'; @@ -26,9 +28,15 @@ type SortItem = EuiSelectableOption & { direction: Direction; }; -export type SortColumnField = 'updatedAt' | 'attributes.title'; +export type SortColumnField = 'updatedAt' | 'attributes.title' | 'accessedAt'; const i18nText = { + accessedDescSort: i18n.translate( + 'contentManagement.tableList.listing.tableSortSelect.recentlyAccessedLabel', + { + defaultMessage: 'Recently viewed', + } + ), nameAscSort: i18n.translate('contentManagement.tableList.listing.tableSortSelect.nameAscLabel', { defaultMessage: 'Name A-Z', }), @@ -57,11 +65,17 @@ const i18nText = { interface Props { hasUpdatedAtMetadata: boolean; + hasRecentlyAccessedMetadata: boolean; tableSort: State['tableSort']; onChange?: (column: SortColumnField, direction: Direction) => void; } -export function TableSortSelect({ tableSort, hasUpdatedAtMetadata, onChange }: Props) { +export function TableSortSelect({ + tableSort, + hasUpdatedAtMetadata, + hasRecentlyAccessedMetadata, + onChange, +}: Props) { const { euiTheme } = useEuiTheme(); const [isPopoverOpen, setIsPopoverOpen] = useState(false); @@ -81,6 +95,40 @@ export function TableSortSelect({ tableSort, hasUpdatedAtMetadata, onChange }: P }, ]; + if (hasRecentlyAccessedMetadata) { + opts = [ + { + label: i18nText.accessedDescSort, + column: + 'accessedAt' /* nonexistent field, used to identify this custom type of sorting */, + direction: 'desc', + append: ( + + ), + }, + ...opts, + ]; + } + if (hasUpdatedAtMetadata) { opts = opts.concat([ { @@ -100,6 +148,7 @@ export function TableSortSelect({ tableSort, hasUpdatedAtMetadata, onChange }: P return opts; }); + const selectedOptionLabel = options.find(({ checked }) => checked === 'on')?.label ?? ''; const panelHeaderCSS = css` @@ -165,8 +214,11 @@ export function TableSortSelect({ tableSort, hasUpdatedAtMetadata, onChange }: P <> {i18nText.headerSort} - singleSelection - aria-label="some aria label" + singleSelection={'always'} + aria-label={i18n.translate( + 'contentManagement.tableList.listing.tableSortSelect.sortingOptionsAriaLabel', + { defaultMessage: 'Sorting options' } + )} options={options} onChange={onSelectChange} data-test-subj="sortSelect" @@ -214,3 +266,25 @@ export function saveSorting( /* empty */ } } + +/** + * Default custom sorting for the table when recently accessed info is available + * Sorts by recently accessed list first and the by lastUpdatedAt + */ +export function sortByRecentlyAccessed( + items: T[], + recentlyAccessed: Array<{ id: string }> +) { + const recentlyAccessedMap = new Map(recentlyAccessed.map((item, index) => [item.id, index])); + return [...items].sort((a, b) => { + if (recentlyAccessedMap.has(a.id) && recentlyAccessedMap.has(b.id)) { + return recentlyAccessedMap.get(a.id)! - recentlyAccessedMap.get(b.id)!; + } else if (recentlyAccessedMap.has(a.id)) { + return -1; + } else if (recentlyAccessedMap.has(b.id)) { + return 1; + } else { + return a.updatedAt > b.updatedAt ? -1 : 1; + } + }); +} diff --git a/packages/content-management/table_list_view_table/src/reducer.tsx b/packages/content-management/table_list_view_table/src/reducer.tsx index b4cf3691f9d75..d239fad38c724 100644 --- a/packages/content-management/table_list_view_table/src/reducer.tsx +++ b/packages/content-management/table_list_view_table/src/reducer.tsx @@ -33,11 +33,18 @@ export function getReducer() { // Only change the table sort if it hasn't been changed already. // For example if its state comes from the URL, we don't want to override it here. - if (hasUpdatedAtMetadata && !state.sortColumnChanged) { - tableSort = { - field: 'updatedAt' as const, - direction: 'desc' as const, - }; + if (!state.sortColumnChanged) { + if (state.hasRecentlyAccessedMetadata) { + tableSort = { + field: 'accessedAt' as const, + direction: 'desc' as const, + }; + } else if (hasUpdatedAtMetadata) { + tableSort = { + field: 'updatedAt' as const, + direction: 'desc' as const, + }; + } } } diff --git a/packages/content-management/table_list_view_table/src/table_list_view.test.tsx b/packages/content-management/table_list_view_table/src/table_list_view.test.tsx index c874747799480..e56322099d5ff 100644 --- a/packages/content-management/table_list_view_table/src/table_list_view.test.tsx +++ b/packages/content-management/table_list_view_table/src/table_list_view.test.tsx @@ -653,6 +653,91 @@ describe('TableListView', () => { }); }); + describe('column sorting with recently accessed', () => { + const setupColumnSorting = registerTestBed( + WithServices(TableListViewTable, { + TagList: getTagList({ references: [] }), + }), + { + defaultProps: { + ...requiredProps, + recentlyAccessed: { get: () => [{ id: '123', link: '', label: '' }] }, + }, + memoryRouter: { wrapComponent: true }, + } + ); + + const hits: UserContentCommonSchema[] = [ + { + id: '123', + updatedAt: twoDaysAgo.toISOString(), // first asc, last desc + type: 'dashboard', + attributes: { + title: 'z-foo', // first desc, last asc + }, + references: [{ id: 'id-tag-1', name: 'tag-1', type: 'tag' }], + }, + { + id: '456', + updatedAt: yesterday.toISOString(), // first desc, last asc + type: 'dashboard', + attributes: { + title: 'a-foo', // first asc, last desc + }, + references: [], + }, + ]; + + test('should initially sort by "Recently Accessed"', async () => { + let testBed: TestBed; + + await act(async () => { + testBed = await setupColumnSorting({ + findItems: jest.fn().mockResolvedValue({ total: hits.length, hits }), + }); + }); + + const { component, table } = testBed!; + component.update(); + + const { tableCellsValues } = table.getMetaData('itemsInMemTable'); + + expect(tableCellsValues).toEqual([ + ['z-foo', twoDaysAgoToString], + ['a-foo', yesterdayToString], + ]); + }); + + test('filter select should have 5 options', async () => { + let testBed: TestBed; + + await act(async () => { + testBed = await setupColumnSorting({ + findItems: jest.fn().mockResolvedValue({ total: hits.length, hits }), + }); + }); + const { openSortSelect } = getActions(testBed!); + const { component, find } = testBed!; + component.update(); + + act(() => { + openSortSelect(); + }); + component.update(); + + const filterOptions = find('sortSelect').find('li'); + + expect(filterOptions.length).toBe(5); + expect(filterOptions.map((wrapper) => wrapper.text())).toEqual([ + 'Recently viewed. Checked option.Additional information ', + 'Name A-Z ', + 'Name Z-A ', + 'Recently updated ', + 'Least recently updated ', + ]); + }); + }); + describe('content editor', () => { const setupInspector = registerTestBed( WithServices(TableListViewTable), diff --git a/packages/content-management/table_list_view_table/src/table_list_view_table.tsx b/packages/content-management/table_list_view_table/src/table_list_view_table.tsx index 9e69c4e0f4434..888e0a312d049 100644 --- a/packages/content-management/table_list_view_table/src/table_list_view_table.tsx +++ b/packages/content-management/table_list_view_table/src/table_list_view_table.tsx @@ -37,6 +37,7 @@ import type { SavedObjectsReference, } from '@kbn/content-management-content-editor'; import type { UserContentCommonSchema } from '@kbn/content-management-table-list-view-common'; +import type { RecentlyAccessed } from '@kbn/recently-accessed'; import { Table, @@ -52,6 +53,7 @@ import { type SortColumnField, getInitialSorting, saveSorting } from './componen import { useTags } from './use_tags'; import { useInRouterContext, useUrlState } from './use_url_state'; import { RowActions, TableItemsRowActions } from './types'; +import { sortByRecentlyAccessed } from './components/table_sort_select'; interface ContentEditorConfig extends Pick< @@ -116,6 +118,7 @@ export interface TableListViewTableProps< */ withoutPageTemplateWrapper?: boolean; contentEditor?: ContentEditorConfig; + recentlyAccessed?: Pick; tableCaption: string; /** Flag to force a new fetch of the table items. Whenever it changes, the `findItems()` will be called. */ @@ -145,6 +148,7 @@ export interface State { // in the query params. We might want to stop supporting both in a future release (v9.0?) stateFromURL.s = sanitizedParams.s ?? sanitizedParams.title; - if (sanitizedParams.sort === 'title' || sanitizedParams.sort === 'updatedAt') { - const field = sanitizedParams.sort === 'title' ? 'attributes.title' : 'updatedAt'; + if ( + sanitizedParams.sort === 'title' || + sanitizedParams.sort === 'updatedAt' || + sanitizedParams.sort === 'accessedAt' + ) { + const field = + sanitizedParams.sort === 'title' + ? 'attributes.title' + : sanitizedParams.sort === 'accessedAt' + ? 'accessedAt' + : 'updatedAt'; - stateFromURL.sort = { field, direction: 'asc' }; + stateFromURL.sort = { field, direction: field === 'attributes.title' ? 'asc' : 'desc' }; if (sanitizedParams.sortdir === 'desc' || sanitizedParams.sortdir === 'asc') { stateFromURL.sort.direction = sanitizedParams.sortdir; @@ -302,6 +315,7 @@ function TableListViewTableComp({ refreshListBouncer, setPageDataTestSubject, createdByEnabled = false, + recentlyAccessed, }: TableListViewTableProps) { useEffect(() => { setPageDataTestSubject(`${entityName}LandingPage`); @@ -373,6 +387,7 @@ function TableListViewTableComp({ showDeleteModal: false, hasUpdatedAtMetadata: false, hasCreatedByMetadata: false, + hasRecentlyAccessedMetadata: recentlyAccessed ? recentlyAccessed.get().length > 0 : false, selectedIds: [], searchQuery: { text: '', query: new Query(Ast.create([]), undefined, '') }, pagination: { @@ -387,7 +402,7 @@ function TableListViewTableComp({ createdBy: [], }, }; - }, [initialPageSize, entityName]); + }, [initialPageSize, entityName, recentlyAccessed]); const [state, dispatch] = useReducer(reducer, initialState); @@ -404,6 +419,7 @@ function TableListViewTableComp({ totalItems, hasUpdatedAtMetadata, hasCreatedByMetadata, + hasRecentlyAccessedMetadata, pagination, tableSort, tableFilter, @@ -433,6 +449,12 @@ function TableListViewTableComp({ } if (idx === fetchIdx.current) { + // when recentlyAccessed is available, we sort the items by the recently accessed items + // then this sort will be used as the default sort for the table + if (recentlyAccessed && recentlyAccessed.get().length > 0) { + response.hits = sortByRecentlyAccessed(response.hits, recentlyAccessed.get()); + } + dispatch({ type: 'onFetchItemsSuccess', data: { @@ -448,7 +470,7 @@ function TableListViewTableComp({ data: err, }); } - }, [searchQueryParser, searchQuery.text, findItems, onFetchSuccess]); + }, [searchQueryParser, searchQuery.text, findItems, onFetchSuccess, recentlyAccessed]); const updateQuery = useCallback( (query: Query) => { @@ -1109,6 +1131,7 @@ function TableListViewTableComp({ searchQuery={searchQuery} tableColumns={tableColumns} hasUpdatedAtMetadata={hasUpdatedAtMetadata} + hasRecentlyAccessedMetadata={hasRecentlyAccessedMetadata} tableSort={tableSort} tableFilter={tableFilter} tableItemsRowActions={tableItemsRowActions} diff --git a/packages/content-management/table_list_view_table/tsconfig.json b/packages/content-management/table_list_view_table/tsconfig.json index 09bf8256764d1..b8add47c2bfb9 100644 --- a/packages/content-management/table_list_view_table/tsconfig.json +++ b/packages/content-management/table_list_view_table/tsconfig.json @@ -34,7 +34,8 @@ "@kbn/user-profile-components", "@kbn/core-user-profile-browser", "@kbn/react-kibana-mount", - "@kbn/content-management-user-profiles" + "@kbn/content-management-user-profiles", + "@kbn/recently-accessed" ], "exclude": [ "target/**/*" diff --git a/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx index d2922b9a96611..380314554dedd 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx +++ b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx @@ -39,13 +39,13 @@ import type { SideNavComponent as ISideNavComponent, ChromeHelpMenuLink, } from '@kbn/core-chrome-browser'; +import { RecentlyAccessedService } from '@kbn/recently-accessed'; import { Logger } from '@kbn/logging'; import { DocTitleService } from './doc_title'; import { NavControlsService } from './nav_controls'; import { NavLinksService } from './nav_links'; import { ProjectNavigationService } from './project_navigation'; -import { RecentlyAccessedService } from './recently_accessed'; import { Header, LoadingIndicator, ProjectHeader } from './ui'; import { registerAnalyticsContextProvider } from './register_analytics_context_provider'; import type { InternalChromeStart } from './types'; @@ -252,7 +252,7 @@ export class ChromeService { chromeBreadcrumbs$: breadcrumbs$, logger: this.logger, }); - const recentlyAccessed = await this.recentlyAccessed.start({ http }); + const recentlyAccessed = this.recentlyAccessed.start({ http, key: 'recentlyAccessed' }); const docTitle = this.docTitle.start(); const { customBranding$ } = customBranding; const helpMenuLinks$ = navControls.getHelpMenuLinks$(); diff --git a/packages/core/chrome/core-chrome-browser-internal/tsconfig.json b/packages/core/chrome/core-chrome-browser-internal/tsconfig.json index 5d81526f2c970..d4512b515f640 100644 --- a/packages/core/chrome/core-chrome-browser-internal/tsconfig.json +++ b/packages/core/chrome/core-chrome-browser-internal/tsconfig.json @@ -16,7 +16,6 @@ "**/*.tsx", ], "kbn_references": [ - "@kbn/crypto-browser", "@kbn/i18n", "@kbn/i18n-react", "@kbn/core-injected-metadata-browser-internal", @@ -55,6 +54,7 @@ "@kbn/core-i18n-browser-mocks", "@kbn/core-theme-browser-mocks", "@kbn/react-kibana-context-render", + "@kbn/recently-accessed", ], "exclude": [ "target/**/*", diff --git a/packages/core/security/core-security-server-internal/src/security_route_handler_context.ts b/packages/core/security/core-security-server-internal/src/security_route_handler_context.ts index 4fa328782dd0e..bae1c11d152a4 100644 --- a/packages/core/security/core-security-server-internal/src/security_route_handler_context.ts +++ b/packages/core/security/core-security-server-internal/src/security_route_handler_context.ts @@ -26,6 +26,16 @@ export class CoreSecurityRouteHandlerContext implements SecurityRequestHandlerCo if (this.#authc == null) { this.#authc = { getCurrentUser: () => this.securityStart.authc.getCurrentUser(this.request), + apiKeys: { + areAPIKeysEnabled: () => this.securityStart.authc.apiKeys.areAPIKeysEnabled(), + create: (createParams) => + this.securityStart.authc.apiKeys.create(this.request, createParams), + update: (updateParams) => + this.securityStart.authc.apiKeys.update(this.request, updateParams), + validate: (apiKeyParams) => this.securityStart.authc.apiKeys.validate(apiKeyParams), + invalidate: (apiKeyParams) => + this.securityStart.authc.apiKeys.invalidate(this.request, apiKeyParams), + }, }; } return this.#authc; diff --git a/packages/core/security/core-security-server-internal/src/utils/convert_security_api.test.ts b/packages/core/security/core-security-server-internal/src/utils/convert_security_api.test.ts index 7c2e49092f73e..40d9e788ea01b 100644 --- a/packages/core/security/core-security-server-internal/src/utils/convert_security_api.test.ts +++ b/packages/core/security/core-security-server-internal/src/utils/convert_security_api.test.ts @@ -15,6 +15,16 @@ describe('convertSecurityApi', () => { const source: CoreSecurityDelegateContract = { authc: { getCurrentUser: jest.fn(), + apiKeys: { + areAPIKeysEnabled: jest.fn(), + areCrossClusterAPIKeysEnabled: jest.fn(), + validate: jest.fn(), + invalidate: jest.fn(), + invalidateAsInternalUser: jest.fn(), + grantAsInternalUser: jest.fn(), + create: jest.fn(), + update: jest.fn(), + }, }, audit: { asScoped: jest.fn().mockReturnValue(createAuditLoggerMock.create()), @@ -23,6 +33,7 @@ describe('convertSecurityApi', () => { }; const output = convertSecurityApi(source); expect(output.authc.getCurrentUser).toBe(source.authc.getCurrentUser); + expect(output.authc.apiKeys).toBe(source.authc.apiKeys); expect(output.audit.asScoped).toBe(source.audit.asScoped); expect(output.audit.withoutRequest).toBe(source.audit.withoutRequest); }); diff --git a/packages/core/security/core-security-server-internal/src/utils/default_implementation.test.ts b/packages/core/security/core-security-server-internal/src/utils/default_implementation.test.ts index e4348404671b9..bc7fac96b7dd3 100644 --- a/packages/core/security/core-security-server-internal/src/utils/default_implementation.test.ts +++ b/packages/core/security/core-security-server-internal/src/utils/default_implementation.test.ts @@ -23,6 +23,15 @@ describe('getDefaultSecurityImplementation', () => { }); }); + describe('authc.apiKeys', () => { + it('returns stub object', async () => { + const { apiKeys } = implementation.authc; + const areAPIKeysEnabled = await apiKeys.areAPIKeysEnabled(); + + expect(areAPIKeysEnabled).toBe(false); + }); + }); + describe('audit.asScoped', () => { it('returns null', async () => { const logger = implementation.audit.asScoped({} as any); diff --git a/packages/core/security/core-security-server-internal/src/utils/default_implementation.ts b/packages/core/security/core-security-server-internal/src/utils/default_implementation.ts index 91819807f1064..8eaeb7b2577b5 100644 --- a/packages/core/security/core-security-server-internal/src/utils/default_implementation.ts +++ b/packages/core/security/core-security-server-internal/src/utils/default_implementation.ts @@ -8,10 +8,23 @@ import type { CoreSecurityDelegateContract } from '@kbn/core-security-server'; +const API_KEYS_DISABLED_ERROR = new Error('API keys are disabled'); +const REJECT_WHEN_API_KEYS_DISABLED = () => Promise.reject(API_KEYS_DISABLED_ERROR); + export const getDefaultSecurityImplementation = (): CoreSecurityDelegateContract => { return { authc: { getCurrentUser: () => null, + apiKeys: { + areAPIKeysEnabled: () => Promise.resolve(false), + areCrossClusterAPIKeysEnabled: () => Promise.resolve(false), + create: REJECT_WHEN_API_KEYS_DISABLED, + update: REJECT_WHEN_API_KEYS_DISABLED, + grantAsInternalUser: REJECT_WHEN_API_KEYS_DISABLED, + validate: REJECT_WHEN_API_KEYS_DISABLED, + invalidate: REJECT_WHEN_API_KEYS_DISABLED, + invalidateAsInternalUser: REJECT_WHEN_API_KEYS_DISABLED, + }, }, audit: { asScoped: () => { diff --git a/packages/core/security/core-security-server-mocks/index.ts b/packages/core/security/core-security-server-mocks/index.ts index 23c49282252f0..c834759973c1e 100644 --- a/packages/core/security/core-security-server-mocks/index.ts +++ b/packages/core/security/core-security-server-mocks/index.ts @@ -9,3 +9,4 @@ export { securityServiceMock } from './src/security_service.mock'; export type { InternalSecurityStartMock, SecurityStartMock } from './src/security_service.mock'; export { auditLoggerMock } from './src/audit.mock'; +export { apiKeysMock } from './src/api_keys.mock'; diff --git a/x-pack/plugins/security/server/authentication/api_keys/api_keys.mock.ts b/packages/core/security/core-security-server-mocks/src/api_keys.mock.ts similarity index 59% rename from x-pack/plugins/security/server/authentication/api_keys/api_keys.mock.ts rename to packages/core/security/core-security-server-mocks/src/api_keys.mock.ts index cfa857ca833a2..108f8380264e6 100644 --- a/x-pack/plugins/security/server/authentication/api_keys/api_keys.mock.ts +++ b/packages/core/security/core-security-server-mocks/src/api_keys.mock.ts @@ -1,16 +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. + * 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 type { PublicMethodsOf } from '@kbn/utility-types'; - -import type { APIKeys } from './api_keys'; +import type { APIKeysService } from '@kbn/core-security-server'; export const apiKeysMock = { - create: (): jest.Mocked> => ({ + create: (): jest.MockedObjectDeep => ({ areAPIKeysEnabled: jest.fn(), areCrossClusterAPIKeysEnabled: jest.fn(), create: jest.fn(), diff --git a/packages/core/security/core-security-server-mocks/src/security_service.mock.ts b/packages/core/security/core-security-server-mocks/src/security_service.mock.ts index d833048990ff5..86a39af3b16d5 100644 --- a/packages/core/security/core-security-server-mocks/src/security_service.mock.ts +++ b/packages/core/security/core-security-server-mocks/src/security_service.mock.ts @@ -15,6 +15,7 @@ import type { InternalSecurityServiceSetup, InternalSecurityServiceStart, } from '@kbn/core-security-server-internal'; +import { apiKeysMock } from './api_keys.mock'; import { auditServiceMock, type MockedAuditService } from './audit.mock'; import { mockAuthenticatedUser, MockAuthenticatedUserProps } from '@kbn/core-security-common/mocks'; @@ -35,6 +36,7 @@ const createStartMock = (): SecurityStartMock => { const mock = { authc: { getCurrentUser: jest.fn(), + apiKeys: apiKeysMock.create(), }, audit: auditServiceMock.create(), }; @@ -61,6 +63,7 @@ const createInternalStartMock = (): InternalSecurityStartMock => { const mock = { authc: { getCurrentUser: jest.fn(), + apiKeys: apiKeysMock.create(), }, audit: auditServiceMock.create(), }; @@ -82,6 +85,13 @@ const createRequestHandlerContextMock = () => { const mock: jest.MockedObjectDeep = { authc: { getCurrentUser: jest.fn(), + apiKeys: { + areAPIKeysEnabled: jest.fn(), + create: jest.fn(), + update: jest.fn(), + validate: jest.fn(), + invalidate: jest.fn(), + }, }, audit: { logger: { diff --git a/packages/core/security/core-security-server/index.ts b/packages/core/security/core-security-server/index.ts index 6a111ab6e27ab..b5dd091c7b87a 100644 --- a/packages/core/security/core-security-server/index.ts +++ b/packages/core/security/core-security-server/index.ts @@ -26,4 +26,26 @@ export type { AuditRequest, } from './src/audit_logging/audit_events'; export type { AuditLogger } from './src/audit_logging/audit_logger'; + +export type { + APIKeysServiceWithContext, + APIKeysService, + CreateAPIKeyParams, + CreateAPIKeyResult, + InvalidateAPIKeyResult, + InvalidateAPIKeysParams, + ValidateAPIKeyParams, + CreateRestAPIKeyParams, + CreateRestAPIKeyWithKibanaPrivilegesParams, + CreateCrossClusterAPIKeyParams, + GrantAPIKeyResult, + UpdateAPIKeyParams, + UpdateAPIKeyResult, + UpdateCrossClusterAPIKeyParams, + UpdateRestAPIKeyParams, + UpdateRestAPIKeyWithKibanaPrivilegesParams, +} from './src/authentication/api_keys'; + +export type { KibanaPrivilegesType, ElasticsearchPrivilegesType } from './src/roles'; +export { isCreateRestAPIKeyParams } from './src/authentication/api_keys'; export type { CoreFipsService } from './src/fips'; diff --git a/packages/core/security/core-security-server/src/authc.ts b/packages/core/security/core-security-server/src/authc.ts index 97654104858ea..85ba4fc71542a 100644 --- a/packages/core/security/core-security-server/src/authc.ts +++ b/packages/core/security/core-security-server/src/authc.ts @@ -8,6 +8,7 @@ import type { KibanaRequest } from '@kbn/core-http-server'; import type { AuthenticatedUser } from '@kbn/core-security-common'; +import type { APIKeysService } from './authentication/api_keys'; /** * Core's authentication service @@ -22,4 +23,5 @@ export interface CoreAuthenticationService { * @param request The request to retrieve the authenticated user for. */ getCurrentUser(request: KibanaRequest): AuthenticatedUser | null; + apiKeys: APIKeysService; } diff --git a/packages/core/security/core-security-server/src/authentication/api_keys/api_keys.ts b/packages/core/security/core-security-server/src/authentication/api_keys/api_keys.ts new file mode 100644 index 0000000000000..e842c38d8674d --- /dev/null +++ b/packages/core/security/core-security-server/src/authentication/api_keys/api_keys.ts @@ -0,0 +1,268 @@ +/* + * 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 type { estypes } from '@elastic/elasticsearch'; + +import type { KibanaRequest } from '@kbn/core-http-server'; + +import { ElasticsearchPrivilegesType, KibanaPrivilegesType } from '../../roles'; + +/** + * Interface for managing API keys in Elasticsearch, including creation, + * validation, and invalidation of API keys, + * as well as checking the status of API key features. + */ +export interface APIKeys { + /** + * Determines if API Keys are enabled in Elasticsearch. + */ + areAPIKeysEnabled(): Promise; + + /** + * Determines if Cross-Cluster API Keys are enabled in Elasticsearch. + */ + areCrossClusterAPIKeysEnabled(): Promise; + + /** + * Tries to create an API key for the current user. + * + * Returns newly created API key or `null` if API keys are disabled. + * + * User needs `manage_api_key` privilege to create REST API keys and `manage_security` for Cross-Cluster API keys. + * + * @param request Request instance. + * @param createParams The params to create an API key + */ + create( + request: KibanaRequest, + createParams: CreateAPIKeyParams + ): Promise; + + /** + * Attempts update an API key with the provided 'role_descriptors' and 'metadata' + * + * Returns `updated`, `true` if the update was successful, `false` if there was nothing to update + * + * User needs `manage_api_key` privilege to update REST API keys and `manage_security` for cross-cluster API keys. + * + * @param request Request instance. + * @param updateParams The params to edit an API key + */ + update( + request: KibanaRequest, + updateParams: UpdateAPIKeyParams + ): Promise; + + /** + * Tries to grant an API key for the current user. + * @param request Request instance. + * @param createParams Create operation parameters. + */ + grantAsInternalUser( + request: KibanaRequest, + createParams: CreateRestAPIKeyParams | CreateRestAPIKeyWithKibanaPrivilegesParams + ): Promise; + + /** + * Tries to validate an API key. + * @param apiKeyPrams ValidateAPIKeyParams. + */ + validate(apiKeyPrams: ValidateAPIKeyParams): Promise; + + /** + * Tries to invalidate an API keys. + * @param request Request instance. + * @param params The params to invalidate an API keys. + */ + invalidate( + request: KibanaRequest, + params: InvalidateAPIKeysParams + ): Promise; + + /** + * Tries to invalidate the API keys by using the internal user. + * @param params The params to invalidate the API keys. + */ + invalidateAsInternalUser(params: InvalidateAPIKeysParams): Promise; +} + +export type CreateAPIKeyParams = + | CreateRestAPIKeyParams + | CreateRestAPIKeyWithKibanaPrivilegesParams + | CreateCrossClusterAPIKeyParams; + +/** + * Response of Kibana Create API key endpoint. + */ +export type CreateAPIKeyResult = estypes.SecurityCreateApiKeyResponse; + +export interface CreateRestAPIKeyParams { + type?: 'rest'; + expiration?: string; + name: string; + role_descriptors: Record; + metadata?: { [key: string]: any }; +} + +export interface CreateRestAPIKeyWithKibanaPrivilegesParams { + type?: 'rest'; + expiration?: string; + name: string; + metadata?: { [key: string]: any }; + kibana_role_descriptors: Record< + string, + { + elasticsearch: ElasticsearchPrivilegesType & { [key: string]: unknown }; + kibana: KibanaPrivilegesType; + } + >; +} + +export interface CreateCrossClusterAPIKeyParams { + type: 'cross_cluster'; + expiration?: string; + name: string; + metadata?: { [key: string]: any }; + access: { + search?: Array<{ + names: string[]; + query?: unknown; + field_security?: unknown; + allow_restricted_indices?: boolean; + }>; + replication?: Array<{ + names: string[]; + }>; + }; +} + +export interface GrantAPIKeyResult { + /** + * Unique id for this API key + */ + id: string; + /** + * Name for this API key + */ + name: string; + /** + * Generated API key + */ + api_key: string; +} + +/** + * Represents the parameters for validating API Key credentials. + */ +export interface ValidateAPIKeyParams { + /** + * Unique id for this API key + */ + id: string; + + /** + * Generated API Key (secret) + */ + api_key: string; +} + +/** + * Represents the params for invalidating multiple API keys + */ +export interface InvalidateAPIKeysParams { + /** + * List of unique API key IDs + */ + ids: string[]; +} + +/** + * The return value when invalidating an API key in Elasticsearch. + */ +export interface InvalidateAPIKeyResult { + /** + * The IDs of the API keys that were invalidated as part of the request. + */ + invalidated_api_keys: string[]; + /** + * The IDs of the API keys that were already invalidated. + */ + previously_invalidated_api_keys: string[]; + /** + * The number of errors that were encountered when invalidating the API keys. + */ + error_count: number; + /** + * Details about these errors. This field is not present in the response when error_count is 0. + */ + error_details?: Array<{ + type?: string; + reason?: string; + caused_by?: { + type?: string; + reason?: string; + }; + }>; +} + +/** + * Response of Kibana Update API key endpoint. + */ +export type UpdateAPIKeyResult = estypes.SecurityUpdateApiKeyResponse; + +/** + * Request body of Kibana Update API key endpoint. + */ +export type UpdateAPIKeyParams = + | UpdateRestAPIKeyParams + | UpdateCrossClusterAPIKeyParams + | UpdateRestAPIKeyWithKibanaPrivilegesParams; + +export interface UpdateRestAPIKeyParams { + id: string; + type?: 'rest'; + expiration?: string; + role_descriptors: Record; + metadata?: { [key: string]: any }; +} + +export interface UpdateCrossClusterAPIKeyParams { + id: string; + type: 'cross_cluster'; + expiration?: string; + metadata?: { [key: string]: any }; + access: { + search?: Array<{ + names: string[]; + query?: unknown; + field_security?: unknown; + allow_restricted_indices?: boolean; + }>; + replication?: Array<{ + names: string[]; + }>; + }; +} + +export interface UpdateRestAPIKeyWithKibanaPrivilegesParams { + id: string; + type?: 'rest'; + expiration?: string; + metadata?: { [key: string]: any }; + kibana_role_descriptors: Record< + string, + { + elasticsearch: ElasticsearchPrivilegesType & { [key: string]: unknown }; + kibana: KibanaPrivilegesType; + } + >; +} + +export function isCreateRestAPIKeyParams(params: any): params is CreateRestAPIKeyParams { + return 'role_descriptors' in params; +} diff --git a/packages/core/security/core-security-server/src/authentication/api_keys/api_keys_context.ts b/packages/core/security/core-security-server/src/authentication/api_keys/api_keys_context.ts new file mode 100644 index 0000000000000..7090f7312774f --- /dev/null +++ b/packages/core/security/core-security-server/src/authentication/api_keys/api_keys_context.ts @@ -0,0 +1,64 @@ +/* + * 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 type { + CreateAPIKeyParams, + CreateAPIKeyResult, + UpdateAPIKeyParams, + UpdateAPIKeyResult, + ValidateAPIKeyParams, + InvalidateAPIKeyResult, + InvalidateAPIKeysParams, +} from './api_keys'; + +/** + * Public API Keys service exposed through core context to manage + * API keys in Elasticsearch, including creation, + * validation, and invalidation of API keys, + * as well as checking the status of API key features. + */ +export interface APIKeysServiceWithContext { + /** + * Determines if API Keys are enabled in Elasticsearch. + */ + areAPIKeysEnabled(): Promise; + + /** + * Tries to create an API key for the current user. + * + * Returns newly created API key or `null` if API keys are disabled. + * + * User needs `manage_api_key` privilege to create REST API keys and `manage_security` for Cross-Cluster API keys. + * + * @param createParams The params to create an API key + */ + create(createParams: CreateAPIKeyParams): Promise; + + /** + * Attempts update an API key with the provided 'role_descriptors' and 'metadata' + * + * Returns `updated`, `true` if the update was successful, `false` if there was nothing to update + * + * User needs `manage_api_key` privilege to update REST API keys and `manage_security` for cross-cluster API keys. + * + * @param updateParams The params to edit an API key + */ + update(updateParams: UpdateAPIKeyParams): Promise; + + /** + * Tries to validate an API key. + * @param apiKeyPrams ValidateAPIKeyParams. + */ + validate(apiKeyPrams: ValidateAPIKeyParams): Promise; + + /** + * Tries to invalidate an API keys. + * @param params The params to invalidate an API keys. + */ + invalidate(params: InvalidateAPIKeysParams): Promise; +} diff --git a/packages/core/security/core-security-server/src/authentication/api_keys/index.ts b/packages/core/security/core-security-server/src/authentication/api_keys/index.ts new file mode 100644 index 0000000000000..da7163bb50879 --- /dev/null +++ b/packages/core/security/core-security-server/src/authentication/api_keys/index.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +export type { + APIKeys as APIKeysService, + CreateAPIKeyParams, + CreateAPIKeyResult, + InvalidateAPIKeyResult, + InvalidateAPIKeysParams, + ValidateAPIKeyParams, + CreateRestAPIKeyParams, + CreateRestAPIKeyWithKibanaPrivilegesParams, + CreateCrossClusterAPIKeyParams, + GrantAPIKeyResult, + UpdateAPIKeyParams, + UpdateAPIKeyResult, + UpdateCrossClusterAPIKeyParams, + UpdateRestAPIKeyParams, + UpdateRestAPIKeyWithKibanaPrivilegesParams, +} from './api_keys'; +export type { APIKeysServiceWithContext } from './api_keys_context'; +export { isCreateRestAPIKeyParams } from './api_keys'; diff --git a/packages/core/security/core-security-server/src/request_handler_context.ts b/packages/core/security/core-security-server/src/request_handler_context.ts index 37915c24ddaa1..6cb13b3afb9a8 100644 --- a/packages/core/security/core-security-server/src/request_handler_context.ts +++ b/packages/core/security/core-security-server/src/request_handler_context.ts @@ -7,7 +7,9 @@ */ import type { AuthenticatedUser } from '@kbn/core-security-common'; + import { AuditLogger } from './audit_logging/audit_logger'; +import type { APIKeysServiceWithContext } from './authentication/api_keys'; export interface SecurityRequestHandlerContext { authc: AuthcRequestHandlerContext; @@ -16,6 +18,7 @@ export interface SecurityRequestHandlerContext { export interface AuthcRequestHandlerContext { getCurrentUser(): AuthenticatedUser | null; + apiKeys: APIKeysServiceWithContext; } export interface AuditRequestHandlerContext { diff --git a/packages/core/security/core-security-server/src/roles/index.ts b/packages/core/security/core-security-server/src/roles/index.ts new file mode 100644 index 0000000000000..420f6780fdd85 --- /dev/null +++ b/packages/core/security/core-security-server/src/roles/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export type { ElasticsearchPrivilegesType, KibanaPrivilegesType } from './schema'; diff --git a/packages/core/security/core-security-server/src/roles/schema.ts b/packages/core/security/core-security-server/src/roles/schema.ts new file mode 100644 index 0000000000000..693916ef3d9b3 --- /dev/null +++ b/packages/core/security/core-security-server/src/roles/schema.ts @@ -0,0 +1,42 @@ +/* + * 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. + */ + +/** + * Type representing Elasticsearch specific portion of the role definition. + */ +export interface ElasticsearchPrivilegesType { + cluster?: string[]; + remote_cluster?: Array<{ + privileges: string[]; + clusters: string[]; + }>; + indices?: Array<{ + names: string[]; + field_security?: Record<'grant' | 'except', string[]>; + privileges: string[]; + query?: string; + allow_restricted_indices?: boolean; + }>; + remote_indices?: Array<{ + clusters: string[]; + names: string[]; + field_security?: Record<'grant' | 'except', string[]>; + privileges: string[]; + query?: string; + allow_restricted_indices?: boolean; + }>; + run_as?: string[]; +} +/** + * Type representing Kibana specific portion of the role definition. + */ +export type KibanaPrivilegesType = Array<{ + spaces: string[]; + base?: string[]; + feature?: Record; +}>; diff --git a/packages/deeplinks/observability/locators/dataset_quality.ts b/packages/deeplinks/observability/locators/dataset_quality.ts index bfa760bf62c06..e30648e3f129c 100644 --- a/packages/deeplinks/observability/locators/dataset_quality.ts +++ b/packages/deeplinks/observability/locators/dataset_quality.ts @@ -8,7 +8,7 @@ import { SerializableRecord } from '@kbn/utility-types'; -export const DATASET_QUALITY_LOCATOR_ID = 'DATASET_QUALITY_LOCATOR'; +export const DATA_QUALITY_LOCATOR_ID = 'DATA_QUALITY_LOCATOR'; // eslint-disable-next-line @typescript-eslint/consistent-type-definitions type RefreshInterval = { @@ -23,11 +23,22 @@ type TimeRangeConfig = { refresh: RefreshInterval; }; +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +type DatasetConfig = { + rawName: string; + type: string; + name: string; + namespace: string; +}; + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions type Filters = { timeRange: TimeRangeConfig; }; -export interface DatasetQualityLocatorParams extends SerializableRecord { +export interface DataQualityLocatorParams extends SerializableRecord { filters?: Filters; + flyout?: { + dataset: DatasetConfig; + }; } diff --git a/packages/deeplinks/search/constants.ts b/packages/deeplinks/search/constants.ts index 3fdcc78bb68a1..8aa53658320f5 100644 --- a/packages/deeplinks/search/constants.ts +++ b/packages/deeplinks/search/constants.ts @@ -8,7 +8,7 @@ export const ENTERPRISE_SEARCH_APP_ID = 'enterpriseSearch'; export const ENTERPRISE_SEARCH_CONTENT_APP_ID = 'enterpriseSearchContent'; -export const ENTERPRISE_SEARCH_INFERENCE_ENDPOINTS_APP_ID = 'enterpriseSearchInferenceEndpoints'; +export const ENTERPRISE_SEARCH_RELEVANCE_APP_ID = 'enterpriseSearchRelevance'; export const ENTERPRISE_SEARCH_APPLICATIONS_APP_ID = 'enterpriseSearchApplications'; export const ENTERPRISE_SEARCH_ANALYTICS_APP_ID = 'enterpriseSearchAnalytics'; export const ENTERPRISE_SEARCH_APPSEARCH_APP_ID = 'appSearch'; diff --git a/packages/deeplinks/search/deep_links.ts b/packages/deeplinks/search/deep_links.ts index f004d1b2c9dd6..4b32ec9757bde 100644 --- a/packages/deeplinks/search/deep_links.ts +++ b/packages/deeplinks/search/deep_links.ts @@ -12,6 +12,7 @@ import { ENTERPRISE_SEARCH_APP_ID, ENTERPRISE_SEARCH_CONTENT_APP_ID, ENTERPRISE_SEARCH_APPLICATIONS_APP_ID, + ENTERPRISE_SEARCH_RELEVANCE_APP_ID, ENTERPRISE_SEARCH_ANALYTICS_APP_ID, ENTERPRISE_SEARCH_APPSEARCH_APP_ID, ENTERPRISE_SEARCH_WORKPLACESEARCH_APP_ID, @@ -23,6 +24,7 @@ import { export type EnterpriseSearchApp = typeof ENTERPRISE_SEARCH_APP_ID; export type EnterpriseSearchContentApp = typeof ENTERPRISE_SEARCH_CONTENT_APP_ID; export type EnterpriseSearchApplicationsApp = typeof ENTERPRISE_SEARCH_APPLICATIONS_APP_ID; +export type EnterpriseSearchRelevanceApp = typeof ENTERPRISE_SEARCH_RELEVANCE_APP_ID; export type EnterpriseSearchAnalyticsApp = typeof ENTERPRISE_SEARCH_ANALYTICS_APP_ID; export type EnterpriseSearchAppsearchApp = typeof ENTERPRISE_SEARCH_APPSEARCH_APP_ID; export type EnterpriseSearchWorkplaceSearchApp = typeof ENTERPRISE_SEARCH_WORKPLACESEARCH_APP_ID; @@ -38,10 +40,13 @@ export type ApplicationsLinkId = 'searchApplications' | 'playground'; export type AppsearchLinkId = 'engines'; +export type RelevanceLinkId = 'inferenceEndpoints'; + export type DeepLinkId = | EnterpriseSearchApp | EnterpriseSearchContentApp | EnterpriseSearchApplicationsApp + | EnterpriseSearchRelevanceApp | EnterpriseSearchAnalyticsApp | EnterpriseSearchAppsearchApp | EnterpriseSearchWorkplaceSearchApp @@ -52,4 +57,5 @@ export type DeepLinkId = | SearchHomepage | `${EnterpriseSearchContentApp}:${ContentLinkId}` | `${EnterpriseSearchApplicationsApp}:${ApplicationsLinkId}` - | `${EnterpriseSearchAppsearchApp}:${AppsearchLinkId}`; + | `${EnterpriseSearchAppsearchApp}:${AppsearchLinkId}` + | `${EnterpriseSearchRelevanceApp}:${RelevanceLinkId}`; diff --git a/packages/deeplinks/search/index.ts b/packages/deeplinks/search/index.ts index 663d625e9fd72..a18f0cb31426f 100644 --- a/packages/deeplinks/search/index.ts +++ b/packages/deeplinks/search/index.ts @@ -9,7 +9,7 @@ export { ENTERPRISE_SEARCH_APP_ID, ENTERPRISE_SEARCH_CONTENT_APP_ID, - ENTERPRISE_SEARCH_INFERENCE_ENDPOINTS_APP_ID, + ENTERPRISE_SEARCH_RELEVANCE_APP_ID, ENTERPRISE_SEARCH_APPLICATIONS_APP_ID, ENTERPRISE_SEARCH_ANALYTICS_APP_ID, ENTERPRISE_SEARCH_APPSEARCH_APP_ID, diff --git a/packages/kbn-alerts-ui-shared/src/common/apis/update_rule/update_rule.test.ts b/packages/kbn-alerts-ui-shared/src/common/apis/update_rule/update_rule.test.ts index 28a0c39c998df..7cb64b51427e0 100644 --- a/packages/kbn-alerts-ui-shared/src/common/apis/update_rule/update_rule.test.ts +++ b/packages/kbn-alerts-ui-shared/src/common/apis/update_rule/update_rule.test.ts @@ -114,13 +114,9 @@ describe('updateRule', () => { }, ], }); - expect(http.put.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerting/rule/12%2F3", - Object { - "body": "{\\"name\\":\\"test\\",\\"tags\\":[\\"foo\\"],\\"schedule\\":{\\"interval\\":\\"1m\\"},\\"params\\":{},\\"actions\\":[],\\"alert_delay\\":{\\"active\\":10}}", - }, - ] - `); + + expect(http.put).toHaveBeenCalledWith('/api/alerting/rule/12%2F3', { + body: '{"name":"test","tags":["foo"],"schedule":{"interval":"1m"},"params":{},"actions":[{"group":"default","id":"2","params":{},"frequency":{"notify_when":"onActionGroupChange","throttle":null,"summary":false},"use_alert_data_for_template":false},{"id":".test-system-action","params":{}}],"alert_delay":{"active":10}}', + }); }); }); diff --git a/packages/kbn-alerts-ui-shared/src/common/apis/update_rule/update_rule.ts b/packages/kbn-alerts-ui-shared/src/common/apis/update_rule/update_rule.ts index 7d9dbf71211ee..841778eaa52ee 100644 --- a/packages/kbn-alerts-ui-shared/src/common/apis/update_rule/update_rule.ts +++ b/packages/kbn-alerts-ui-shared/src/common/apis/update_rule/update_rule.ts @@ -44,7 +44,7 @@ export async function updateRule({ const res = await http.put>( `${BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(id)}`, { - body: JSON.stringify(transformUpdateRuleBody(pick(rule, UPDATE_FIELDS))), + body: JSON.stringify(transformUpdateRuleBody(pick(rule, UPDATE_FIELDS_WITH_ACTIONS))), } ); return transformRule(res); diff --git a/packages/kbn-check-mappings-update-cli/current_fields.json b/packages/kbn-check-mappings-update-cli/current_fields.json index 9cecd6df6d6f6..59cd87288e8ec 100644 --- a/packages/kbn-check-mappings-update-cli/current_fields.json +++ b/packages/kbn-check-mappings-update-cli/current_fields.json @@ -301,7 +301,8 @@ "metrics", "name", "staticFields", - "type" + "type", + "version" ], "entity-discovery-api-key": [ "apiKey" diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json index f0df154b4f012..47602ca181b5e 100644 --- a/packages/kbn-check-mappings-update-cli/current_mappings.json +++ b/packages/kbn-check-mappings-update-cli/current_mappings.json @@ -1028,6 +1028,9 @@ }, "type": { "type": "keyword" + }, + "version": { + "type": "keyword" } } }, diff --git a/packages/kbn-data-view-utils/index.ts b/packages/kbn-data-view-utils/index.ts index 1c881dbdacf79..ad783bc163c59 100644 --- a/packages/kbn-data-view-utils/index.ts +++ b/packages/kbn-data-view-utils/index.ts @@ -7,6 +7,6 @@ */ export * from './src/constants'; - +export { convertDatatableColumnToDataViewFieldSpec } from './src/utils/convert_to_data_view_field_spec'; export { createRegExpPatternFrom } from './src/utils/create_regexp_pattern_from'; export { testPatternAgainstAllowedList } from './src/utils/test_pattern_against_allowed_list'; diff --git a/packages/kbn-data-view-utils/src/utils/convert_to_data_view_field_spec.test.ts b/packages/kbn-data-view-utils/src/utils/convert_to_data_view_field_spec.test.ts new file mode 100644 index 0000000000000..94dce4a6a60da --- /dev/null +++ b/packages/kbn-data-view-utils/src/utils/convert_to_data_view_field_spec.test.ts @@ -0,0 +1,60 @@ +/* + * 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 { DatatableColumnType } from '@kbn/expressions-plugin/common'; +import { convertDatatableColumnToDataViewFieldSpec } from './convert_to_data_view_field_spec'; + +describe('convertDatatableColumnToDataViewFieldSpec', () => { + it('should return a DataViewField object for a counter column', () => { + const column = { + id: 'bytes_counter', + name: 'bytes_counter', + meta: { + esType: 'counter_long', + type: 'number' as DatatableColumnType, + }, + isNull: false, + }; + const result = convertDatatableColumnToDataViewFieldSpec(column); + expect(result).toEqual( + expect.objectContaining({ + name: 'bytes_counter', + type: 'number', + esTypes: ['long'], + searchable: true, + aggregatable: false, + isNull: false, + timeSeriesMetric: 'counter', + }) + ); + }); + + it('should return a DataViewField object with timeSeriesMetric undefined if esType does not start with counter_', () => { + const column = { + id: 'test', + name: 'test', + meta: { + esType: 'keyword', + type: 'string' as DatatableColumnType, + }, + isNull: false, + }; + const result = convertDatatableColumnToDataViewFieldSpec(column); + expect(result.timeSeriesMetric).toBeUndefined(); + expect(result).toEqual( + expect.objectContaining({ + name: 'test', + type: 'string', + esTypes: ['keyword'], + searchable: true, + aggregatable: false, + isNull: false, + }) + ); + }); +}); diff --git a/packages/kbn-data-view-utils/src/utils/convert_to_data_view_field_spec.ts b/packages/kbn-data-view-utils/src/utils/convert_to_data_view_field_spec.ts new file mode 100644 index 0000000000000..7cc408a414ade --- /dev/null +++ b/packages/kbn-data-view-utils/src/utils/convert_to_data_view_field_spec.ts @@ -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 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 type { DatatableColumn } from '@kbn/expressions-plugin/common'; +import type { MappingTimeSeriesMetricType } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { FieldSpec } from '@kbn/data-views-plugin/common'; + +/** + * Convert a datatable column to a DataViewFieldSpec + */ +export function convertDatatableColumnToDataViewFieldSpec(column: DatatableColumn): FieldSpec { + let esType = column.meta?.esType; + let timeSeriesMetric: MappingTimeSeriesMetricType | undefined; + + // 'counter_integer', 'counter_long', 'counter_double'... + if (esType?.startsWith('counter_')) { + esType = esType?.replace('counter_', ''); + timeSeriesMetric = 'counter'; + } + + // `DataViewField` class is defined in "data-views" plugin, so we can't create an instance of it from a package. + // We will return a data view field spec here instead then. + return { + name: column.name, + type: column.meta?.type ?? 'unknown', + esTypes: esType ? [esType] : undefined, + searchable: true, + aggregatable: false, + isNull: Boolean(column?.isNull), + ...(timeSeriesMetric ? { timeSeriesMetric } : {}), + }; +} diff --git a/packages/kbn-data-view-utils/tsconfig.json b/packages/kbn-data-view-utils/tsconfig.json index a41af0b7c5017..05400030e1001 100644 --- a/packages/kbn-data-view-utils/tsconfig.json +++ b/packages/kbn-data-view-utils/tsconfig.json @@ -14,5 +14,9 @@ ], "exclude": [ "target/**/*" + ], + "kbn_references": [ + "@kbn/data-views-plugin", + "@kbn/expressions-plugin", ] } diff --git a/packages/kbn-esql-ast/index.ts b/packages/kbn-esql-ast/index.ts index fc129c5c6fac3..6c8cd3c23e50b 100644 --- a/packages/kbn-esql-ast/index.ts +++ b/packages/kbn-esql-ast/index.ts @@ -22,6 +22,7 @@ export type { ESQLSource, ESQLColumn, ESQLLiteral, + ESQLParamLiteral, AstProviderFn, EditorError, ESQLAstNode, diff --git a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts index effd6b1b16ddd..32bd23e393431 100644 --- a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts +++ b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts @@ -17,7 +17,7 @@ import type { ESQLSource, ESQLTimeInterval, } from '@kbn/esql-ast'; -import { ESQLInlineCast } from '@kbn/esql-ast/src/types'; +import { ESQLInlineCast, ESQLParamLiteral } from '@kbn/esql-ast/src/types'; import { statsAggregationFunctionDefinitions } from '../definitions/aggs'; import { builtinFunctions } from '../definitions/builtin'; import { commandDefinitions } from '../definitions/commands'; @@ -407,7 +407,7 @@ export function checkFunctionArgMatchesDefinition( parentCommand?: string ) { const argType = parameterDefinition.type; - if (argType === 'any') { + if (argType === 'any' || isParam(arg)) { return true; } if (arg.type === 'literal') { @@ -575,3 +575,9 @@ export function shouldBeQuotedText( export const isAggFunction = (arg: ESQLFunction): boolean => getFunctionDefinition(arg.name)?.type === 'agg'; + +export const isParam = (x: unknown): x is ESQLParamLiteral => + !!x && + typeof x === 'object' && + (x as ESQLParamLiteral).type === 'literal' && + (x as ESQLParamLiteral).literalType === 'param'; diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/validation.params.test.ts b/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/validation.params.test.ts new file mode 100644 index 0000000000000..17f91a72c52a4 --- /dev/null +++ b/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/validation.params.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { setup } from './helpers'; + +test('should allow param inside agg function argument', async () => { + const { validate } = await setup(); + + const res1 = await validate('FROM index | STATS avg(?)'); + const res2 = await validate('FROM index | STATS avg(?named)'); + const res3 = await validate('FROM index | STATS avg(?123)'); + + expect(res1).toMatchObject({ errors: [], warnings: [] }); + expect(res2).toMatchObject({ errors: [], warnings: [] }); + expect(res3).toMatchObject({ errors: [], warnings: [] }); +}); + +test('allow params in WHERE command expressions', async () => { + const { validate } = await setup(); + + const res1 = await validate('FROM index | WHERE stringField >= ?earliest'); + const res2 = await validate(` + FROM index + | WHERE stringField >= ?earliest + | WHERE stringField <= ?0 + | WHERE stringField == ? + `); + const res3 = await validate(` + FROM index + | WHERE stringField >= ?earliest + AND stringField <= ?0 + AND stringField == ? + `); + + expect(res1).toMatchObject({ errors: [], warnings: [] }); + expect(res2).toMatchObject({ errors: [], warnings: [] }); + expect(res3).toMatchObject({ errors: [], warnings: [] }); +}); diff --git a/packages/kbn-field-types/src/kbn_field_types.ts b/packages/kbn-field-types/src/kbn_field_types.ts index 7ec22de078230..5059d37da1d0e 100644 --- a/packages/kbn-field-types/src/kbn_field_types.ts +++ b/packages/kbn-field-types/src/kbn_field_types.ts @@ -50,6 +50,10 @@ export const getFilterableKbnTypeNames = (): string[] => registeredKbnTypes.filter((type) => type.filterable).map((type) => type.name); export function esFieldTypeToKibanaFieldType(type: string) { + // 'counter_integer', 'counter_long', 'counter_double'... + if (type.startsWith('counter_')) { + return KBN_FIELD_TYPES.NUMBER; + } switch (type) { case ES_FIELD_TYPES._INDEX: return KBN_FIELD_TYPES.STRING; diff --git a/packages/kbn-field-utils/src/utils/get_text_based_column_icon_type.ts b/packages/kbn-field-utils/src/utils/get_text_based_column_icon_type.ts index 0ccad94674208..3c37b9b01c3fb 100644 --- a/packages/kbn-field-utils/src/utils/get_text_based_column_icon_type.ts +++ b/packages/kbn-field-utils/src/utils/get_text_based_column_icon_type.ts @@ -7,6 +7,7 @@ */ import type { DatatableColumnMeta } from '@kbn/expressions-plugin/common'; +import { convertDatatableColumnToDataViewFieldSpec } from '@kbn/data-view-utils'; import { getFieldIconType } from './get_field_icon_type'; export function getTextBasedColumnIconType( @@ -19,10 +20,8 @@ export function getTextBasedColumnIconType( | null ): string | null { return columnMeta && columnMeta.type - ? getFieldIconType({ - name: '', - type: columnMeta.type, - esTypes: columnMeta.esType ? [columnMeta.esType] : [], - }) + ? getFieldIconType( + convertDatatableColumnToDataViewFieldSpec({ id: '', name: '', meta: columnMeta }) + ) : null; } diff --git a/packages/kbn-field-utils/tsconfig.json b/packages/kbn-field-utils/tsconfig.json index 4b75159b5f7fe..9ac5ba7e942bc 100644 --- a/packages/kbn-field-utils/tsconfig.json +++ b/packages/kbn-field-utils/tsconfig.json @@ -10,6 +10,7 @@ "@kbn/react-field", "@kbn/field-types", "@kbn/expressions-plugin", + "@kbn/data-view-utils", ], "exclude": ["target/**/*"] } diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 00bcbf6481939..3b7828dc7f67d 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -42,6 +42,7 @@ pageLoadAssetSize: embeddableEnhanced: 22107 enterpriseSearch: 66810 entityManager: 17175 + esql: 37000 esqlDataGrid: 24582 esUiShared: 326654 eventAnnotation: 30000 @@ -156,7 +157,6 @@ pageLoadAssetSize: synthetics: 40958 telemetry: 51957 telemetryManagementSection: 38586 - textBasedLanguages: 37000 threatIntelligence: 44299 timelines: 327300 transform: 41007 diff --git a/packages/kbn-recently-accessed/README.md b/packages/kbn-recently-accessed/README.md new file mode 100644 index 0000000000000..a7579822c9123 --- /dev/null +++ b/packages/kbn-recently-accessed/README.md @@ -0,0 +1,4 @@ +# @kbn/recently-accessed + +The `RecentlyAccessedService` uses browser local storage to manage records of recently accessed objects. +This can be used to make recent items easier for users to find in listing UIs. diff --git a/packages/kbn-recently-accessed/index.ts b/packages/kbn-recently-accessed/index.ts new file mode 100644 index 0000000000000..3b23a548ce946 --- /dev/null +++ b/packages/kbn-recently-accessed/index.ts @@ -0,0 +1,13 @@ +/* + * 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. + */ + +export { + type RecentlyAccessed, + type RecentlyAccessedHistoryItem, + RecentlyAccessedService, +} from './src'; diff --git a/packages/kbn-recently-accessed/jest.config.js b/packages/kbn-recently-accessed/jest.config.js new file mode 100644 index 0000000000000..1492f7da1fcfe --- /dev/null +++ b/packages/kbn-recently-accessed/jest.config.js @@ -0,0 +1,13 @@ +/* + * 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-recently-accessed'], +}; diff --git a/packages/kbn-recently-accessed/kibana.jsonc b/packages/kbn-recently-accessed/kibana.jsonc new file mode 100644 index 0000000000000..0ec9917dc6b77 --- /dev/null +++ b/packages/kbn-recently-accessed/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/recently-accessed", + "owner": "@elastic/appex-sharedux" +} diff --git a/packages/kbn-recently-accessed/package.json b/packages/kbn-recently-accessed/package.json new file mode 100644 index 0000000000000..825ccbd8cfdaf --- /dev/null +++ b/packages/kbn-recently-accessed/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/recently-accessed", + "private": true, + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0" +} \ No newline at end of file diff --git a/packages/core/chrome/core-chrome-browser-internal/src/recently_accessed/create_log_key.test.ts b/packages/kbn-recently-accessed/src/create_log_key.test.ts similarity index 100% rename from packages/core/chrome/core-chrome-browser-internal/src/recently_accessed/create_log_key.test.ts rename to packages/kbn-recently-accessed/src/create_log_key.test.ts diff --git a/packages/core/chrome/core-chrome-browser-internal/src/recently_accessed/create_log_key.ts b/packages/kbn-recently-accessed/src/create_log_key.ts similarity index 88% rename from packages/core/chrome/core-chrome-browser-internal/src/recently_accessed/create_log_key.ts rename to packages/kbn-recently-accessed/src/create_log_key.ts index a9b0c5cdcf7d4..5a5e8e7cc7ad0 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/recently_accessed/create_log_key.ts +++ b/packages/kbn-recently-accessed/src/create_log_key.ts @@ -8,7 +8,7 @@ import { Sha256 } from '@kbn/crypto-browser'; -export async function createLogKey(type: string, optionalIdentifier?: string) { +export function createLogKey(type: string, optionalIdentifier?: string) { const baseKey = `kibana.history.${type}`; if (!optionalIdentifier) { diff --git a/packages/core/chrome/core-chrome-browser-internal/src/recently_accessed/index.ts b/packages/kbn-recently-accessed/src/index.ts similarity index 84% rename from packages/core/chrome/core-chrome-browser-internal/src/recently_accessed/index.ts rename to packages/kbn-recently-accessed/src/index.ts index 2256f60061b87..389b7eca5c1b2 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/recently_accessed/index.ts +++ b/packages/kbn-recently-accessed/src/index.ts @@ -7,3 +7,4 @@ */ export { RecentlyAccessedService } from './recently_accessed_service'; +export type { RecentlyAccessed, RecentlyAccessedHistoryItem } from './types'; diff --git a/packages/core/chrome/core-chrome-browser-internal/src/recently_accessed/persisted_log.test.ts b/packages/kbn-recently-accessed/src/persisted_log.test.ts similarity index 100% rename from packages/core/chrome/core-chrome-browser-internal/src/recently_accessed/persisted_log.test.ts rename to packages/kbn-recently-accessed/src/persisted_log.test.ts diff --git a/packages/core/chrome/core-chrome-browser-internal/src/recently_accessed/persisted_log.ts b/packages/kbn-recently-accessed/src/persisted_log.ts similarity index 100% rename from packages/core/chrome/core-chrome-browser-internal/src/recently_accessed/persisted_log.ts rename to packages/kbn-recently-accessed/src/persisted_log.ts diff --git a/packages/core/chrome/core-chrome-browser-internal/src/recently_accessed/recently_accessed_service.test.ts b/packages/kbn-recently-accessed/src/recently_accessed_service.test.ts similarity index 98% rename from packages/core/chrome/core-chrome-browser-internal/src/recently_accessed/recently_accessed_service.test.ts rename to packages/kbn-recently-accessed/src/recently_accessed_service.test.ts index e394f81fab11d..0260949295634 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/recently_accessed/recently_accessed_service.test.ts +++ b/packages/kbn-recently-accessed/src/recently_accessed_service.test.ts @@ -54,7 +54,10 @@ describe('RecentlyAccessed#start()', () => { const getStart = async () => { const http = httpServiceMock.createStartContract(); - const recentlyAccessed = await new RecentlyAccessedService().start({ http }); + const recentlyAccessed = await new RecentlyAccessedService().start({ + http, + key: 'recentlyAccessed', + }); return { http, recentlyAccessed }; }; diff --git a/packages/core/chrome/core-chrome-browser-internal/src/recently_accessed/recently_accessed_service.ts b/packages/kbn-recently-accessed/src/recently_accessed_service.ts similarity index 68% rename from packages/core/chrome/core-chrome-browser-internal/src/recently_accessed/recently_accessed_service.ts rename to packages/kbn-recently-accessed/src/recently_accessed_service.ts index ec9a04ab5551c..4df97b556e5b6 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/recently_accessed/recently_accessed_service.ts +++ b/packages/kbn-recently-accessed/src/recently_accessed_service.ts @@ -6,23 +6,20 @@ * Side Public License, v 1. */ -import type { InternalHttpStart } from '@kbn/core-http-browser-internal'; -import type { - ChromeRecentlyAccessed, - ChromeRecentlyAccessedHistoryItem, -} from '@kbn/core-chrome-browser'; +import type { HttpStart } from '@kbn/core-http-browser'; +import type { RecentlyAccessed, RecentlyAccessedHistoryItem } from './types'; import { PersistedLog } from './persisted_log'; import { createLogKey } from './create_log_key'; interface StartDeps { - http: InternalHttpStart; + key: string; + http: Pick; } -/** @internal */ export class RecentlyAccessedService { - async start({ http }: StartDeps): Promise { - const logKey = await createLogKey('recentlyAccessed', http.basePath.get()); - const history = new PersistedLog(logKey, { + start({ http, key }: StartDeps): RecentlyAccessed { + const logKey = createLogKey(key, http.basePath.get()); + const history = new PersistedLog(logKey, { maxLength: 20, isEqual: (oldItem, newItem) => oldItem.id === newItem.id, }); diff --git a/packages/kbn-recently-accessed/src/types.ts b/packages/kbn-recently-accessed/src/types.ts new file mode 100644 index 0000000000000..9b729ffc9ed2c --- /dev/null +++ b/packages/kbn-recently-accessed/src/types.ts @@ -0,0 +1,56 @@ +/* + * 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 { Observable } from 'rxjs'; + +/** @public */ +export interface RecentlyAccessedHistoryItem { + link: string; + label: string; + id: string; +} + +/** + * {@link RecentlyAccessed | APIs} for recently accessed history. + * @public + */ +export interface RecentlyAccessed { + /** + * Adds a new item to the recently accessed history. + * + * @example + * ```js + * chrome.recentlyAccessed.add('/app/map/1234', 'Map 1234', '1234'); + * ``` + * + * @param link a relative URL to the resource (not including the {@link HttpStart.basePath | `http.basePath`}) + * @param label the label to display in the UI + * @param id a unique string used to de-duplicate the recently accessed list. + */ + add(link: string, label: string, id: string): void; + + /** + * Gets an Array of the current recently accessed history. + * + * @example + * ```js + * recentlyAccessed.get().forEach(console.log); + * ``` + */ + get(): RecentlyAccessedHistoryItem[]; + + /** + * Gets an Observable of the array of recently accessed history. + * + * @example + * ```js + * recentlyAccessed.get$().subscribe(console.log); + * ``` + */ + get$(): Observable; +} diff --git a/packages/kbn-recently-accessed/tsconfig.json b/packages/kbn-recently-accessed/tsconfig.json new file mode 100644 index 0000000000000..f6029007a4376 --- /dev/null +++ b/packages/kbn-recently-accessed/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/crypto-browser", + "@kbn/core-http-browser", + "@kbn/core-http-browser-mocks", + ] +} diff --git a/packages/kbn-test-eui-helpers/src/rtl_helpers.tsx b/packages/kbn-test-eui-helpers/src/rtl_helpers.tsx index e55b61a380bbf..58f0444f8b63f 100644 --- a/packages/kbn-test-eui-helpers/src/rtl_helpers.tsx +++ b/packages/kbn-test-eui-helpers/src/rtl_helpers.tsx @@ -53,7 +53,7 @@ export class EuiButtonGroupTestHarness { } /** - * Returns selected value of button group + * Returns selected option of button group */ public get selected() { return within(this.#buttonGroup).getByRole('button', { pressed: true }); @@ -136,3 +136,59 @@ export class EuiSuperDatePickerTestHarness { userEvent.click(screen.getByRole('button', { name: 'Refresh' })); } } + +export class EuiSelectTestHarness { + #testId: string; + + /** + * Returns select or throws + */ + get #selectEl() { + return screen.getByTestId(this.#testId); + } + + constructor(testId: string) { + this.#testId = testId; + } + + /** + * Returns `data-test-subj` of select + */ + public get testId() { + return this.#testId; + } + + /** + * Returns button select if found, otherwise `null` + */ + public get self() { + return screen.queryByTestId(this.#testId); + } + + /** + * Returns all options of select + */ + public get options(): HTMLOptionElement[] { + return within(this.#selectEl).getAllByRole('option'); + } + + /** + * Returns selected option + */ + public get selected() { + return (this.#selectEl as HTMLSelectElement).value; + } + + /** + * Select option by value + */ + public select(optionName: string | RegExp) { + const option = this.options.find((o) => o.value === optionName)?.value; + + if (!option) { + throw new Error(`Option [${optionName}] not found`); + } + + fireEvent.change(this.#selectEl, { target: { value: option } }); + } +} diff --git a/packages/kbn-unified-field-list/src/components/field_list_filters/field_name_search.test.tsx b/packages/kbn-unified-field-list/src/components/field_list_filters/field_name_search.test.tsx index e5a5a4bb82a5f..52858ba292812 100644 --- a/packages/kbn-unified-field-list/src/components/field_list_filters/field_name_search.test.tsx +++ b/packages/kbn-unified-field-list/src/components/field_list_filters/field_name_search.test.tsx @@ -8,12 +8,18 @@ import React, { useState } from 'react'; import userEvent from '@testing-library/user-event'; +import { render, screen } from '@testing-library/react'; import { FieldNameSearch, type FieldNameSearchProps } from './field_name_search'; -import { render, screen, waitFor } from '@testing-library/react'; -// Flaky: https://github.com/elastic/kibana/issues/187714 -describe.skip('UnifiedFieldList ', () => { - it('should render correctly', async () => { +describe('UnifiedFieldList ', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + afterAll(() => { + jest.useRealTimers(); + }); + + it('should render correctly', () => { const props: FieldNameSearchProps = { nameFilter: '', onChange: jest.fn(), @@ -24,11 +30,12 @@ describe.skip('UnifiedFieldList ', () => { const input = screen.getByRole('searchbox', { name: 'Search field names' }); expect(input).toHaveAttribute('aria-describedby', 'htmlId'); userEvent.type(input, 'hey'); - await waitFor(() => expect(props.onChange).toHaveBeenCalledWith('hey'), { timeout: 256 }); + jest.advanceTimersByTime(256); + expect(props.onChange).toHaveBeenCalledWith('hey'); expect(props.onChange).toBeCalledTimes(1); }); - it('should accept the updates from the top', async () => { + it('should accept the updates from the top', () => { const FieldNameSearchWithWrapper = ({ defaultNameFilter = '' }) => { const [nameFilter, setNameFilter] = useState(defaultNameFilter); const props: FieldNameSearchProps = { diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts index 73f8b7f9e9313..51c06947d224b 100644 --- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts +++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts @@ -88,7 +88,7 @@ describe('checking migration metadata changes on all registered SO types', () => "endpoint:unified-user-artifact-manifest": "71c7fcb52c658b21ea2800a6b6a76972ae1c776e", "endpoint:user-artifact-manifest": "1c3533161811a58772e30cdc77bac4631da3ef2b", "enterprise_search_telemetry": "9ac912e1417fc8681e0cd383775382117c9e3d3d", - "entity-definition": "33fe0194bd896f0bfe479d55f6de20f8ba1d7713", + "entity-definition": "331a2ba0ee9f24936ef049683549c8af7e46f03a", "entity-discovery-api-key": "c267a65c69171d1804362155c1378365f5acef88", "epm-packages": "8042d4a1522f6c4e6f5486e791b3ffe3a22f88fd", "epm-packages-assets": "7a3e58efd9a14191d0d1a00b8aaed30a145fd0b1", diff --git a/src/core/server/integration_tests/elasticsearch/user_agent.test.ts b/src/core/server/integration_tests/elasticsearch/user_agent.test.ts new file mode 100644 index 0000000000000..b864e3f330308 --- /dev/null +++ b/src/core/server/integration_tests/elasticsearch/user_agent.test.ts @@ -0,0 +1,76 @@ +/* + * 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 { esTestConfig } from '@kbn/test'; +import * as http from 'http'; +import { loggerMock } from '@kbn/logging-mocks'; +import { Root } from '@kbn/core-root-server-internal'; +import { + PRODUCT_RESPONSE_HEADER, + USER_AGENT_HEADER, + configureClient, + AgentManager, +} from '@kbn/core-elasticsearch-client-server-internal'; +import { configSchema, ElasticsearchConfig } from '@kbn/core-elasticsearch-server-internal'; + +function createFakeElasticsearchServer(hook: (req: http.IncomingMessage) => void) { + const server = http.createServer((req, res) => { + hook(req); + res.writeHead(200, undefined, { [PRODUCT_RESPONSE_HEADER]: 'Elasticsearch' }); + res.write('{}'); + res.end(); + }); + server.listen(esTestConfig.getPort()); + + return server; +} + +describe('ES Client - custom user-agent', () => { + let esServer: http.Server; + let kibanaServer: Root; + + afterAll(async () => { + try { + await kibanaServer?.shutdown(); + } catch (e) { + // trap + } + try { + await new Promise((resolve, reject) => + esServer.close((err) => (err ? reject(err) : resolve())) + ); + } catch (e) { + // trap + } + }); + + test('should send a custom user-agent header matching the expected format', async () => { + const kibanaVersion = '8.42.9'; + const logger = loggerMock.create(); + const rawConfig = configSchema.validate({ + hosts: [`${esTestConfig.getUrl()}`], + }); + const config = new ElasticsearchConfig(rawConfig); + const agentFactoryProvider = new AgentManager(logger, { dnsCacheTtlInSeconds: 0 }); + const esClient = configureClient(config, { + type: 'foo', + logger, + kibanaVersion, + agentFactoryProvider, + }); + + let userAgentHeader: string | undefined; + esServer = createFakeElasticsearchServer((res) => { + userAgentHeader = res.headers[USER_AGENT_HEADER]; + }); + + await esClient.ping(); + + expect(userAgentHeader).toEqual(`Kibana/${kibanaVersion}`); + }); +}); diff --git a/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.test.ts b/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.test.ts index abfa37dd8df6e..88568b1c231cb 100644 --- a/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.test.ts +++ b/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.test.ts @@ -25,6 +25,10 @@ describe('interpreter/functions#metricVis', () => { progressDirection: 'horizontal', maxCols: 1, inspectorTableId: 'random-id', + titlesTextAlign: 'left', + valuesTextAlign: 'right', + iconAlign: 'left', + valueFontSize: 'default', }; it('should pass over overrides from variables', async () => { diff --git a/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts b/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts index db1e0cf5cea5f..c40af033ceab9 100644 --- a/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts +++ b/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts @@ -77,6 +77,30 @@ export const metricVisFunction = (): MetricVisExpressionFunctionDefinition => ({ 'The direction the progress bar should grow. Must be provided to render a progress bar.', }), }, + titlesTextAlign: { + types: ['string'], + help: i18n.translate('expressionMetricVis.function.titlesTextAlign.help', { + defaultMessage: 'The alignment of the Title and Subtitle.', + }), + }, + valuesTextAlign: { + types: ['string'], + help: i18n.translate('expressionMetricVis.function.valuesTextAlign.help', { + defaultMessage: 'The alignment of the Primary and Secondary Metric.', + }), + }, + iconAlign: { + types: ['string'], + help: i18n.translate('expressionMetricVis.function.iconAlign.help', { + defaultMessage: 'The alignment of icon.', + }), + }, + valueFontSize: { + types: ['string', 'number'], + help: i18n.translate('expressionMetricVis.function.valueFontSize.help', { + defaultMessage: 'The value font size.', + }), + }, color: { types: ['string'], help: i18n.translate('expressionMetricVis.function.color.help', { @@ -189,6 +213,10 @@ export const metricVisFunction = (): MetricVisExpressionFunctionDefinition => ({ icon: args.icon, palette: args.palette?.params, progressDirection: args.progressDirection, + titlesTextAlign: args.titlesTextAlign, + valuesTextAlign: args.valuesTextAlign, + iconAlign: args.iconAlign, + valueFontSize: args.valueFontSize, maxCols: args.maxCols, minTiles: args.minTiles, trends: args.trendline?.trends, diff --git a/src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts index 28199c684ea15..7e7438aa3d8ad 100644 --- a/src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts @@ -7,7 +7,7 @@ */ import type { PaletteOutput } from '@kbn/coloring'; -import { LayoutDirection, MetricWTrend } from '@elastic/charts'; +import { LayoutDirection, MetricStyle, MetricWTrend } from '@elastic/charts'; import { $Values } from '@kbn/utility-types'; import { Datatable, @@ -38,6 +38,10 @@ export interface MetricArguments { subtitle?: string; secondaryPrefix?: string; progressDirection?: LayoutDirection; + titlesTextAlign: MetricStyle['titlesTextAlign']; + valuesTextAlign: MetricStyle['valuesTextAlign']; + iconAlign: MetricStyle['iconAlign']; + valueFontSize: MetricStyle['valueFontSize']; color?: string; icon?: string; palette?: PaletteOutput; diff --git a/src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts b/src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts index b9a43af0752b2..083f464670d89 100644 --- a/src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts +++ b/src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts @@ -8,7 +8,7 @@ import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common'; import { CustomPaletteState } from '@kbn/charts-plugin/common'; -import { LayoutDirection } from '@elastic/charts'; +import { LayoutDirection, MetricStyle } from '@elastic/charts'; import { TrendlineResult } from './expression_functions'; export const visType = 'metric'; @@ -27,6 +27,10 @@ export interface MetricVisParam { icon?: string; palette?: CustomPaletteState; progressDirection?: LayoutDirection; + titlesTextAlign: MetricStyle['titlesTextAlign']; + valuesTextAlign: MetricStyle['valuesTextAlign']; + iconAlign: MetricStyle['iconAlign']; + valueFontSize: MetricStyle['valueFontSize']; maxCols: number; minTiles?: number; trends?: TrendlineResult['trends']; diff --git a/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.test.tsx b/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.test.tsx index ed57f38ec886f..cd93d3997b8d5 100644 --- a/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.test.tsx +++ b/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.test.tsx @@ -23,7 +23,7 @@ import { SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; import { SerializableRecord } from '@kbn/utility-types'; import type { IUiSettingsClient } from '@kbn/core/public'; import { CustomPaletteState } from '@kbn/charts-plugin/common/expressions/palette/types'; -import { DimensionsVisParam } from '../../common'; +import { DimensionsVisParam, MetricVisParam } from '../../common'; import { euiThemeVars } from '@kbn/ui-theme'; import { DEFAULT_TRENDLINE_NAME } from '../../common/constants'; import faker from 'faker'; @@ -73,6 +73,15 @@ const dayOfWeekColumnId = 'col-0-0'; const basePriceColumnId = 'col-1-1'; const minPriceColumnId = 'col-2-2'; +const defaultMetricParams: MetricVisParam = { + progressDirection: 'vertical', + maxCols: 5, + titlesTextAlign: 'left', + valuesTextAlign: 'right', + iconAlign: 'left', + valueFontSize: 'default', +}; + const table: Datatable = { type: 'datatable', columns: [ @@ -217,8 +226,7 @@ describe('MetricVisComponent', function () { describe('single metric', () => { const config: Props['config'] = { metric: { - progressDirection: 'vertical', - maxCols: 5, + ...defaultMetricParams, icon: 'empty', }, dimensions: { @@ -402,8 +410,7 @@ describe('MetricVisComponent', function () { describe('metric grid', () => { const config: Props['config'] = { metric: { - progressDirection: 'vertical', - maxCols: 5, + ...defaultMetricParams, }, dimensions: { metric: basePriceColumnId, @@ -856,8 +863,7 @@ describe('MetricVisComponent', function () { data={table} config={{ metric: { - progressDirection: 'vertical', - maxCols: 5, + ...defaultMetricParams, }, dimensions: { metric: basePriceColumnId, @@ -911,8 +917,7 @@ describe('MetricVisComponent', function () { { const config: Props['config'] = { metric: { - progressDirection: 'vertical', - maxCols: 5, + ...defaultMetricParams, }, dimensions: { metric: '1', @@ -1416,8 +1413,7 @@ describe('MetricVisComponent', function () { { if (settings.fields && settings.fieldsIndices) { - const mappings = await esClient.asInternalUser.indices.getMapping( + const mappings = await esClient.asCurrentUser.indices.getMapping( { index: settings.fieldsIndices, }, @@ -33,7 +33,7 @@ const getMappings = async (settings: SettingsToRetrieve, esClient: IScopedCluste const getAliases = async (settings: SettingsToRetrieve, esClient: IScopedClusterClient) => { if (settings.indices) { - const aliases = await esClient.asInternalUser.indices.getAlias(); + const aliases = await esClient.asCurrentUser.indices.getAlias(); return aliases; } // If the user doesn't want autocomplete suggestions, then clear any that exist. @@ -42,7 +42,7 @@ const getAliases = async (settings: SettingsToRetrieve, esClient: IScopedCluster const getDataStreams = async (settings: SettingsToRetrieve, esClient: IScopedClusterClient) => { if (settings.dataStreams) { - const dataStreams = await esClient.asInternalUser.indices.getDataStream(); + const dataStreams = await esClient.asCurrentUser.indices.getDataStream(); return dataStreams; } // If the user doesn't want autocomplete suggestions, then clear any that exist. @@ -51,7 +51,7 @@ const getDataStreams = async (settings: SettingsToRetrieve, esClient: IScopedClu const getLegacyTemplates = async (settings: SettingsToRetrieve, esClient: IScopedClusterClient) => { if (settings.templates) { - const legacyTemplates = await esClient.asInternalUser.indices.getTemplate(); + const legacyTemplates = await esClient.asCurrentUser.indices.getTemplate(); return legacyTemplates; } // If the user doesn't want autocomplete suggestions, then clear any that exist. @@ -60,7 +60,7 @@ const getLegacyTemplates = async (settings: SettingsToRetrieve, esClient: IScope const getIndexTemplates = async (settings: SettingsToRetrieve, esClient: IScopedClusterClient) => { if (settings.templates) { - const indexTemplates = await esClient.asInternalUser.indices.getIndexTemplate(); + const indexTemplates = await esClient.asCurrentUser.indices.getIndexTemplate(); return indexTemplates; } // If the user doesn't want autocomplete suggestions, then clear any that exist. @@ -72,7 +72,7 @@ const getComponentTemplates = async ( esClient: IScopedClusterClient ) => { if (settings.templates) { - const componentTemplates = await esClient.asInternalUser.cluster.getComponentTemplate(); + const componentTemplates = await esClient.asCurrentUser.cluster.getComponentTemplate(); return componentTemplates; } // If the user doesn't want autocomplete suggestions, then clear any that exist. diff --git a/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.test.tsx b/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.test.tsx index b2493454d4da2..da9783676cba3 100644 --- a/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.test.tsx +++ b/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.test.tsx @@ -157,6 +157,7 @@ describe('useDashboardListingTable', () => { showActivityView: true, }, createdByEnabled: true, + recentlyAccessed: expect.objectContaining({ get: expect.any(Function) }), }; expect(tableListViewTableProps).toEqual(expectedProps); diff --git a/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx b/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx index 659e5e43930ea..097fc5ea6d866 100644 --- a/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx +++ b/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx @@ -100,6 +100,7 @@ export const useDashboardListingTable = ({ checkForDuplicateDashboardTitle, }, notifications: { toasts }, + dashboardRecentlyAccessed, } = pluginServices.getServices(); const { getEntityName, getTableListTitle, getEntityNamePlural } = dashboardListingTableStrings; @@ -302,6 +303,7 @@ export const useDashboardListingTable = ({ title, urlStateEnabled, createdByEnabled: true, + recentlyAccessed: dashboardRecentlyAccessed, }), [ contentEditorValidators, @@ -324,6 +326,7 @@ export const useDashboardListingTable = ({ title, updateItemMeta, urlStateEnabled, + dashboardRecentlyAccessed, ] ); diff --git a/src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx b/src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx index d3e0ac3459049..6cef6ab1871f4 100644 --- a/src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx @@ -83,6 +83,7 @@ export function InternalDashboardTopNav({ embeddable: { getStateTransfer }, initializerContext: { allowByValueEmbeddables }, dashboardCapabilities: { saveQuery: allowSaveQuery, showWriteControls }, + dashboardRecentlyAccessed, } = pluginServices.getServices(); const isLabsEnabled = uiSettings.get(UI_SETTINGS.ENABLE_LABS_UI); const { setHeaderActionMenu, onAppLeave } = useDashboardMountContext(); @@ -143,6 +144,11 @@ export function InternalDashboardTopNav({ title, lastSavedId ); + dashboardRecentlyAccessed.add( + getFullEditPath(lastSavedId, viewMode === ViewMode.EDIT), + title, + lastSavedId + ); } return () => subscription.unsubscribe(); }, [ @@ -152,6 +158,7 @@ export function InternalDashboardTopNav({ lastSavedId, viewMode, title, + dashboardRecentlyAccessed, ]); /** diff --git a/src/plugins/dashboard/public/services/dashboard_recently_accessed/dashboard_recently_accessed.stub.ts b/src/plugins/dashboard/public/services/dashboard_recently_accessed/dashboard_recently_accessed.stub.ts new file mode 100644 index 0000000000000..32f17f01d0e06 --- /dev/null +++ b/src/plugins/dashboard/public/services/dashboard_recently_accessed/dashboard_recently_accessed.stub.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; +import { DashboardRecentlyAccessedService } from './types'; + +type DashboardRecentlyAccessedServiceFactory = + PluginServiceFactory; + +export const dashboardRecentlyAccessedServiceFactory: DashboardRecentlyAccessedServiceFactory = + () => { + return { + add: jest.fn(), + get: jest.fn(), + get$: jest.fn(), + }; + }; diff --git a/src/plugins/dashboard/public/services/dashboard_recently_accessed/dashboard_recently_accessed.ts b/src/plugins/dashboard/public/services/dashboard_recently_accessed/dashboard_recently_accessed.ts new file mode 100644 index 0000000000000..8a80544aacad4 --- /dev/null +++ b/src/plugins/dashboard/public/services/dashboard_recently_accessed/dashboard_recently_accessed.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { RecentlyAccessedService } from '@kbn/recently-accessed'; +import type { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; + +import { DashboardHTTPService } from '../http/types'; +import { DashboardStartDependencies } from '../../plugin'; +import { DashboardRecentlyAccessedService } from './types'; + +interface DashboardRecentlyAccessedRequiredServices { + http: DashboardHTTPService; +} + +export type DashboardBackupServiceFactory = KibanaPluginServiceFactory< + DashboardRecentlyAccessedService, + DashboardStartDependencies, + DashboardRecentlyAccessedRequiredServices +>; + +export const dashboardRecentlyAccessedFactory: DashboardBackupServiceFactory = ( + core, + requiredServices +) => { + const { http } = requiredServices; + return new RecentlyAccessedService().start({ http, key: 'dashboardRecentlyAccessed' }); +}; diff --git a/src/plugins/dashboard/public/services/dashboard_recently_accessed/types.ts b/src/plugins/dashboard/public/services/dashboard_recently_accessed/types.ts new file mode 100644 index 0000000000000..0b27bfe89aa63 --- /dev/null +++ b/src/plugins/dashboard/public/services/dashboard_recently_accessed/types.ts @@ -0,0 +1,11 @@ +/* + * 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 { RecentlyAccessed } from '@kbn/recently-accessed'; + +export type DashboardRecentlyAccessedService = RecentlyAccessed; diff --git a/src/plugins/dashboard/public/services/plugin_services.stub.ts b/src/plugins/dashboard/public/services/plugin_services.stub.ts index 8f0f1dd4f5bbc..59c78ac9685b4 100644 --- a/src/plugins/dashboard/public/services/plugin_services.stub.ts +++ b/src/plugins/dashboard/public/services/plugin_services.stub.ts @@ -47,6 +47,7 @@ import { userProfileServiceFactory } from './user_profile/user_profile_service.s import { observabilityAIAssistantServiceStubFactory } from './observability_ai_assistant/observability_ai_assistant_service.stub'; import { noDataPageServiceFactory } from './no_data_page/no_data_page_service.stub'; import { uiActionsServiceFactory } from './ui_actions/ui_actions_service.stub'; +import { dashboardRecentlyAccessedServiceFactory } from './dashboard_recently_accessed/dashboard_recently_accessed.stub'; export const providers: PluginServiceProviders = { dashboardContentManagement: new PluginServiceProvider(dashboardContentManagementServiceFactory), @@ -82,6 +83,7 @@ export const providers: PluginServiceProviders = { uiActions: new PluginServiceProvider(uiActionsServiceFactory), userProfile: new PluginServiceProvider(userProfileServiceFactory), observabilityAIAssistant: new PluginServiceProvider(observabilityAIAssistantServiceStubFactory), + dashboardRecentlyAccessed: new PluginServiceProvider(dashboardRecentlyAccessedServiceFactory), }; export const registry = new PluginServiceRegistry(providers); diff --git a/src/plugins/dashboard/public/services/plugin_services.ts b/src/plugins/dashboard/public/services/plugin_services.ts index 9880308e7c5e5..b46c261fa6507 100644 --- a/src/plugins/dashboard/public/services/plugin_services.ts +++ b/src/plugins/dashboard/public/services/plugin_services.ts @@ -48,6 +48,7 @@ import { noDataPageServiceFactory } from './no_data_page/no_data_page_service'; import { uiActionsServiceFactory } from './ui_actions/ui_actions_service'; import { observabilityAIAssistantServiceFactory } from './observability_ai_assistant/observability_ai_assistant_service'; import { userProfileServiceFactory } from './user_profile/user_profile_service'; +import { dashboardRecentlyAccessedFactory } from './dashboard_recently_accessed/dashboard_recently_accessed'; const providers: PluginServiceProviders = { dashboardContentManagement: new PluginServiceProvider(dashboardContentManagementServiceFactory, [ @@ -96,6 +97,7 @@ const providers: PluginServiceProviders(); diff --git a/src/plugins/dashboard/public/services/types.ts b/src/plugins/dashboard/public/services/types.ts index ebb5fa110d0e2..bb0a8d8d6914d 100644 --- a/src/plugins/dashboard/public/services/types.ts +++ b/src/plugins/dashboard/public/services/types.ts @@ -43,6 +43,7 @@ import { NoDataPageService } from './no_data_page/types'; import { DashboardUiActionsService } from './ui_actions/types'; import { ObservabilityAIAssistantService } from './observability_ai_assistant/types'; import { DashboardUserProfileService } from './user_profile/types'; +import { DashboardRecentlyAccessedService } from './dashboard_recently_accessed/types'; export type DashboardPluginServiceParams = KibanaPluginServiceParams & { initContext: PluginInitializerContext; // need a custom type so that initContext is a required parameter for initializerContext @@ -82,4 +83,5 @@ export interface DashboardServices { uiActions: DashboardUiActionsService; observabilityAIAssistant: ObservabilityAIAssistantService; // TODO: make this optional in follow up userProfile: DashboardUserProfileService; + dashboardRecentlyAccessed: DashboardRecentlyAccessedService; } diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json index 8659b44b914ff..a29aa10853035 100644 --- a/src/plugins/dashboard/tsconfig.json +++ b/src/plugins/dashboard/tsconfig.json @@ -83,6 +83,7 @@ "@kbn/esql-utils", "@kbn/lens-embeddable-utils", "@kbn/lens-plugin", + "@kbn/recently-accessed", ], "exclude": ["target/**/*"] } diff --git a/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_list.ts b/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_list.ts index 99d911dd14f61..36a07faaef80d 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_list.ts +++ b/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_list.ts @@ -8,6 +8,7 @@ import { difference } from 'lodash'; import { type DataView, DataViewField } from '@kbn/data-views-plugin/public'; +import { convertDatatableColumnToDataViewFieldSpec } from '@kbn/data-view-utils'; import type { DatatableColumn } from '@kbn/expressions-plugin/common'; import { fieldWildcardFilter } from '@kbn/kibana-utils-plugin/public'; import { isNestedFieldParent } from '@kbn/discover-utils'; @@ -66,14 +67,6 @@ export function getEsqlQueryFieldList(esqlQueryColumns?: DatatableColumn[]): Dat return []; } return esqlQueryColumns.map( - (column) => - new DataViewField({ - name: column.name, - type: column.meta?.type ?? 'unknown', - esTypes: column.meta?.esType ? [column.meta?.esType] : undefined, - searchable: true, - aggregatable: false, - isNull: Boolean(column?.isNull), - }) + (column) => new DataViewField(convertDatatableColumnToDataViewFieldSpec(column)) ); } diff --git a/src/plugins/esql/.i18nrc.json b/src/plugins/esql/.i18nrc.json new file mode 100755 index 0000000000000..fce2490c832cf --- /dev/null +++ b/src/plugins/esql/.i18nrc.json @@ -0,0 +1,6 @@ +{ + "prefix": "esql", + "paths": { + "esql": "." + } +} diff --git a/src/plugins/text_based_languages/README.md b/src/plugins/esql/README.md similarity index 86% rename from src/plugins/text_based_languages/README.md rename to src/plugins/esql/README.md index 42d3375220682..05a7406e06a3b 100644 --- a/src/plugins/text_based_languages/README.md +++ b/src/plugins/esql/README.md @@ -1,4 +1,4 @@ -# @kbn/text-based-languages +# @kbn/esql ## Component properties The editor accepts the following properties: @@ -11,8 +11,8 @@ The editor accepts the following properties: - isLoading: As the editor is not responsible for the data fetching request, the consumer could update this property when the data are being fetched. If this property is defined, the query history component will be rendered ``` -To use it on your application, you need to add the textBasedLanguages to your requiredBundles and the @kbn/text-based-languages to your tsconfig.json and use the component like that: -import { TextBasedLangEditor } from '@kbn/text-based-languages/public'; +To use it on your application, you need to add the textBasedLanguages to your requiredBundles and the @kbn/esql to your tsconfig.json and use the component like that: +import { TextBasedLangEditor } from '@kbn/esql/public'; /src/plugins/text_based_languages'], - coverageDirectory: '/target/kibana-coverage/jest/src/plugins/text_based_languages', + roots: ['/src/plugins/esql'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/esql', coverageReporters: ['text', 'html'], - collectCoverageFrom: [ - '/src/plugins/text_based_languages/{common,public,server}/**/*.{js,ts,tsx}', - ], + collectCoverageFrom: ['/src/plugins/esql/{common,public,server}/**/*.{js,ts,tsx}'], setupFiles: ['jest-canvas-mock'], }; diff --git a/src/plugins/text_based_languages/kibana.jsonc b/src/plugins/esql/kibana.jsonc similarity index 82% rename from src/plugins/text_based_languages/kibana.jsonc rename to src/plugins/esql/kibana.jsonc index 5bed408add15a..797b9066e46ae 100644 --- a/src/plugins/text_based_languages/kibana.jsonc +++ b/src/plugins/esql/kibana.jsonc @@ -1,9 +1,9 @@ { "type": "plugin", - "id": "@kbn/text-based-languages", + "id": "@kbn/esql", "owner": "@elastic/kibana-esql", "plugin": { - "id": "textBasedLanguages", + "id": "esql", "server": true, "browser": true, "optionalPlugins": [ diff --git a/src/plugins/text_based_languages/package.json b/src/plugins/esql/package.json similarity index 70% rename from src/plugins/text_based_languages/package.json rename to src/plugins/esql/package.json index a13edb1990192..fe166ce25a71b 100644 --- a/src/plugins/text_based_languages/package.json +++ b/src/plugins/esql/package.json @@ -1,5 +1,5 @@ { - "name": "@kbn/text-based-languages", + "name": "@kbn/esql", "private": true, "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0" diff --git a/src/plugins/text_based_languages/public/create_editor.tsx b/src/plugins/esql/public/create_editor.tsx similarity index 100% rename from src/plugins/text_based_languages/public/create_editor.tsx rename to src/plugins/esql/public/create_editor.tsx diff --git a/src/plugins/text_based_languages/public/index.ts b/src/plugins/esql/public/index.ts similarity index 76% rename from src/plugins/text_based_languages/public/index.ts rename to src/plugins/esql/public/index.ts index 697e1d0e1d319..8e797ed591fca 100644 --- a/src/plugins/text_based_languages/public/index.ts +++ b/src/plugins/esql/public/index.ts @@ -6,11 +6,11 @@ * Side Public License, v 1. */ -import { TextBasedLanguagesPlugin } from './plugin'; +import { EsqlPlugin } from './plugin'; export type { TextBasedLanguagesEditorProps } from '@kbn/text-based-editor'; -export type { TextBasedLanguagesPluginStart } from './types'; +export type { EsqlPluginStart } from './types'; export { TextBasedLangEditor } from './create_editor'; export function plugin() { - return new TextBasedLanguagesPlugin(); + return new EsqlPlugin(); } diff --git a/src/plugins/text_based_languages/public/kibana_services.ts b/src/plugins/esql/public/kibana_services.ts similarity index 100% rename from src/plugins/text_based_languages/public/kibana_services.ts rename to src/plugins/esql/public/kibana_services.ts diff --git a/src/plugins/text_based_languages/public/plugin.ts b/src/plugins/esql/public/plugin.ts similarity index 83% rename from src/plugins/text_based_languages/public/plugin.ts rename to src/plugins/esql/public/plugin.ts index 841641c8fce25..5b78a0b48dc2b 100755 --- a/src/plugins/text_based_languages/public/plugin.ts +++ b/src/plugins/esql/public/plugin.ts @@ -19,22 +19,22 @@ import { } from './triggers'; import { setKibanaServices } from './kibana_services'; -interface TextBasedLanguagesPluginStart { +interface EsqlPluginStart { dataViews: DataViewsPublicPluginStart; expressions: ExpressionsStart; uiActions: UiActionsStart; data: DataPublicPluginStart; } -interface TextBasedLanguagesPluginSetup { +interface EsqlPluginSetup { indexManagement: IndexManagementPluginSetup; uiActions: UiActionsSetup; } -export class TextBasedLanguagesPlugin implements Plugin<{}, void> { +export class EsqlPlugin implements Plugin<{}, void> { private indexManagement?: IndexManagementPluginSetup; - public setup(_: CoreSetup, { indexManagement, uiActions }: TextBasedLanguagesPluginSetup) { + public setup(_: CoreSetup, { indexManagement, uiActions }: EsqlPluginSetup) { this.indexManagement = indexManagement; uiActions.registerTrigger(updateESQLQueryTrigger); @@ -44,7 +44,7 @@ export class TextBasedLanguagesPlugin implements Plugin<{}, void> { public start( core: CoreStart, - { dataViews, expressions, data, uiActions }: TextBasedLanguagesPluginStart + { dataViews, expressions, data, uiActions }: EsqlPluginStart ): void { const appendESQLAction = new UpdateESQLQueryAction(data); uiActions.addTriggerAction(UPDATE_ESQL_QUERY_TRIGGER, appendESQLAction); diff --git a/src/plugins/text_based_languages/public/triggers/index.ts b/src/plugins/esql/public/triggers/index.ts similarity index 100% rename from src/plugins/text_based_languages/public/triggers/index.ts rename to src/plugins/esql/public/triggers/index.ts diff --git a/src/plugins/text_based_languages/public/triggers/update_esql_query_actions.test.ts b/src/plugins/esql/public/triggers/update_esql_query_actions.test.ts similarity index 100% rename from src/plugins/text_based_languages/public/triggers/update_esql_query_actions.test.ts rename to src/plugins/esql/public/triggers/update_esql_query_actions.test.ts diff --git a/src/plugins/text_based_languages/public/triggers/update_esql_query_actions.ts b/src/plugins/esql/public/triggers/update_esql_query_actions.ts similarity index 95% rename from src/plugins/text_based_languages/public/triggers/update_esql_query_actions.ts rename to src/plugins/esql/public/triggers/update_esql_query_actions.ts index 4aa7b015b366b..798ac803d3e3b 100644 --- a/src/plugins/text_based_languages/public/triggers/update_esql_query_actions.ts +++ b/src/plugins/esql/public/triggers/update_esql_query_actions.ts @@ -25,7 +25,7 @@ export class UpdateESQLQueryAction implements Action { constructor(protected readonly data: DataPublicPluginStart) {} public getDisplayName(): string { - return i18n.translate('textBasedLanguages.updateESQLQueryLabel', { + return i18n.translate('esql.updateESQLQueryLabel', { defaultMessage: 'Update the ES|QL query in the editor', }); } diff --git a/src/plugins/text_based_languages/public/triggers/update_esql_query_helpers.ts b/src/plugins/esql/public/triggers/update_esql_query_helpers.ts similarity index 100% rename from src/plugins/text_based_languages/public/triggers/update_esql_query_helpers.ts rename to src/plugins/esql/public/triggers/update_esql_query_helpers.ts diff --git a/src/plugins/text_based_languages/public/triggers/update_esql_query_trigger.ts b/src/plugins/esql/public/triggers/update_esql_query_trigger.ts similarity index 80% rename from src/plugins/text_based_languages/public/triggers/update_esql_query_trigger.ts rename to src/plugins/esql/public/triggers/update_esql_query_trigger.ts index 13164647607ef..79feddcd42c0c 100644 --- a/src/plugins/text_based_languages/public/triggers/update_esql_query_trigger.ts +++ b/src/plugins/esql/public/triggers/update_esql_query_trigger.ts @@ -13,10 +13,10 @@ export const UPDATE_ESQL_QUERY_TRIGGER = 'UPDATE_ESQL_QUERY_TRIGGER'; export const updateESQLQueryTrigger: Trigger = { id: UPDATE_ESQL_QUERY_TRIGGER, - title: i18n.translate('textBasedLanguages.triggers.updateEsqlQueryTrigger', { + title: i18n.translate('esql.triggers.updateEsqlQueryTrigger', { defaultMessage: 'Update ES|QL query', }), - description: i18n.translate('textBasedLanguages.triggers.updateEsqlQueryTriggerDescription', { + description: i18n.translate('esql.triggers.updateEsqlQueryTriggerDescription', { defaultMessage: 'Update ES|QL query with a new one', }), }; diff --git a/src/plugins/text_based_languages/public/types.ts b/src/plugins/esql/public/types.ts similarity index 90% rename from src/plugins/text_based_languages/public/types.ts rename to src/plugins/esql/public/types.ts index c2dd5249d3d19..ef28bddc3c458 100644 --- a/src/plugins/text_based_languages/public/types.ts +++ b/src/plugins/esql/public/types.ts @@ -7,6 +7,6 @@ */ import { TextBasedLanguagesEditorProps } from '@kbn/text-based-editor'; -export interface TextBasedLanguagesPluginStart { +export interface EsqlPluginStart { Editor: React.ComponentType; } diff --git a/src/plugins/text_based_languages/server/index.ts b/src/plugins/esql/server/index.ts similarity index 76% rename from src/plugins/text_based_languages/server/index.ts rename to src/plugins/esql/server/index.ts index bca404b161bf8..fa0a0cec5bdbe 100644 --- a/src/plugins/text_based_languages/server/index.ts +++ b/src/plugins/esql/server/index.ts @@ -7,6 +7,6 @@ */ export const plugin = async () => { - const { TextBasedLanguagesServerPlugin } = await import('./plugin'); - return new TextBasedLanguagesServerPlugin(); + const { EsqlServerPlugin } = await import('./plugin'); + return new EsqlServerPlugin(); }; diff --git a/src/plugins/text_based_languages/server/plugin.ts b/src/plugins/esql/server/plugin.ts similarity index 91% rename from src/plugins/text_based_languages/server/plugin.ts rename to src/plugins/esql/server/plugin.ts index 95a341a467cc5..416c1dacc04e5 100644 --- a/src/plugins/text_based_languages/server/plugin.ts +++ b/src/plugins/esql/server/plugin.ts @@ -9,7 +9,7 @@ import type { CoreSetup, CoreStart, Plugin } from '@kbn/core/server'; import { getUiSettings } from './ui_settings'; -export class TextBasedLanguagesServerPlugin implements Plugin { +export class EsqlServerPlugin implements Plugin { public setup(core: CoreSetup) { core.uiSettings.register(getUiSettings()); return {}; diff --git a/src/plugins/text_based_languages/server/ui_settings.ts b/src/plugins/esql/server/ui_settings.ts similarity index 82% rename from src/plugins/text_based_languages/server/ui_settings.ts rename to src/plugins/esql/server/ui_settings.ts index 32717b0d2cb8c..9c43fdf55cc25 100644 --- a/src/plugins/text_based_languages/server/ui_settings.ts +++ b/src/plugins/esql/server/ui_settings.ts @@ -14,17 +14,17 @@ import { ENABLE_ESQL } from '@kbn/esql-utils'; export const getUiSettings: () => Record = () => ({ [ENABLE_ESQL]: { - name: i18n.translate('textBasedLanguages.advancedSettings.enableESQLTitle', { + name: i18n.translate('esql.advancedSettings.enableESQLTitle', { defaultMessage: 'Enable ES|QL', }), value: true, - description: i18n.translate('textBasedLanguages.advancedSettings.enableESQLDescription', { + description: i18n.translate('esql.advancedSettings.enableESQLDescription', { defaultMessage: 'This setting enables ES|QL in Kibana. By switching it off you will hide the ES|QL user interface from various applications. However, users will be able to access existing ES|QL saved searches, visualizations, etc. If you have feedback on this experience please reach out to us on {link}', values: { link: `` + - i18n.translate('textBasedLanguages.advancedSettings.enableESQL.discussLinkText', { + i18n.translate('esql.advancedSettings.enableESQL.discussLinkText', { defaultMessage: 'https://ela.st/esql-feedback', }) + '', diff --git a/src/plugins/text_based_languages/tsconfig.json b/src/plugins/esql/tsconfig.json similarity index 100% rename from src/plugins/text_based_languages/tsconfig.json rename to src/plugins/esql/tsconfig.json diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts index 4ffe9d483a1a4..8d6343337f152 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts @@ -135,7 +135,7 @@ export const applicationUsageSchema = { canvas: commonSchema, enterpriseSearch: commonSchema, enterpriseSearchContent: commonSchema, - enterpriseSearchInferenceEndpoints: commonSchema, + enterpriseSearchRelevance: commonSchema, enterpriseSearchAnalytics: commonSchema, enterpriseSearchApplications: commonSchema, enterpriseSearchAISearch: commonSchema, diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 22e75e5d4b658..00c4a48da72fa 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -2098,7 +2098,7 @@ } } }, - "enterpriseSearchInferenceEndpoints": { + "enterpriseSearchRelevance": { "properties": { "appId": { "type": "keyword", diff --git a/src/plugins/text_based_languages/.i18nrc.json b/src/plugins/text_based_languages/.i18nrc.json deleted file mode 100755 index d5a020d5dd392..0000000000000 --- a/src/plugins/text_based_languages/.i18nrc.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "prefix": "textBasedLanguages", - "paths": { - "textBasedLanguages": "." - } -} diff --git a/src/plugins/unified_doc_viewer/kibana.jsonc b/src/plugins/unified_doc_viewer/kibana.jsonc index 2361a10120e9b..e2febffda4df6 100644 --- a/src/plugins/unified_doc_viewer/kibana.jsonc +++ b/src/plugins/unified_doc_viewer/kibana.jsonc @@ -8,7 +8,7 @@ "server": false, "browser": true, "requiredBundles": ["kibanaUtils"], - "requiredPlugins": ["data", "discoverShared", "fieldFormats"], + "requiredPlugins": ["data", "discoverShared", "fieldFormats", "share"], "optionalPlugins": ["fieldsMetadata"] } } diff --git a/src/plugins/unified_doc_viewer/public/__mocks__/services.ts b/src/plugins/unified_doc_viewer/public/__mocks__/services.ts index 8496b919b38f0..29ae5f2981a6e 100644 --- a/src/plugins/unified_doc_viewer/public/__mocks__/services.ts +++ b/src/plugins/unified_doc_viewer/public/__mocks__/services.ts @@ -12,6 +12,7 @@ import { discoverSharedPluginMock } from '@kbn/discover-shared-plugin/public/moc import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks'; import { fieldsMetadataPluginPublicMock } from '@kbn/fields-metadata-plugin/public/mocks'; import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks'; +import { sharePluginMock } from '@kbn/share-plugin/public/mocks'; import type { UnifiedDocViewerServices, UnifiedDocViewerStart } from '../types'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import { DocViewsRegistry } from '@kbn/unified-doc-viewer'; @@ -29,4 +30,5 @@ export const mockUnifiedDocViewerServices: jest.Mocked storage: new Storage(localStorage), uiSettings: uiSettingsServiceMock.createStartContract(), unifiedDocViewer: mockUnifiedDocViewer, + share: sharePluginMock.createStartContract(), }; diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/logs_overview.test.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/logs_overview.test.tsx index 7b9379654cec1..a0f4ba8e993ff 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/logs_overview.test.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/logs_overview.test.tsx @@ -18,6 +18,8 @@ const DATASET_NAME = 'logs.overview'; const NAMESPACE = 'default'; const DATA_STREAM_NAME = `logs-${DATASET_NAME}-${NAMESPACE}`; const NOW = Date.now(); +const MORE_THAN_1024_CHARS = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?'; const dataView = { fields: { @@ -82,7 +84,7 @@ const fullHit = buildDataTableRecord( cloud: { provider: ['gcp'], region: 'us-central-1', - availability_zone: 'us-central-1a', + availability_zone: MORE_THAN_1024_CHARS, project: { id: 'elastic-project', }, @@ -92,6 +94,9 @@ const fullHit = buildDataTableRecord( }, 'agent.name': 'node', }, + ignored_field_values: { + 'cloud.availability_zone': [MORE_THAN_1024_CHARS], + }, }, dataView ); @@ -159,4 +164,37 @@ describe('LogsOverview', () => { expect(screen.queryByTestId('unifiedDocViewLogsOverviewLogShipper')).toBeInTheDocument(); }); }); + describe('Degraded Fields section', () => { + it('should load the degraded fields container when present', async () => { + expect( + screen.queryByTestId('unifiedDocViewLogsOverviewDegradedFieldsAccordion') + ).toBeInTheDocument(); + expect( + screen.queryByTestId('unifiedDocViewLogsOverviewDegradedFieldsTechPreview') + ).toBeInTheDocument(); + expect( + screen.queryByTestId('unifiedDocViewLogsOverviewDegradedFieldTitleCount') + ).toBeInTheDocument(); + + // The accordion must be closed by default + const accordion = screen.queryByTestId('unifiedDocViewLogsOverviewDegradedFieldsAccordion1'); + + if (accordion === null) { + return; + } + const button = accordion.querySelector('button'); + + if (button === null) { + return; + } + // Check the aria-expanded property of the button + const isExpanded = button.getAttribute('aria-expanded'); + expect(isExpanded).toBe('false'); + + button.click(); + expect( + screen.queryByTestId('unifiedDocViewLogsOverviewDegradedFieldsQualityIssuesTable') + ).toBeInTheDocument(); + }); + }); }); diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/logs_overview.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/logs_overview.tsx index c0161d112e955..b46570f4f0d37 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/logs_overview.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/logs_overview.tsx @@ -15,6 +15,7 @@ import { LogsOverviewHighlights } from './logs_overview_highlights'; import { FieldActionsProvider } from '../../hooks/use_field_actions'; import { getUnifiedDocViewerServices } from '../../plugin'; import { LogsOverviewAIAssistant } from './logs_overview_ai_assistant'; +import { LogsOverviewDegradedFields } from './logs_overview_degraded_fields'; export function LogsOverview({ columns, @@ -38,6 +39,7 @@ export function LogsOverview({ + ); diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/logs_overview_degraded_fields.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/logs_overview_degraded_fields.tsx new file mode 100644 index 0000000000000..976ef71c6b647 --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/logs_overview_degraded_fields.tsx @@ -0,0 +1,321 @@ +/* + * 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, { useMemo, useState } from 'react'; +import { DataTableRecord } from '@kbn/discover-utils'; +import { + EuiAccordion, + EuiBadge, + EuiBetaBadge, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiTitle, + EuiBasicTable, + useGeneratedHtmlId, + EuiBasicTableColumn, + EuiHeaderLink, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { orderBy } from 'lodash'; +import { getRouterLinkProps } from '@kbn/router-utils'; +import { DATA_QUALITY_LOCATOR_ID, DataQualityLocatorParams } from '@kbn/deeplinks-observability'; +import { BrowserUrlService } from '@kbn/share-plugin/public'; +import { getUnifiedDocViewerServices } from '../../plugin'; + +type Direction = 'asc' | 'desc'; +type SortField = 'issue' | 'values'; + +const DEFAULT_SORT_FIELD = 'issue'; +const DEFAULT_SORT_DIRECTION = 'asc'; +const DEFAULT_ROWS_PER_PAGE = 5; + +interface DegradedField { + issue: string; + values: string[]; +} + +interface ParamsForLocator { + dataStreamType: string; + dataStreamName: string; + dataStreamNamespace: string; + rawName: string; +} + +interface TableOptions { + page: { + index: number; + size: number; + }; + sort: { + field: SortField; + direction: Direction; + }; +} + +const DEFAULT_TABLE_OPTIONS: TableOptions = { + page: { + index: 0, + size: 0, + }, + sort: { + field: DEFAULT_SORT_FIELD, + direction: DEFAULT_SORT_DIRECTION, + }, +}; + +const qualityIssuesAccordionTitle = i18n.translate( + 'unifiedDocViewer.docView.logsOverview.accordion.title.qualityIssues', + { + defaultMessage: 'Quality Issues', + } +); + +const qualityIssuesAccordionTechPreviewBadge = i18n.translate( + 'unifiedDocViewer.docView.logsOverview.accordion.title.techPreview', + { + defaultMessage: 'TECH PREVIEW', + } +); + +const issueColumnName = i18n.translate( + 'unifiedDocViewer.docView.logsOverview.accordion.qualityIssues.table.field', + { + defaultMessage: 'Issue', + } +); + +const valuesColumnName = i18n.translate( + 'unifiedDocViewer.docView.logsOverview.accordion.qualityIssues.table.values', + { + defaultMessage: 'Values', + } +); + +const textFieldIgnored = i18n.translate( + 'unifiedDocViewer.docView.logsOverview.accordion.qualityIssues.table.textIgnored', + { + defaultMessage: 'field ignored', + } +); + +export const datasetQualityLinkTitle = i18n.translate( + 'unifiedDocViewer.docView.logsOverview.accordion.qualityIssues.table.datasetQualityLinkTitle', + { + defaultMessage: 'Data set details', + } +); + +export const LogsOverviewDegradedFields = ({ rawDoc }: { rawDoc: DataTableRecord['raw'] }) => { + const { ignored_field_values: ignoredFieldValues = {}, fields: sourceFields = {} } = rawDoc; + const countOfDegradedFields = Object.keys(ignoredFieldValues)?.length; + + const columns = getDegradedFieldsColumns(); + const tableData = getDataFormattedForTable(ignoredFieldValues); + + const paramsForLocator = getParamsForLocator(sourceFields); + + const accordionId = useGeneratedHtmlId({ + prefix: qualityIssuesAccordionTitle, + }); + + const [tableOptions, setTableOptions] = useState(DEFAULT_TABLE_OPTIONS); + + const onTableChange = (options: { + page: { index: number; size: number }; + sort?: { field: SortField; direction: Direction }; + }) => { + setTableOptions({ + page: { + index: options.page.index, + size: options.page.size, + }, + sort: { + field: options.sort?.field ?? DEFAULT_SORT_FIELD, + direction: options.sort?.direction ?? DEFAULT_SORT_DIRECTION, + }, + }); + }; + + const pagination = useMemo( + () => ({ + pageIndex: tableOptions.page.index, + pageSize: DEFAULT_ROWS_PER_PAGE, + totalItemCount: tableData?.length ?? 0, + hidePerPageOptions: true, + }), + [tableData, tableOptions] + ); + + const renderedItems = useMemo(() => { + const sortedItems = orderBy(tableData, tableOptions.sort.field, tableOptions.sort.direction); + return sortedItems.slice( + tableOptions.page.index * DEFAULT_ROWS_PER_PAGE, + (tableOptions.page.index + 1) * DEFAULT_ROWS_PER_PAGE + ); + }, [tableData, tableOptions]); + + const { share } = getUnifiedDocViewerServices(); + const { url: urlService } = share; + + const accordionTitle = ( + + + +

{qualityIssuesAccordionTitle}

+
+
+ + + {countOfDegradedFields} + + + + + +
+ ); + + return countOfDegradedFields > 0 ? ( + <> + + } + data-test-subj="unifiedDocViewLogsOverviewDegradedFieldsAccordion" + > + + + + + ) : null; +}; + +const getDegradedFieldsColumns = (): Array> => [ + { + name: issueColumnName, + sortable: true, + field: 'issue', + render: (issue: string) => { + return ( + <> + {issue} {textFieldIgnored} + + ); + }, + }, + { + name: valuesColumnName, + sortable: true, + field: 'values', + render: (values: string[]) => { + return values.map((value, idx) => {value}); + }, + }, +]; + +const getDataFormattedForTable = ( + ignoredFieldValues: Record +): DegradedField[] => { + return Object.entries(ignoredFieldValues).map(([field, values]) => ({ + issue: field, + values, + })); +}; + +const getParamsForLocator = ( + sourceFields: DataTableRecord['raw']['fields'] +): ParamsForLocator | undefined => { + if (sourceFields) { + const dataStreamTypeArr = sourceFields['data_stream.type']; + const dataStreamType = dataStreamTypeArr ? dataStreamTypeArr[0] : undefined; + const dataStreamNameArr = sourceFields['data_stream.dataset']; + const dataStreamName = dataStreamNameArr ? dataStreamNameArr[0] : undefined; + const dataStreamNamespaceArr = sourceFields['data_stream.namespace']; + const dataStreamNamespace = dataStreamNamespaceArr ? dataStreamNamespaceArr[0] : undefined; + let rawName; + + if (dataStreamType && dataStreamName && dataStreamNamespace) { + rawName = `${dataStreamType}-${dataStreamName}-${dataStreamNamespace}`; + } + + if (rawName) { + return { + dataStreamType, + dataStreamName, + dataStreamNamespace, + rawName, + }; + } + } +}; + +const DatasetQualityLink = React.memo( + ({ + urlService, + paramsForLocator, + }: { + urlService: BrowserUrlService; + paramsForLocator?: ParamsForLocator; + }) => { + const locator = urlService.locators.get(DATA_QUALITY_LOCATOR_ID); + const locatorParams: DataQualityLocatorParams = paramsForLocator + ? { + flyout: { + dataset: { + rawName: paramsForLocator.rawName, + type: paramsForLocator.dataStreamType, + name: paramsForLocator.dataStreamName, + namespace: paramsForLocator.dataStreamNamespace, + }, + }, + } + : {}; + + const datasetQualityUrl = locator?.getRedirectUrl(locatorParams); + + const navigateToDatasetQuality = () => { + locator?.navigate(locatorParams); + }; + + const datasetQualityLinkProps = getRouterLinkProps({ + href: datasetQualityUrl, + onClick: navigateToDatasetQuality, + }); + + return paramsForLocator ? ( + + {datasetQualityLinkTitle} + + ) : null; + } +); diff --git a/src/plugins/unified_doc_viewer/public/plugin.tsx b/src/plugins/unified_doc_viewer/public/plugin.tsx index 13027a2541084..9c4d7117c37dd 100644 --- a/src/plugins/unified_doc_viewer/public/plugin.tsx +++ b/src/plugins/unified_doc_viewer/public/plugin.tsx @@ -19,6 +19,7 @@ import { CoreStart } from '@kbn/core/public'; import { dynamic } from '@kbn/shared-ux-utility'; import { DiscoverSharedPublicStart } from '@kbn/discover-shared-plugin/public'; import { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public'; +import { SharePluginStart } from '@kbn/share-plugin/public'; import type { UnifiedDocViewerServices } from './types'; export const [getUnifiedDocViewerServices, setUnifiedDocViewerServices] = @@ -52,6 +53,7 @@ export interface UnifiedDocViewerStartDeps { discoverShared: DiscoverSharedPublicStart; fieldFormats: FieldFormatsStart; fieldsMetadata: FieldsMetadataPublicStart; + share: SharePluginStart; } export class UnifiedDocViewerPublicPlugin @@ -121,7 +123,7 @@ export class UnifiedDocViewerPublicPlugin public start(core: CoreStart, deps: UnifiedDocViewerStartDeps) { const { analytics, uiSettings } = core; - const { data, discoverShared, fieldFormats, fieldsMetadata } = deps; + const { data, discoverShared, fieldFormats, fieldsMetadata, share } = deps; const storage = new Storage(localStorage); const unifiedDocViewer = { registry: this.docViewsRegistry, @@ -135,6 +137,7 @@ export class UnifiedDocViewerPublicPlugin storage, uiSettings, unifiedDocViewer, + share, }; setUnifiedDocViewerServices(services); return unifiedDocViewer; diff --git a/src/plugins/unified_doc_viewer/public/types.ts b/src/plugins/unified_doc_viewer/public/types.ts index c19c60da72b13..e471fa87b85c1 100644 --- a/src/plugins/unified_doc_viewer/public/types.ts +++ b/src/plugins/unified_doc_viewer/public/types.ts @@ -5,7 +5,6 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - export type { JsonCodeEditorProps } from './components'; export type { EsDocSearchProps } from './hooks'; export type { UnifiedDocViewerSetup, UnifiedDocViewerStart } from './plugin'; @@ -17,6 +16,7 @@ import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public'; import type { Storage } from '@kbn/kibana-utils-plugin/public'; import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; import type { UnifiedDocViewerStart } from './plugin'; export interface UnifiedDocViewerServices { @@ -28,4 +28,5 @@ export interface UnifiedDocViewerServices { storage: Storage; uiSettings: IUiSettingsClient; unifiedDocViewer: UnifiedDocViewerStart; + share: SharePluginStart; } diff --git a/src/plugins/unified_doc_viewer/tsconfig.json b/src/plugins/unified_doc_viewer/tsconfig.json index fbe2ac83c5f1a..3b271744ed4af 100644 --- a/src/plugins/unified_doc_viewer/tsconfig.json +++ b/src/plugins/unified_doc_viewer/tsconfig.json @@ -32,7 +32,10 @@ "@kbn/discover-shared-plugin", "@kbn/fields-metadata-plugin", "@kbn/unified-data-table", - "@kbn/core-notifications-browser" + "@kbn/core-notifications-browser", + "@kbn/deeplinks-observability", + "@kbn/share-plugin", + "@kbn/router-utils" ], "exclude": [ "target/**/*", diff --git a/src/plugins/unified_search/.storybook/main.js b/src/plugins/unified_search/.storybook/main.js index f0e135be6d8a2..3c5919edc3e53 100644 --- a/src/plugins/unified_search/.storybook/main.js +++ b/src/plugins/unified_search/.storybook/main.js @@ -12,10 +12,7 @@ import { resolve } from 'path'; const mockConfig = { resolve: { alias: { - '@kbn/text-based-languages/public': resolve( - __dirname, - '../public/mocks/text_based_languages_editor.tsx' - ), + '@kbn/esql/public': resolve(__dirname, '../public/mocks/text_based_languages_editor.tsx'), }, }, }; diff --git a/src/plugins/unified_search/kibana.jsonc b/src/plugins/unified_search/kibana.jsonc index 9311d6cece2e2..ad7f73a608857 100644 --- a/src/plugins/unified_search/kibana.jsonc +++ b/src/plugins/unified_search/kibana.jsonc @@ -27,7 +27,7 @@ "kibanaUtils", "kibanaReact", "data", - "textBasedLanguages" + "esql" ] } } diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx index 941040dfd30f8..1ef6086640200 100644 --- a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx @@ -18,7 +18,7 @@ import { isOfAggregateQueryType, getLanguageDisplayName, } from '@kbn/es-query'; -import { TextBasedLangEditor } from '@kbn/text-based-languages/public'; +import { TextBasedLangEditor } from '@kbn/esql/public'; import { EMPTY } from 'rxjs'; import { map } from 'rxjs'; import { throttle } from 'lodash'; diff --git a/src/plugins/unified_search/tsconfig.json b/src/plugins/unified_search/tsconfig.json index 82df1ffe507cf..fc830033a69b7 100644 --- a/src/plugins/unified_search/tsconfig.json +++ b/src/plugins/unified_search/tsconfig.json @@ -37,7 +37,7 @@ "@kbn/react-field", "@kbn/ui-theme", "@kbn/saved-objects-management-plugin", - "@kbn/text-based-languages", + "@kbn/esql", "@kbn/text-based-editor", "@kbn/core-doc-links-browser", "@kbn/core-lifecycle-browser", diff --git a/src/plugins/visualizations/public/actions/add_agg_vis_action.test.ts b/src/plugins/visualizations/public/actions/add_agg_vis_action.test.ts new file mode 100644 index 0000000000000..15003c4b04566 --- /dev/null +++ b/src/plugins/visualizations/public/actions/add_agg_vis_action.test.ts @@ -0,0 +1,62 @@ +/* + * 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 { + AddAggVisualizationPanelAction, + ADD_AGG_VIS_ACTION_ID, + type AddAggVisualizationPanelActionApi, +} from './add_agg_vis_action'; +import type { BaseVisType } from '../vis_types/base_vis_type'; +import { VisGroups } from '../vis_types/vis_groups_enum'; +import { TypesService, type TypesStart } from '../vis_types/types_service'; + +const mockCompatibleEmbeddableAPI: AddAggVisualizationPanelActionApi = { + type: ADD_AGG_VIS_ACTION_ID, + addNewPanel: jest.fn(), + getAppContext: jest.fn(), +}; + +describe('AddAggVisualizationPanelAction', () => { + let typeServiceStart: TypesStart; + + beforeEach(() => { + const typeService = new TypesService(); + + typeServiceStart = typeService.start(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('invoking the compatibility function returns false when initialized with types that are not grouped as agg visualizations', async () => { + jest.spyOn(typeServiceStart, 'all').mockReturnValue([]); + + const addAggVisualizationPanelAction = new AddAggVisualizationPanelAction(typeServiceStart); + + expect( + await addAggVisualizationPanelAction.isCompatible({ embeddable: mockCompatibleEmbeddableAPI }) + ).toBe(false); + }); + + test('invoking the compatibility function returns true when the registered agg visualizations type does not have creation disabled', async () => { + jest.spyOn(typeServiceStart, 'all').mockReturnValue([ + { + group: VisGroups.AGGBASED, + disableCreate: false, + name: 'test visualization', + } as BaseVisType, + ]); + + const addAggVisualizationPanelAction = new AddAggVisualizationPanelAction(typeServiceStart); + + expect( + await addAggVisualizationPanelAction.isCompatible({ embeddable: mockCompatibleEmbeddableAPI }) + ).toBe(true); + }); +}); diff --git a/src/plugins/visualizations/public/actions/add_agg_vis_action.ts b/src/plugins/visualizations/public/actions/add_agg_vis_action.ts index 62c8e3654db6e..d0b7b2d9a7f6d 100644 --- a/src/plugins/visualizations/public/actions/add_agg_vis_action.ts +++ b/src/plugins/visualizations/public/actions/add_agg_vis_action.ts @@ -17,11 +17,13 @@ import { COMMON_EMBEDDABLE_GROUPING } from '@kbn/embeddable-plugin/public'; import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; import { apiHasType } from '@kbn/presentation-publishing'; import { apiCanAddNewPanel, CanAddNewPanel } from '@kbn/presentation-containers'; +import { VisGroups } from '../vis_types/vis_groups_enum'; +import type { TypesStart } from '../vis_types/types_service'; import { showNewVisModal } from '../wizard/show_new_vis'; -const ADD_AGG_VIS_ACTION_ID = 'ADD_AGG_VIS'; +export const ADD_AGG_VIS_ACTION_ID = 'ADD_AGG_VIS'; -type AddAggVisualizationPanelActionApi = HasType & CanAddNewPanel & HasAppContext; +export type AddAggVisualizationPanelActionApi = HasType & CanAddNewPanel & HasAppContext; const isApiCompatible = (api: unknown | null): api is AddAggVisualizationPanelActionApi => { return apiHasType(api) && apiCanAddNewPanel(api) && apiHasAppContext(api); @@ -31,10 +33,15 @@ export class AddAggVisualizationPanelAction implements Action { + return !type.disableCreate && type.group === VisGroups.AGGBASED; + }); + } public getIconType() { return 'visualizeApp'; @@ -47,7 +54,8 @@ export class AddAggVisualizationPanelAction implements Action { diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index c97ff8f4eba45..bb931a072f192 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -400,8 +400,6 @@ export class VisualizationsPlugin uiActions.registerTrigger(dashboardVisualizationPanelTrigger); const editInLensAction = new EditInLensAction(data.query.timefilter.timefilter); uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, editInLensAction); - const addAggVisAction = new AddAggVisualizationPanelAction(); - uiActions.addTriggerAction(ADD_PANEL_TRIGGER, addAggVisAction); const embeddableFactory = new VisualizeEmbeddableFactory({ start }); embeddable.registerEmbeddableFactory(VISUALIZE_EMBEDDABLE_TYPE, embeddableFactory); @@ -499,6 +497,9 @@ export class VisualizationsPlugin setSavedObjectTagging(savedObjectsTaggingOss); } + const addAggVisAction = new AddAggVisualizationPanelAction(types); + uiActions.addTriggerAction(ADD_PANEL_TRIGGER, addAggVisAction); + return { ...types, showNewVisModal, diff --git a/test/functional/services/dashboard/add_panel.ts b/test/functional/services/dashboard/add_panel.ts index ffc62bdfdb68a..16b283f2b5c53 100644 --- a/test/functional/services/dashboard/add_panel.ts +++ b/test/functional/services/dashboard/add_panel.ts @@ -69,9 +69,14 @@ export class DashboardAddPanelService extends FtrService { await this.testSubjects.click(`visType-${visType}`); } - async verifyEmbeddableFactoryGroupExists(groupId: string) { + async verifyEmbeddableFactoryGroupExists(groupId: string, expectExist: boolean = true) { this.log.debug('DashboardAddPanel.verifyEmbeddableFactoryGroupExists'); - await this.testSubjects.existOrFail(`dashboardEditorMenu-${groupId}Group`); + const testSubject = `dashboardEditorMenu-${groupId}Group`; + if (expectExist) { + await this.testSubjects.existOrFail(testSubject); + } else { + await this.testSubjects.missingOrFail(testSubject); + } } async clickAddNewEmbeddableLink(type: string) { diff --git a/tsconfig.base.json b/tsconfig.base.json index 85f84a2609046..6d4637281115e 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -808,6 +808,8 @@ "@kbn/eso-model-version-example/*": ["examples/eso_model_version_example/*"], "@kbn/eso-plugin": ["x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin"], "@kbn/eso-plugin/*": ["x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin/*"], + "@kbn/esql": ["src/plugins/esql"], + "@kbn/esql/*": ["src/plugins/esql/*"], "@kbn/esql-ast": ["packages/kbn-esql-ast"], "@kbn/esql-ast/*": ["packages/kbn-esql-ast/*"], "@kbn/esql-ast-inspector-plugin": ["examples/esql_ast_inspector"], @@ -1310,6 +1312,8 @@ "@kbn/react-kibana-context-theme/*": ["packages/react/kibana_context/theme/*"], "@kbn/react-kibana-mount": ["packages/react/kibana_mount"], "@kbn/react-kibana-mount/*": ["packages/react/kibana_mount/*"], + "@kbn/recently-accessed": ["packages/kbn-recently-accessed"], + "@kbn/recently-accessed/*": ["packages/kbn-recently-accessed/*"], "@kbn/remote-clusters-plugin": ["x-pack/plugins/remote_clusters"], "@kbn/remote-clusters-plugin/*": ["x-pack/plugins/remote_clusters/*"], "@kbn/rendering-plugin": ["test/plugin_functional/plugins/rendering_plugin"], @@ -1724,8 +1728,6 @@ "@kbn/testing-embedded-lens-plugin/*": ["x-pack/examples/testing_embedded_lens/*"], "@kbn/text-based-editor": ["packages/kbn-text-based-editor"], "@kbn/text-based-editor/*": ["packages/kbn-text-based-editor/*"], - "@kbn/text-based-languages": ["src/plugins/text_based_languages"], - "@kbn/text-based-languages/*": ["src/plugins/text_based_languages/*"], "@kbn/third-party-lens-navigation-prompt-plugin": ["x-pack/examples/third_party_lens_navigation_prompt"], "@kbn/third-party-lens-navigation-prompt-plugin/*": ["x-pack/examples/third_party_lens_navigation_prompt/*"], "@kbn/third-party-vis-lens-example-plugin": ["x-pack/examples/third_party_vis_lens_example"], diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/mock/get_anonymized_value/index.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/mock/get_anonymized_value/index.ts index 3822c736b670e..256f9776c4563 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/mock/get_anonymized_value/index.ts +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/mock/get_anonymized_value/index.ts @@ -5,13 +5,6 @@ * 2.0. */ -import { Replacements } from '../../schemas'; - /** This mock returns the reverse of `value` */ -export const mockGetAnonymizedValue = ({ - currentReplacements, - rawValue, -}: { - currentReplacements: Replacements | undefined; - rawValue: string; -}): string => rawValue.split('').reverse().join(''); +export const mockGetAnonymizedValue = ({ rawValue }: { rawValue: string }): string => + rawValue.split('').reverse().join(''); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/assistant_header_flyout.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/assistant_header_flyout.tsx deleted file mode 100644 index 5725d983eff33..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/assistant_header_flyout.tsx +++ /dev/null @@ -1,284 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState, useMemo, useCallback } from 'react'; -import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from '@tanstack/react-query'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiPopover, - EuiContextMenu, - EuiButtonIcon, - EuiPanel, - EuiConfirmModal, - EuiToolTip, -} from '@elastic/eui'; -import { css } from '@emotion/react'; -import { euiThemeVars } from '@kbn/ui-theme'; -import { DocLinksStart } from '@kbn/core-doc-links-browser'; -import { isEmpty } from 'lodash'; -import { Conversation } from '../../..'; -import { AssistantTitle } from '../assistant_title'; -import { ConnectorSelectorInline } from '../../connectorland/connector_selector_inline/connector_selector_inline'; -import { FlyoutNavigation } from '../assistant_overlay/flyout_navigation'; -import { AssistantSettingsButton } from '../settings/assistant_settings_button'; -import * as i18n from './translations'; -import { AIConnector } from '../../connectorland/connector_selector'; - -interface OwnProps { - selectedConversation: Conversation | undefined; - defaultConnector?: AIConnector; - docLinks: Omit; - isDisabled: boolean; - isSettingsModalVisible: boolean; - onToggleShowAnonymizedValues: () => void; - setIsSettingsModalVisible: React.Dispatch>; - showAnonymizedValues: boolean; - onChatCleared: () => void; - onCloseFlyout?: () => void; - chatHistoryVisible?: boolean; - setChatHistoryVisible?: React.Dispatch>; - onConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => void; - conversations: Record; - conversationsLoaded: boolean; - refetchConversationsState: () => Promise; - onConversationCreate: () => Promise; - isAssistantEnabled: boolean; - refetchPrompts?: ( - options?: RefetchOptions & RefetchQueryFilters - ) => Promise>; -} - -type Props = OwnProps; -/** - * Renders the header of the Elastic AI Assistant. - * Provide a user interface for selecting and managing conversations, - * toggling the display of anonymized values, and accessing the assistant settings. - */ -export const AssistantHeaderFlyout: React.FC = ({ - selectedConversation, - defaultConnector, - docLinks, - isDisabled, - isSettingsModalVisible, - onToggleShowAnonymizedValues, - setIsSettingsModalVisible, - showAnonymizedValues, - onChatCleared, - chatHistoryVisible, - setChatHistoryVisible, - onCloseFlyout, - onConversationSelected, - conversations, - conversationsLoaded, - refetchConversationsState, - onConversationCreate, - isAssistantEnabled, - refetchPrompts, -}) => { - const showAnonymizedValuesChecked = useMemo( - () => - selectedConversation?.replacements != null && - Object.keys(selectedConversation?.replacements).length > 0 && - showAnonymizedValues, - [selectedConversation?.replacements, showAnonymizedValues] - ); - - const selectedConnectorId = useMemo( - () => selectedConversation?.apiConfig?.connectorId, - [selectedConversation?.apiConfig?.connectorId] - ); - - const [isPopoverOpen, setPopover] = useState(false); - - const onButtonClick = useCallback(() => { - setPopover(!isPopoverOpen); - }, [isPopoverOpen]); - - const closePopover = useCallback(() => { - setPopover(false); - }, []); - - const [isResetConversationModalVisible, setIsResetConversationModalVisible] = useState(false); - - const closeDestroyModal = useCallback(() => setIsResetConversationModalVisible(false), []); - const showDestroyModal = useCallback(() => setIsResetConversationModalVisible(true), []); - - const onConversationChange = useCallback( - (updatedConversation) => { - onConversationSelected({ - cId: updatedConversation.id, - cTitle: updatedConversation.title, - }); - }, - [onConversationSelected] - ); - - const panels = useMemo( - () => [ - { - id: 0, - items: [ - { - name: i18n.RESET_CONVERSATION, - css: css` - color: ${euiThemeVars.euiColorDanger}; - `, - onClick: showDestroyModal, - icon: 'refresh', - 'data-test-subj': 'clear-chat', - }, - ], - }, - ], - [showDestroyModal] - ); - - const handleReset = useCallback(() => { - onChatCleared(); - closeDestroyModal(); - closePopover(); - }, [onChatCleared, closeDestroyModal, closePopover]); - - return ( - <> - - - - - - - {onCloseFlyout && ( - - - - )} - - - - - - - - - - - - - - - - - - - - - } - isOpen={isPopoverOpen} - closePopover={closePopover} - panelPaddingSize="none" - anchorPosition="downLeft" - > - - - - - - - - {isResetConversationModalVisible && ( - -

{i18n.CLEAR_CHAT_CONFIRMATION}

-
- )} - - ); -}; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.test.tsx index f806f5d1ef7c6..b4f4bd2c25384 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.test.tsx @@ -20,7 +20,7 @@ const mockConversations = { }; const testProps = { conversationsLoaded: true, - currentConversation: welcomeConvo, + selectedConversation: welcomeConvo, title: 'Test Title', docLinks: { ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', @@ -30,12 +30,13 @@ const testProps = { isSettingsModalVisible: false, onConversationSelected, onToggleShowAnonymizedValues: jest.fn(), - selectedConversationId: emptyWelcomeConvo.id, setIsSettingsModalVisible: jest.fn(), - onConversationDeleted: jest.fn(), + onConversationCreate: jest.fn(), + onChatCleared: jest.fn(), showAnonymizedValues: false, conversations: mockConversations, refetchConversationsState: jest.fn(), + isAssistantEnabled: true, anonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] }, refetchAnonymizationFieldsResults: jest.fn(), allPrompts: [], @@ -69,53 +70,64 @@ describe('AssistantHeader', () => { beforeEach(() => { jest.clearAllMocks(); }); - it('showAnonymizedValues is not checked when currentConversation.replacements is null', () => { + it('showAnonymizedValues is not checked when selectedConversation.replacements is null', () => { const { getByText, getByTestId } = render(, { wrapper: TestProviders, }); - expect(getByText('Test Title')).toBeInTheDocument(); - expect(getByTestId('showAnonymizedValues')).toHaveAttribute('aria-checked', 'false'); + expect(getByText(welcomeConvo.title)).toBeInTheDocument(); + expect(getByTestId('showAnonymizedValues').firstChild).toHaveAttribute( + 'data-euiicon-type', + 'eyeClosed' + ); }); - it('showAnonymizedValues is not checked when currentConversation.replacements is empty', () => { + it('showAnonymizedValues is not checked when selectedConversation.replacements is empty', () => { const { getByText, getByTestId } = render( , { wrapper: TestProviders, } ); - expect(getByText('Test Title')).toBeInTheDocument(); - expect(getByTestId('showAnonymizedValues')).toHaveAttribute('aria-checked', 'false'); + expect(getByText(welcomeConvo.title)).toBeInTheDocument(); + expect(getByTestId('showAnonymizedValues').firstChild).toHaveAttribute( + 'data-euiicon-type', + 'eyeClosed' + ); }); - it('showAnonymizedValues is not checked when currentConversation.replacements has values and showAnonymizedValues is false', () => { + it('showAnonymizedValues is not checked when selectedConversation.replacements has values and showAnonymizedValues is false', () => { const { getByTestId } = render( - , + , { wrapper: TestProviders, } ); - expect(getByTestId('showAnonymizedValues')).toHaveAttribute('aria-checked', 'false'); + expect(getByTestId('showAnonymizedValues').firstChild).toHaveAttribute( + 'data-euiicon-type', + 'eyeClosed' + ); }); - it('showAnonymizedValues is checked when currentConversation.replacements has values and showAnonymizedValues is true', () => { + it('showAnonymizedValues is checked when selectedConversation.replacements has values and showAnonymizedValues is true', () => { const { getByTestId } = render( - , + , { wrapper: TestProviders, } ); - expect(getByTestId('showAnonymizedValues')).toHaveAttribute('aria-checked', 'true'); + expect(getByTestId('showAnonymizedValues').firstChild).toHaveAttribute( + 'data-euiicon-type', + 'eye' + ); }); it('Conversation is updated when connector change occurs', async () => { const { getByTestId } = render(, { wrapper: TestProviders, }); - fireEvent.click(getByTestId('connectorSelectorPlaceholderButton')); fireEvent.click(getByTestId('connector-selector')); await act(async () => { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx index 7507c14648614..30e620ea38873 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx @@ -5,44 +5,47 @@ * 2.0. */ -import React, { useCallback, useMemo } from 'react'; +import React, { useState, useMemo, useCallback } from 'react'; +import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from '@tanstack/react-query'; import { EuiFlexGroup, EuiFlexItem, - EuiHorizontalRule, - EuiSpacer, - EuiSwitch, + EuiPopover, + EuiContextMenu, + EuiButtonIcon, + EuiPanel, + EuiConfirmModal, EuiToolTip, } from '@elastic/eui'; -import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from '@tanstack/react-query'; import { css } from '@emotion/react'; -import { DocLinksStart } from '@kbn/core-doc-links-browser'; +import { euiThemeVars } from '@kbn/ui-theme'; import { isEmpty } from 'lodash'; -import { PromptResponse } from '@kbn/elastic-assistant-common'; -import { AIConnector } from '../../connectorland/connector_selector'; import { Conversation } from '../../..'; import { AssistantTitle } from '../assistant_title'; -import { ConversationSelector } from '../conversations/conversation_selector'; +import { ConnectorSelectorInline } from '../../connectorland/connector_selector_inline/connector_selector_inline'; +import { FlyoutNavigation } from '../assistant_overlay/flyout_navigation'; import { AssistantSettingsButton } from '../settings/assistant_settings_button'; import * as i18n from './translations'; +import { AIConnector } from '../../connectorland/connector_selector'; interface OwnProps { - currentConversation?: Conversation; + selectedConversation: Conversation | undefined; defaultConnector?: AIConnector; - docLinks: Omit; isDisabled: boolean; isSettingsModalVisible: boolean; - onConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => void; - onConversationDeleted: (conversationId: string) => void; onToggleShowAnonymizedValues: () => void; setIsSettingsModalVisible: React.Dispatch>; - shouldDisableKeyboardShortcut?: () => boolean; showAnonymizedValues: boolean; - title: string; + onChatCleared: () => void; + onCloseFlyout?: () => void; + chatHistoryVisible?: boolean; + setChatHistoryVisible?: React.Dispatch>; + onConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => void; conversations: Record; conversationsLoaded: boolean; refetchConversationsState: () => Promise; - allPrompts: PromptResponse[]; + onConversationCreate: () => Promise; + isAssistantEnabled: boolean; refetchPrompts?: ( options?: RefetchOptions & RefetchQueryFilters ) => Promise>; @@ -55,31 +58,53 @@ type Props = OwnProps; * toggling the display of anonymized values, and accessing the assistant settings. */ export const AssistantHeader: React.FC = ({ - currentConversation, + selectedConversation, defaultConnector, - docLinks, isDisabled, isSettingsModalVisible, - onConversationSelected, - onConversationDeleted, onToggleShowAnonymizedValues, setIsSettingsModalVisible, - shouldDisableKeyboardShortcut, showAnonymizedValues, - title, + onChatCleared, + chatHistoryVisible, + setChatHistoryVisible, + onCloseFlyout, + onConversationSelected, conversations, conversationsLoaded, refetchConversationsState, - allPrompts, + onConversationCreate, + isAssistantEnabled, refetchPrompts, }) => { const showAnonymizedValuesChecked = useMemo( () => - currentConversation?.replacements != null && - Object.keys(currentConversation?.replacements).length > 0 && + selectedConversation?.replacements != null && + Object.keys(selectedConversation?.replacements).length > 0 && showAnonymizedValues, - [currentConversation?.replacements, showAnonymizedValues] + [selectedConversation?.replacements, showAnonymizedValues] ); + + const selectedConnectorId = useMemo( + () => selectedConversation?.apiConfig?.connectorId, + [selectedConversation?.apiConfig?.connectorId] + ); + + const [isPopoverOpen, setPopover] = useState(false); + + const onButtonClick = useCallback(() => { + setPopover(!isPopoverOpen); + }, [isPopoverOpen]); + + const closePopover = useCallback(() => { + setPopover(false); + }, []); + + const [isResetConversationModalVisible, setIsResetConversationModalVisible] = useState(false); + + const closeDestroyModal = useCallback(() => setIsResetConversationModalVisible(false), []); + const showDestroyModal = useCallback(() => setIsResetConversationModalVisible(true), []); + const onConversationChange = useCallback( (updatedConversation) => { onConversationSelected({ @@ -89,90 +114,163 @@ export const AssistantHeader: React.FC = ({ }, [onConversationSelected] ); - const selectedConversationId = useMemo( - () => - !isEmpty(currentConversation?.id) ? currentConversation?.id : currentConversation?.title, - [currentConversation?.id, currentConversation?.title] + + const panels = useMemo( + () => [ + { + id: 0, + items: [ + { + name: i18n.RESET_CONVERSATION, + css: css` + color: ${euiThemeVars.euiColorDanger}; + `, + onClick: showDestroyModal, + icon: 'refresh', + 'data-test-subj': 'clear-chat', + }, + ], + }, + ], + [showDestroyModal] ); + const handleReset = useCallback(() => { + onChatCleared(); + closeDestroyModal(); + closePopover(); + }, [onChatCleared, closeDestroyModal, closePopover]); + return ( <> - + + + + + + {onCloseFlyout && ( + + + + )} + + + - - - - - - + + + + - <> - - + + + + + - - - - + + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downLeft" + > + + - - - - + + + + {isResetConversationModalVisible && ( + +

{i18n.CLEAR_CHAT_CONFIRMATION}

+
+ )} ); }; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_overlay/flyout_navigation.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_overlay/flyout_navigation.tsx index 85d7360c2870a..3f7c3f7ea1bcb 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_overlay/flyout_navigation.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_overlay/flyout_navigation.tsx @@ -48,6 +48,7 @@ export const FlyoutNavigation = memo( onClick={onToggle} iconType={isExpanded ? 'arrowEnd' : 'arrowStart'} size="xs" + data-test-subj="aiAssistantFlyoutNavigationToggle" aria-label={ isExpanded ? i18n.translate( diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_overlay/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_overlay/index.test.tsx index 679901bc02748..9e6a9164607a3 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_overlay/index.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_overlay/index.test.tsx @@ -24,31 +24,33 @@ describe('AssistantOverlay', () => { it('renders when isAssistantEnabled prop is true and keyboard shortcut is pressed', () => { const { getByTestId } = render( - + ); fireEvent.keyDown(document, { key: ';', ctrlKey: true }); - const modal = getByTestId('ai-assistant-modal'); - expect(modal).toBeInTheDocument(); + const flyout = getByTestId('ai-assistant-flyout'); + expect(flyout).toBeInTheDocument(); }); - it('modal closes when close button is clicked', () => { - const { getByLabelText, queryByTestId } = render( + it('flyout closes when close button is clicked', () => { + const { queryByTestId } = render( - + ); fireEvent.keyDown(document, { key: ';', ctrlKey: true }); - const closeButton = getByLabelText('Closes this modal window'); - fireEvent.click(closeButton); - const modal = queryByTestId('ai-assistant-modal'); - expect(modal).not.toBeInTheDocument(); + const closeButton = queryByTestId('euiFlyoutCloseButton'); + if (closeButton) { + fireEvent.click(closeButton); + } + const flyout = queryByTestId('ai-assistant-flyout'); + expect(flyout).not.toBeInTheDocument(); }); - it('Assistant invoked from shortcut tracking happens on modal open only (not close)', () => { + it('Assistant invoked from shortcut tracking happens on flyout open only (not close)', () => { render( - + ); fireEvent.keyDown(document, { key: ';', ctrlKey: true }); @@ -61,26 +63,26 @@ describe('AssistantOverlay', () => { expect(reportAssistantInvoked).toHaveBeenCalledTimes(1); }); - it('modal closes when shortcut is pressed and modal is already open', () => { + it('flyout closes when shortcut is pressed and flyout is already open', () => { const { queryByTestId } = render( - + ); fireEvent.keyDown(document, { key: ';', ctrlKey: true }); fireEvent.keyDown(document, { key: ';', ctrlKey: true }); - const modal = queryByTestId('ai-assistant-modal'); - expect(modal).not.toBeInTheDocument(); + const flyout = queryByTestId('ai-assistant-flyout'); + expect(flyout).not.toBeInTheDocument(); }); - it('modal does not open when incorrect shortcut is pressed', () => { + it('flyout does not open when incorrect shortcut is pressed', () => { const { queryByTestId } = render( - + ); fireEvent.keyDown(document, { key: 'a', ctrlKey: true }); - const modal = queryByTestId('ai-assistant-modal'); - expect(modal).not.toBeInTheDocument(); + const flyout = queryByTestId('ai-assistant-flyout'); + expect(flyout).not.toBeInTheDocument(); }); }); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_overlay/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_overlay/index.tsx index 44907d8b1fd00..689f60f0a52d9 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_overlay/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_overlay/index.tsx @@ -6,12 +6,12 @@ */ import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { EuiModal, EuiFlyoutResizable, useEuiTheme } from '@elastic/eui'; +import { EuiFlyoutResizable } from '@elastic/eui'; import useEvent from 'react-use/lib/useEvent'; -// eslint-disable-next-line @kbn/eslint/module_migration -import styled from 'styled-components'; import { css } from '@emotion/react'; +// eslint-disable-next-line @kbn/eslint/module_migration +import { createGlobalStyle } from 'styled-components'; import { ShowAssistantOverlayProps, useAssistantContext, @@ -22,23 +22,21 @@ import { WELCOME_CONVERSATION_TITLE } from '../use_conversation/translations'; const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0; -const StyledEuiModal = styled(EuiModal)` - ${({ theme }) => `margin-top: ${theme.eui.euiSizeXXL};`} - min-width: 95vw; - min-height: 25vh; -`; - /** * Modal container for Elastic AI Assistant conversations, receiving the page contents as context, plus whatever * component currently has focus and any specific context it may provide through the SAssInterface. */ export interface Props { - isFlyoutMode: boolean; currentUserAvatar?: UserAvatar; } -export const AssistantOverlay = React.memo(({ isFlyoutMode, currentUserAvatar }) => { - const { euiTheme } = useEuiTheme(); +export const UnifiedTimelineGlobalStyles = createGlobalStyle` + body:has(.timeline-portal-overlay-mask) .euiOverlayMask { + z-index: 1003 !important; + } +`; + +export const AssistantOverlay = React.memo(({ currentUserAvatar }) => { const [isModalVisible, setIsModalVisible] = useState(false); const [conversationTitle, setConversationTitle] = useState( WELCOME_CONVERSATION_TITLE @@ -130,8 +128,8 @@ export const AssistantOverlay = React.memo(({ isFlyoutMode, currentUserAv if (!isModalVisible) return null; - if (isFlyoutMode) { - return ( + return ( + <> (({ isFlyoutMode, currentUserAv data-test-subj="ai-assistant-flyout" paddingSize="none" hideCloseButton - // EUI TODO: This z-index override of EuiOverlayMask is a workaround, and ideally should be resolved with a cleaner UI/UX flow long-term - maskProps={{ style: `z-index: ${(euiTheme.levels.flyout as number) + 3}` }} // we need this flyout to be above the timeline flyout (which has a z-index of 1002) > - ); - } - - return ( - <> - {isModalVisible && ( - - - - )} + ); }); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_title/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_title/index.test.tsx index ee4a998a1439f..d9dd84cb0b51d 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_title/index.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_title/index.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { render, fireEvent } from '@testing-library/react'; +import { render } from '@testing-library/react'; import { AssistantTitle } from '.'; import { TestProviders } from '../../mock/test_providers/test_providers'; @@ -14,7 +14,6 @@ const testProps = { title: 'Test Title', docLinks: { ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', DOC_LINK_VERSION: '7.15' }, selectedConversation: undefined, - isFlyoutMode: false, onChange: jest.fn(), refetchConversationsState: jest.fn(), }; @@ -28,22 +27,4 @@ describe('AssistantTitle', () => { ); expect(getByText('Test Title')).toBeInTheDocument(); }); - - it('clicking on the popover button opens the popover with the correct link', () => { - const { getByTestId, queryByTestId } = render( - - - , - { - wrapper: TestProviders, - } - ); - expect(queryByTestId('tooltipContent')).not.toBeInTheDocument(); - fireEvent.click(getByTestId('tooltipIcon')); - expect(getByTestId('tooltipContent')).toBeInTheDocument(); - expect(getByTestId('externalDocumentationLink')).toHaveAttribute( - 'href', - 'https://www.elastic.co/guide/en/security/7.15/security-assistant.html' - ); - }); }); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_title/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_title/index.tsx index 7e9934afcaa90..2090a92645c65 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_title/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_title/index.tsx @@ -5,24 +5,10 @@ * 2.0. */ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { - EuiButtonIcon, - EuiFlexGroup, - EuiFlexItem, - EuiInlineEditTitle, - EuiLink, - EuiModalHeaderTitle, - EuiPopover, - EuiText, - EuiTitle, -} from '@elastic/eui'; -import type { DocLinksStart } from '@kbn/core-doc-links-browser'; -import { FormattedMessage } from '@kbn/i18n-react'; +import React, { useCallback, useEffect, useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiInlineEditTitle } from '@elastic/eui'; import { css } from '@emotion/react'; -import * as i18n from '../translations'; import type { Conversation } from '../../..'; -import { ConnectorSelectorInline } from '../../connectorland/connector_selector_inline/connector_selector_inline'; import { AssistantAvatar } from '../assistant_avatar/assistant_avatar'; import { useConversation } from '../use_conversation'; import { NEW_CHAT } from '../conversations/conversation_sidepanel/translations'; @@ -32,63 +18,14 @@ import { NEW_CHAT } from '../conversations/conversation_sidepanel/translations'; * information about the assistant feature and access to documentation. */ export const AssistantTitle: React.FC<{ - isDisabled?: boolean; title?: string; - docLinks: Omit; selectedConversation: Conversation | undefined; - isFlyoutMode: boolean; - onChange: (updatedConversation: Conversation) => void; refetchConversationsState: () => Promise; -}> = ({ - isDisabled = false, - title, - docLinks, - selectedConversation, - isFlyoutMode, - onChange, - refetchConversationsState, -}) => { +}> = ({ title, selectedConversation, refetchConversationsState }) => { const [newTitle, setNewTitle] = useState(title); const [newTitleError, setNewTitleError] = useState(false); const { updateConversationTitle } = useConversation(); - const selectedConnectorId = selectedConversation?.apiConfig?.connectorId; - - const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; - const url = `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/security-assistant.html`; - - const documentationLink = useMemo( - () => ( - - {i18n.DOCUMENTATION} - - ), - [url] - ); - - const content = useMemo( - () => ( - - ), - [documentationLink] - ); - - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const onButtonClick = useCallback(() => setIsPopoverOpen((isOpen: boolean) => !isOpen), []); - const closePopover = useCallback(() => setIsPopoverOpen(false), []); - const handleUpdateTitle = useCallback( async (updatedTitle: string) => { setNewTitleError(false); @@ -109,108 +46,33 @@ export const AssistantTitle: React.FC<{ setNewTitle(title); }, [title]); - if (isFlyoutMode) { - return ( - - - - - - setNewTitle(e.currentTarget.nodeValue || '')} - onCancel={() => setNewTitle(title)} - onSave={handleUpdateTitle} - editModeProps={{ - formRowProps: { - fullWidth: true, - }, - }} - /> - - - ); - } - return ( - - - - - - - - - - - -

{title}

-
-
- - - } - isOpen={isPopoverOpen} - closePopover={closePopover} - anchorPosition="rightUp" - > - - -

{content}

-
-
-
-
-
-
- {!isFlyoutMode && ( - - - - )} -
-
-
-
+ + + + + + setNewTitle(e.currentTarget.nodeValue || '')} + onCancel={() => setNewTitle(title)} + onSave={handleUpdateTitle} + editModeProps={{ + formRowProps: { + fullWidth: true, + }, + }} + /> + + ); }; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_actions/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_actions/index.test.tsx index 36936c7565112..7fbd7e1a03366 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_actions/index.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_actions/index.test.tsx @@ -9,14 +9,11 @@ import React from 'react'; import { render, fireEvent, within } from '@testing-library/react'; import { ChatActions } from '.'; -const onChatCleared = jest.fn(); const onSendMessage = jest.fn(); const testProps = { isDisabled: false, isLoading: false, - onChatCleared, onSendMessage, - isFlyoutMode: false, promptValue: 'prompt', }; @@ -26,16 +23,9 @@ describe('ChatActions', () => { }); it('the component renders with all props', () => { const { getByTestId } = render(); - expect(getByTestId('clear-chat')).toHaveAttribute('aria-label', 'Clear chat'); expect(getByTestId('submit-chat')).toHaveAttribute('aria-label', 'Submit message'); }); - it('onChatCleared function is called when clear chat button is clicked', () => { - const { getByTestId } = render(); - fireEvent.click(getByTestId('clear-chat')); - expect(onChatCleared).toHaveBeenCalled(); - }); - it('onSendMessage function is called when send message button is clicked', () => { const { getByTestId } = render(); @@ -49,7 +39,6 @@ describe('ChatActions', () => { isDisabled: true, }; const { getByTestId } = render(); - expect(getByTestId('clear-chat')).toBeDisabled(); expect(getByTestId('submit-chat')).toBeDisabled(); }); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_actions/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_actions/index.tsx index e7ff0922b30ae..ba980356351fd 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_actions/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_actions/index.tsx @@ -7,14 +7,12 @@ import React, { useCallback, useRef } from 'react'; import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; -import { CLEAR_CHAT, SUBMIT_MESSAGE } from '../translations'; +import { SUBMIT_MESSAGE } from '../translations'; interface OwnProps { isDisabled: boolean; isLoading: boolean; - isFlyoutMode: boolean; promptValue?: string; - onChatCleared: () => void; onSendMessage: () => void; } @@ -26,9 +24,7 @@ type Props = OwnProps; export const ChatActions: React.FC = ({ isDisabled, isLoading, - onChatCleared, onSendMessage, - isFlyoutMode, promptValue, }) => { const submitTooltipRef = useRef(null); @@ -39,21 +35,6 @@ export const ChatActions: React.FC = ({ return ( - {!isFlyoutMode && ( - - - - - - )} = ({ aria-label={SUBMIT_MESSAGE} data-test-subj="submit-chat" color="primary" - display={isFlyoutMode && promptValue?.length ? 'fill' : 'base'} - size={isFlyoutMode ? 'm' : 'xs'} - iconType={isFlyoutMode ? 'kqlFunction' : 'returnKey'} + display={promptValue?.length ? 'fill' : 'base'} + size={'m'} + iconType={'kqlFunction'} isDisabled={isDisabled || !promptValue?.length} isLoading={isLoading} onClick={onSendMessage} diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/index.test.tsx index ab7b942476f81..99f30cde68a82 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/index.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/index.test.tsx @@ -12,12 +12,10 @@ import { TestProviders } from '../../mock/test_providers/test_providers'; jest.mock('./use_chat_send'); -const handleOnChatCleared = jest.fn(); const handlePromptChange = jest.fn(); const handleSendMessage = jest.fn(); const handleRegenerateResponse = jest.fn(); const testProps: Props = { - handleOnChatCleared, handlePromptChange, handleSendMessage, handleRegenerateResponse, @@ -25,7 +23,6 @@ const testProps: Props = { isDisabled: false, shouldRefocusPrompt: false, userPrompt: '', - isFlyoutMode: false, }; describe('ChatSend', () => { beforeEach(() => { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/index.tsx index 880d4d5f9f88f..c292a70252a03 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/index.tsx @@ -14,11 +14,10 @@ import { ChatActions } from '../chat_actions'; import { PromptTextArea } from '../prompt_textarea'; import { useAutosizeTextArea } from './use_autosize_textarea'; -export interface Props extends Omit { +export interface Props extends Omit { isDisabled: boolean; shouldRefocusPrompt: boolean; userPrompt: string | null; - isFlyoutMode: boolean; } /** @@ -26,12 +25,10 @@ export interface Props extends Omit { * Allows the user to clear the chat and switch between different system prompts. */ export const ChatSend: React.FC = ({ - handleOnChatCleared, handlePromptChange, handleSendMessage, isDisabled, isLoading, - isFlyoutMode, shouldRefocusPrompt, userPrompt, }) => { @@ -58,7 +55,7 @@ export const ChatSend: React.FC = ({ return ( = ({ handlePromptChange={handlePromptChange} value={promptValue} isDisabled={isDisabled} - isFlyoutMode={isFlyoutMode} /> diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.test.tsx index 17a421313e3a4..a9231499570c7 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.test.tsx @@ -21,7 +21,6 @@ jest.mock('../use_conversation'); jest.mock('../../..'); const setEditingSystemPromptId = jest.fn(); -const setPromptTextPreview = jest.fn(); const setSelectedPromptContexts = jest.fn(); const setUserPrompt = jest.fn(); const sendMessage = jest.fn(); @@ -43,7 +42,6 @@ export const testProps: UseChatSendProps = { } as unknown as HttpSetup, editingSystemPromptId: defaultSystemPrompt.id, setEditingSystemPromptId, - setPromptTextPreview, setSelectedPromptContexts, setUserPrompt, setCurrentConversation, @@ -75,7 +73,6 @@ describe('use chat send', () => { }); result.current.handleOnChatCleared(); expect(clearConversation).toHaveBeenCalled(); - expect(setPromptTextPreview).toHaveBeenCalledWith(''); expect(setUserPrompt).toHaveBeenCalledWith(''); expect(setSelectedPromptContexts).toHaveBeenCalledWith({}); await waitFor(() => { @@ -89,7 +86,6 @@ describe('use chat send', () => { wrapper: TestProviders, }); result.current.handlePromptChange('new prompt'); - expect(setPromptTextPreview).toHaveBeenCalledWith('new prompt'); expect(setUserPrompt).toHaveBeenCalledWith('new prompt'); }); it('handleSendMessage sends message with context prompt when a valid prompt text is provided', async () => { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.tsx index 9d5e822fcdf55..5a70b6ad32cd8 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.tsx @@ -25,7 +25,6 @@ export interface UseChatSendProps { http: HttpSetup; selectedPromptContexts: Record; setEditingSystemPromptId: React.Dispatch>; - setPromptTextPreview: React.Dispatch>; setSelectedPromptContexts: React.Dispatch< React.SetStateAction> >; @@ -54,7 +53,6 @@ export const useChatSend = ({ http, selectedPromptContexts, setEditingSystemPromptId, - setPromptTextPreview, setSelectedPromptContexts, setUserPrompt, setCurrentConversation, @@ -69,7 +67,6 @@ export const useChatSend = ({ const { clearConversation, removeLastMessage } = useConversation(); const handlePromptChange = (prompt: string) => { - setPromptTextPreview(prompt); setUserPrompt(prompt); }; @@ -120,7 +117,6 @@ export const useChatSend = ({ // Reset prompt context selection and preview before sending: setSelectedPromptContexts({}); - setPromptTextPreview(''); const rawResponse = await sendMessage({ apiConfig: currentConversation.apiConfig, @@ -168,7 +164,6 @@ export const useChatSend = ({ selectedPromptContexts, sendMessage, setCurrentConversation, - setPromptTextPreview, setSelectedPromptContexts, toasts, ] @@ -214,7 +209,6 @@ export const useChatSend = ({ conversation: currentConversation, })?.id; - setPromptTextPreview(''); setUserPrompt(''); setSelectedPromptContexts({}); if (currentConversation) { @@ -230,7 +224,6 @@ export const useChatSend = ({ currentConversation, setCurrentConversation, setEditingSystemPromptId, - setPromptTextPreview, setSelectedPromptContexts, setUserPrompt, ]); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/context_pills/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/context_pills/index.test.tsx index 0168c27c7f548..da2dd3008a1b0 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/context_pills/index.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/context_pills/index.test.tsx @@ -33,7 +33,6 @@ const mockPromptContexts: Record = { const defaultProps = { anonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] }, promptContexts: mockPromptContexts, - isFlyoutMode: false, }; describe('ContextPills', () => { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/context_pills/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/context_pills/index.tsx index ce5a0cf59ca6a..d3ae29643804e 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/context_pills/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/context_pills/index.tsx @@ -5,20 +5,14 @@ * 2.0. */ -import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { sortBy } from 'lodash/fp'; import React, { useCallback, useMemo } from 'react'; -// eslint-disable-next-line @kbn/eslint/module_migration -import styled from 'styled-components'; import { FindAnonymizationFieldsResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/find_anonymization_fields_route.gen'; import { getNewSelectedPromptContext } from '../../data_anonymization/get_new_selected_prompt_context'; import type { PromptContext, SelectedPromptContext } from '../prompt_context/types'; -const PillButton = styled(EuiButton)` - margin-right: ${({ theme }) => theme.eui.euiSizeXS}; -`; - interface Props { anonymizationFields: FindAnonymizationFieldsResponse; promptContexts: Record; @@ -26,7 +20,6 @@ interface Props { setSelectedPromptContexts: React.Dispatch< React.SetStateAction> >; - isFlyoutMode: boolean; } const ContextPillsComponent: React.FC = ({ @@ -34,7 +27,6 @@ const ContextPillsComponent: React.FC = ({ promptContexts, selectedPromptContexts, setSelectedPromptContexts, - isFlyoutMode, }) => { const sortedPromptContexts = useMemo( () => sortBy('description', Object.values(promptContexts)), @@ -63,7 +55,7 @@ const ContextPillsComponent: React.FC = ({ {sortedPromptContexts.map(({ description, id, tooltip }) => { // Workaround for known issue where tooltip won't dismiss after button state is changed once clicked // See: https://github.com/elastic/eui/issues/6488#issuecomment-1379656704 - const button = isFlyoutMode ? ( + const button = ( = ({ > {description} - ) : ( - selectPromptContext(id)} - > - {description} - ); return ( diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector/index.tsx index fd9cddc39dbbe..4ee8076c42a9d 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector/index.tsx @@ -35,7 +35,6 @@ interface Props { selectedConversationId: string | undefined; onConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => void; onConversationDeleted: (conversationId: string) => void; - shouldDisableKeyboardShortcut?: () => boolean; isDisabled?: boolean; conversations: Record; allPrompts: PromptResponse[]; @@ -65,7 +64,6 @@ export const ConversationSelector: React.FC = React.memo( defaultConnector, onConversationSelected, onConversationDeleted, - shouldDisableKeyboardShortcut = () => false, isDisabled = false, conversations, allPrompts, @@ -199,9 +197,8 @@ export const ConversationSelector: React.FC = React.memo( const renderOption: ( option: ConversationSelectorOption, - searchValue: string, - OPTION_CONTENT_CLASSNAME: string - ) => React.ReactNode = (option, searchValue, contentClassName) => { + searchValue: string + ) => React.ReactNode = (option, searchValue) => { const { label, id, value } = option; return ( diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector_settings/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector_settings/index.tsx index f4b8f9a79412f..f1edb5a9dc2a9 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector_settings/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector_settings/index.tsx @@ -27,7 +27,6 @@ interface Props { onConversationDeleted: (conversationTitle: string) => void; onConversationSelectionChange: (conversation?: Conversation | string) => void; selectedConversationTitle: string; - shouldDisableKeyboardShortcut?: () => boolean; isDisabled?: boolean; } @@ -62,7 +61,6 @@ export const ConversationSelectorSettings: React.FC = React.memo( onConversationSelectionChange, selectedConversationTitle, isDisabled, - shouldDisableKeyboardShortcut = () => false, }) => { const conversationTitles = useMemo( () => Object.values(conversations).map((c) => c.title), diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings.tsx index cba17030e1577..1584a46ee687a 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings.tsx @@ -49,7 +49,6 @@ export interface ConversationSettingsProps { React.SetStateAction >; isDisabled?: boolean; - isFlyoutMode: boolean; } /** @@ -66,7 +65,6 @@ export const ConversationSettings: React.FC = React.m conversationSettings, http, isDisabled = false, - isFlyoutMode, setAssistantStreamingEnabled, setConversationSettings, conversationsSettingsBulkActions, @@ -127,7 +125,6 @@ export const ConversationSettings: React.FC = React.m conversationsSettingsBulkActions={conversationsSettingsBulkActions} http={http} isDisabled={isDisabled} - isFlyoutMode={isFlyoutMode} selectedConversation={selectedConversationWithApiConfig} setConversationSettings={setConversationSettings} setConversationsSettingsBulkActions={setConversationsSettingsBulkActions} diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings_editor.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings_editor.tsx index 41da376d21b73..cf8275203090b 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings_editor.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings_editor.tsx @@ -31,7 +31,6 @@ export interface ConversationSettingsEditorProps { conversationsSettingsBulkActions: ConversationsBulkActions; http: HttpSetup; isDisabled?: boolean; - isFlyoutMode: boolean; selectedConversation?: Conversation; setConversationSettings: React.Dispatch>>; setConversationsSettingsBulkActions: React.Dispatch< @@ -49,7 +48,6 @@ export const ConversationSettingsEditor: React.FC @@ -304,7 +299,6 @@ export const ConversationSettingsEditor: React.FC diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/index.tsx index 485f89358f57a..10608502e70d3 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/index.tsx @@ -36,7 +36,6 @@ interface Props { defaultConnector?: AIConnector; handleSave: (shouldRefetchConversation?: boolean) => void; isDisabled?: boolean; - isFlyoutMode: boolean; onCancelClick: () => void; setAssistantStreamingEnabled: React.Dispatch>; setConversationSettings: React.Dispatch>>; @@ -62,7 +61,6 @@ const ConversationSettingsManagementComponent: React.FC = ({ conversationsLoaded, handleSave, isDisabled, - isFlyoutMode, onSelectedConversationChange, onCancelClick, selectedConversation, @@ -221,7 +219,6 @@ const ConversationSettingsManagementComponent: React.FC = ({ conversationsSettingsBulkActions={conversationsSettingsBulkActions} http={http} isDisabled={isDisabled} - isFlyoutMode={isFlyoutMode} selectedConversation={selectedConversation} setConversationSettings={setConversationSettings} setConversationsSettingsBulkActions={setConversationsSettingsBulkActions} diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_sidepanel/title_field.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_sidepanel/title_field.tsx index acbda15320277..373c052ede6e1 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_sidepanel/title_field.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_sidepanel/title_field.tsx @@ -32,7 +32,7 @@ const TitleFieldComponent = ({ conversationIds, euiFieldProps }: TitleFieldProps ), value: true, }, - validate: (text: string) => { + validate: () => { if (conversationIds?.includes(value)) { return i18n.translate( 'xpack.elasticAssistant.conversationSidepanel.titleField.uniqueTitle', diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/helpers.test.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/helpers.test.ts index 19d703a271edc..b4ed11a82df9e 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/helpers.test.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/helpers.test.ts @@ -22,12 +22,11 @@ const defaultConversation = { replacements: {}, title: 'conversation_id', }; -const isFlyoutMode = false; describe('helpers', () => { describe('isAssistantEnabled = false', () => { const isAssistantEnabled = false; it('When no conversation history, return only enterprise messaging', () => { - const result = getBlockBotConversation(defaultConversation, isAssistantEnabled, isFlyoutMode); + const result = getBlockBotConversation(defaultConversation, isAssistantEnabled); expect(result.messages).toEqual(enterpriseMessaging); expect(result.messages.length).toEqual(1); }); @@ -47,7 +46,7 @@ describe('helpers', () => { }, ], }; - const result = getBlockBotConversation(conversation, isAssistantEnabled, isFlyoutMode); + const result = getBlockBotConversation(conversation, isAssistantEnabled); expect(result.messages.length).toEqual(2); }); @@ -56,7 +55,7 @@ describe('helpers', () => { ...defaultConversation, messages: enterpriseMessaging, }; - const result = getBlockBotConversation(conversation, isAssistantEnabled, isFlyoutMode); + const result = getBlockBotConversation(conversation, isAssistantEnabled); expect(result.messages.length).toEqual(1); expect(result.messages).toEqual(enterpriseMessaging); }); @@ -77,7 +76,7 @@ describe('helpers', () => { }, ], }; - const result = getBlockBotConversation(conversation, isAssistantEnabled, isFlyoutMode); + const result = getBlockBotConversation(conversation, isAssistantEnabled); expect(result.messages.length).toEqual(3); }); }); @@ -85,8 +84,8 @@ describe('helpers', () => { describe('isAssistantEnabled = true', () => { const isAssistantEnabled = true; it('when no conversation history, returns the welcome conversation', () => { - const result = getBlockBotConversation(defaultConversation, isAssistantEnabled, isFlyoutMode); - expect(result.messages.length).toEqual(3); + const result = getBlockBotConversation(defaultConversation, isAssistantEnabled); + expect(result.messages.length).toEqual(0); }); it('returns a conversation history with the welcome conversation appended', () => { const conversation = { @@ -103,8 +102,8 @@ describe('helpers', () => { }, ], }; - const result = getBlockBotConversation(conversation, isAssistantEnabled, isFlyoutMode); - expect(result.messages.length).toEqual(4); + const result = getBlockBotConversation(conversation, isAssistantEnabled); + expect(result.messages.length).toEqual(1); }); }); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/helpers.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/helpers.ts index e9a0599ca4fc2..f369bf430ea54 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/helpers.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/helpers.ts @@ -10,7 +10,7 @@ import { AIConnector } from '../connectorland/connector_selector'; import { FetchConnectorExecuteResponse, FetchConversationsResponse } from './api'; import { Conversation } from '../..'; import type { ClientMessage } from '../assistant_context/types'; -import { enterpriseMessaging, WELCOME_CONVERSATION } from './use_conversation/sample_conversations'; +import { enterpriseMessaging } from './use_conversation/sample_conversations'; export const getMessageFromRawResponse = ( rawResponse: FetchConnectorExecuteResponse @@ -57,8 +57,7 @@ export const mergeBaseWithPersistedConversations = ( export const getBlockBotConversation = ( conversation: Conversation, - isAssistantEnabled: boolean, - isFlyoutMode: boolean + isAssistantEnabled: boolean ): Conversation => { if (!isAssistantEnabled) { if ( @@ -76,7 +75,7 @@ export const getBlockBotConversation = ( return { ...conversation, - messages: [...conversation.messages, ...(!isFlyoutMode ? WELCOME_CONVERSATION.messages : [])], + messages: conversation.messages, }; }; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.test.tsx index b25945dd247bf..cd0d53bd460c3 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.test.tsx @@ -7,12 +7,11 @@ import React from 'react'; -import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react'; +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { Assistant } from '.'; import type { IHttpFetchError } from '@kbn/core/public'; import { useLoadConnectors } from '../connectorland/use_load_connectors'; -import { useConnectorSetup } from '../connectorland/connector_setup'; import { DefinedUseQueryResult, UseQueryResult } from '@tanstack/react-query'; @@ -40,7 +39,7 @@ jest.mock('./use_conversation'); const renderAssistant = (extraProps = {}, providerProps = {}) => render( - + ); @@ -63,11 +62,12 @@ const mockData = { }, }; const mockDeleteConvo = jest.fn(); +const mockGetDefaultConversation = jest.fn().mockReturnValue(mockData.welcome_id); const clearConversation = jest.fn(); const mockUseConversation = { clearConversation: clearConversation.mockResolvedValue(mockData.welcome_id), getConversation: jest.fn(), - getDefaultConversation: jest.fn().mockReturnValue(mockData.welcome_id), + getDefaultConversation: mockGetDefaultConversation, deleteConversation: mockDeleteConvo, setApiConfig: jest.fn().mockResolvedValue({}), }; @@ -83,10 +83,6 @@ describe('Assistant', () => { persistToLocalStorage = jest.fn(); persistToSessionStorage = jest.fn(); (useConversation as jest.Mock).mockReturnValue(mockUseConversation); - jest.mocked(useConnectorSetup).mockReturnValue({ - comments: [], - prompt: <>, - }); jest.mocked(PromptEditor).mockReturnValue(null); jest.mocked(QuickPrompts).mockReturnValue(null); @@ -221,22 +217,21 @@ describe('Assistant', () => { it('should delete conversation when delete button is clicked', async () => { renderAssistant(); + const deleteButton = screen.getAllByTestId('delete-option')[0]; await act(async () => { - fireEvent.click( - within(screen.getByTestId('conversation-selector')).getByTestId( - 'comboBoxToggleListButton' - ) - ); + fireEvent.click(deleteButton); }); - const deleteButton = screen.getAllByTestId('delete-option')[0]; await act(async () => { - fireEvent.click(deleteButton); + fireEvent.click(screen.getByTestId('confirmModalConfirmButton')); + }); + + await waitFor(() => { + expect(mockDeleteConvo).toHaveBeenCalledWith(mockData.electric_sheep_id.id); }); - expect(mockDeleteConvo).toHaveBeenCalledWith(mockData.welcome_id.id); }); it('should refetchConversationsState after clear chat history button click', async () => { - renderAssistant({ isFlyoutMode: true }); + renderAssistant(); fireEvent.click(screen.getByTestId('chat-context-menu')); fireEvent.click(screen.getByTestId('clear-chat')); fireEvent.click(screen.getByTestId('confirmModalConfirmButton')); @@ -259,7 +254,7 @@ describe('Assistant', () => { expect(persistToLocalStorage).toHaveBeenLastCalledWith(mockData.welcome_id.id); - const previousConversationButton = screen.getByLabelText('Previous conversation'); + const previousConversationButton = await screen.findByText(mockData.electric_sheep_id.title); expect(previousConversationButton).toBeInTheDocument(); await act(async () => { @@ -295,13 +290,13 @@ describe('Assistant', () => { isFetched: true, } as unknown as DefinedUseQueryResult, unknown>); - const { getByLabelText } = renderAssistant(); + const { findByText } = renderAssistant(); expect(persistToLocalStorage).toHaveBeenCalled(); expect(persistToLocalStorage).toHaveBeenLastCalledWith(mockData.welcome_id.id); - const previousConversationButton = getByLabelText('Previous conversation'); + const previousConversationButton = await findByText(mockData.electric_sheep_id.title); expect(previousConversationButton).toBeInTheDocument(); @@ -321,7 +316,7 @@ describe('Assistant', () => { renderAssistant({ setConversationTitle }); await act(async () => { - fireEvent.click(screen.getByLabelText('Previous conversation')); + fireEvent.click(await screen.findByText(mockData.electric_sheep_id.title)); }); expect(setConversationTitle).toHaveBeenLastCalledWith('electric sheep'); @@ -351,7 +346,7 @@ describe('Assistant', () => { } as unknown as DefinedUseQueryResult, unknown>); renderAssistant(); - const previousConversationButton = screen.getByLabelText('Previous conversation'); + const previousConversationButton = await screen.findByText('updated title'); await act(async () => { fireEvent.click(previousConversationButton); }); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx index 6892fdcaf48bd..3fe4e1586e239 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx @@ -5,8 +5,6 @@ * 2.0. */ -/* eslint-disable complexity */ - import React, { Dispatch, SetStateAction, @@ -26,9 +24,6 @@ import { EuiFlyoutFooter, EuiFlyoutHeader, EuiFlyoutBody, - EuiModalFooter, - EuiModalHeader, - EuiModalBody, EuiText, } from '@elastic/eui'; import { euiThemeVars } from '@kbn/ui-theme'; @@ -43,7 +38,6 @@ import { PromptTypeEnum } from '@kbn/elastic-assistant-common/impl/schemas/promp import { useChatSend } from './chat_send/use_chat_send'; import { ChatSend } from './chat_send'; import { BlockBotCallToAction } from './block_bot/cta'; -import { AssistantHeader } from './assistant_header'; import { WELCOME_CONVERSATION_TITLE } from './use_conversation/translations'; import { getDefaultConnector, @@ -57,16 +51,15 @@ import { getNewSelectedPromptContext } from '../data_anonymization/get_new_selec import type { PromptContext, SelectedPromptContext } from './prompt_context/types'; import { useConversation } from './use_conversation'; import { CodeBlockDetails, getDefaultSystemPrompt } from './use_conversation/helpers'; -import { PromptEditor } from './prompt_editor'; import { QuickPrompts } from './quick_prompts/quick_prompts'; import { useLoadConnectors } from '../connectorland/use_load_connectors'; -import { useConnectorSetup } from '../connectorland/connector_setup'; +import { ConnectorSetup } from '../connectorland/connector_setup'; import { ConnectorMissingCallout } from '../connectorland/connector_missing_callout'; import { ConversationSidePanel } from './conversations/conversation_sidepanel'; import { NEW_CHAT } from './conversations/conversation_sidepanel/translations'; import { SystemPrompt } from './prompt_editor/system_prompt'; import { SelectedPromptContexts } from './prompt_editor/selected_prompt_contexts'; -import { AssistantHeaderFlyout } from './assistant_header/assistant_header_flyout'; +import { AssistantHeader } from './assistant_header'; import * as i18n from './translations'; export const CONVERSATION_SIDE_PANEL_WIDTH = 220; @@ -77,17 +70,12 @@ const CommentContainer = styled('span')` overflow: hidden; `; -const ModalPromptEditorWrapper = styled.div` - margin-right: 24px; -`; - import { FetchConversationsResponse, useFetchCurrentUserConversations, CONVERSATIONS_QUERY_KEYS, } from './api/conversations/use_fetch_current_user_conversations'; import { Conversation } from '../assistant_context/types'; -import { clearPresentationData } from '../connectorland/connector_setup/helpers'; import { getGenAiConfig } from '../connectorland/helpers'; import { AssistantAnimatedIcon } from './assistant_animated_icon'; import { useFetchAnonymizationFields } from './api/anonymization_fields/use_fetch_anonymization_fields'; @@ -102,7 +90,6 @@ export interface Props { showTitle?: boolean; setConversationTitle?: Dispatch>; onCloseFlyout?: () => void; - isFlyoutMode?: boolean; chatHistoryVisible?: boolean; setChatHistoryVisible?: Dispatch>; currentUserAvatar?: UserAvatar; @@ -120,7 +107,6 @@ const AssistantComponent: React.FC = ({ showTitle = true, setConversationTitle, onCloseFlyout, - isFlyoutMode = false, chatHistoryVisible, setChatHistoryVisible, currentUserAvatar, @@ -129,14 +115,12 @@ const AssistantComponent: React.FC = ({ assistantTelemetry, augmentMessageCodeBlocks, assistantAvailability: { isAssistantEnabled }, - docLinks, getComments, http, knowledgeBase: { isEnabledKnowledgeBase, isEnabledRAGAlerts }, promptContexts, setLastConversationId, getLastConversationId, - title, baseConversations, } = useAssistantContext(); @@ -251,7 +235,7 @@ const AssistantComponent: React.FC = ({ nextConversation?.id !== '' ? nextConversation?.id : nextConversation?.title ]) ?? conversations[WELCOME_CONVERSATION_TITLE] ?? - getDefaultConversation({ cTitle: WELCOME_CONVERSATION_TITLE, isFlyoutMode }); + getDefaultConversation({ cTitle: WELCOME_CONVERSATION_TITLE }); if ( prev && @@ -278,7 +262,6 @@ const AssistantComponent: React.FC = ({ getDefaultConversation, getLastConversationId, isAssistantEnabled, - isFlyoutMode, ]); // Welcome setup state @@ -295,10 +278,8 @@ const AssistantComponent: React.FC = ({ // Welcome conversation is a special 'setup' case when no connector exists, mostly extracted to `ConnectorSetup` component, // but currently a bit of state is littered throughout the assistant component. TODO: clean up/isolate this state const blockBotConversation = useMemo( - () => - currentConversation && - getBlockBotConversation(currentConversation, isAssistantEnabled, isFlyoutMode), - [currentConversation, isAssistantEnabled, isFlyoutMode] + () => currentConversation && getBlockBotConversation(currentConversation, isAssistantEnabled), + [currentConversation, isAssistantEnabled] ); // Settings modal state (so it isn't shared between assistant instances like Timeline) @@ -325,7 +306,6 @@ const AssistantComponent: React.FC = ({ setLastConversationId, ]); - const [promptTextPreview, setPromptTextPreview] = useState(''); const [autoPopulatedOnce, setAutoPopulatedOnce] = useState(false); const [userPrompt, setUserPrompt] = useState(null); @@ -398,16 +378,10 @@ const AssistantComponent: React.FC = ({ // when scrollHeight changes, parent is scrolled to bottom parent.scrollTop = parent.scrollHeight; - if (isFlyoutMode) { - ( - commentsContainerRef.current?.childNodes[0].childNodes[0] as HTMLElement - ).lastElementChild?.scrollIntoView(); - } + ( + commentsContainerRef.current?.childNodes[0].childNodes[0] as HTMLElement + ).lastElementChild?.scrollIntoView(); }); - - const getWrapper = (children: React.ReactNode, isCommentContainer: boolean) => - isCommentContainer ? {children} : <>{children}; - // End Scrolling const selectedSystemPrompt = useMemo( @@ -446,17 +420,6 @@ const AssistantComponent: React.FC = ({ [allSystemPrompts, refetchCurrentConversation, refetchResults] ); - const { comments: connectorComments, prompt: connectorPrompt } = useConnectorSetup({ - isFlyoutMode, - conversation: blockBotConversation, - onConversationUpdate: handleOnConversationSelected, - onSetupComplete: () => { - if (currentConversation) { - setCurrentConversation(clearPresentationData(currentConversation)); - } - }, - }); - const handleOnConversationDeleted = useCallback( async (cTitle: string) => { await deleteConversation(conversations[cTitle].id); @@ -538,14 +501,6 @@ const AssistantComponent: React.FC = ({ isFetchedAnonymizationFields, ]); - useEffect(() => {}, [ - areConnectorsFetched, - connectors, - conversationsLoaded, - currentConversation, - isLoading, - ]); - const createCodeBlockPortals = useCallback( () => messageCodeBlocks?.map((codeBlocks: CodeBlockDetails[], i: number) => { @@ -576,7 +531,6 @@ const AssistantComponent: React.FC = ({ } = useChatSend({ allSystemPrompts, currentConversation, - setPromptTextPreview, setUserPrompt, editingSystemPromptId, http, @@ -601,7 +555,7 @@ const AssistantComponent: React.FC = ({ [currentConversation, handleSendMessage, refetchResults] ); - const chatbotComments = useMemo( + const comments = useMemo( () => ( <> = ({ isFetchingResponse: isLoadingChatSend, setIsStreaming, currentUserAvatar, - isFlyoutMode, })} - {...(!isFlyoutMode - ? { - css: css` - margin-right: ${euiThemeVars.euiSizeL}; + // Avoid comments going off the flyout + css={css` + padding-bottom: ${euiThemeVars.euiSizeL}; - > li > div:nth-child(2) { - overflow: hidden; - } - `, - } - : { - // Avoid comments going off the flyout - css: css` - padding-bottom: ${euiThemeVars.euiSizeL}; - - > li > div:nth-child(2) { - overflow: hidden; - } - `, - })} + > li > div:nth-child(2) { + overflow: hidden; + } + `} /> {currentConversation?.messages.length !== 0 && selectedPromptContextsCount > 0 && ( )} - - {!isFlyoutMode && - (currentConversation?.messages.length === 0 || selectedPromptContextsCount > 0) && ( - - - - )} ), [ @@ -675,34 +596,10 @@ const AssistantComponent: React.FC = ({ isEnabledRAGAlerts, isLoadingChatSend, currentUserAvatar, - isFlyoutMode, selectedPromptContextsCount, - editingSystemPromptId, - isNewConversation, - isSettingsModalVisible, - promptContexts, - promptTextPreview, - handleOnSystemPromptSelectionChange, - selectedPromptContexts, - allSystemPrompts, ] ); - const comments = useMemo(() => { - if (isDisabled && !isFlyoutMode) { - return ( - - ); - } - - return chatbotComments; - }, [isDisabled, isFlyoutMode, chatbotComments, connectorComments]); - const trackPrompt = useCallback( (promptTitle: string) => { if (currentConversation?.title) { @@ -800,19 +697,14 @@ const AssistantComponent: React.FC = ({ textAlign="center" color={euiThemeVars.euiColorMediumShade} size="xs" - css={ - isFlyoutMode - ? css` - margin: 0 ${euiThemeVars.euiSizeL} ${euiThemeVars.euiSizeM} - ${euiThemeVars.euiSizeL}; - ` - : {} - } + css={css` + margin: 0 ${euiThemeVars.euiSizeL} ${euiThemeVars.euiSizeM} ${euiThemeVars.euiSizeL}; + `} > {i18n.DISCLAIMER} ), - [isFlyoutMode, isNewConversation] + [isNewConversation] ); const flyoutBodyContent = useMemo(() => { @@ -842,7 +734,10 @@ const AssistantComponent: React.FC = ({ - {connectorPrompt} + @@ -879,7 +774,6 @@ const AssistantComponent: React.FC = ({ onSystemPromptSelectionChange={handleOnSystemPromptSelectionChange} isSettingsModalVisible={isSettingsModalVisible} setIsSettingsModalVisible={setIsSettingsModalVisible} - isFlyoutMode allSystemPrompts={allSystemPrompts} /> @@ -905,328 +799,212 @@ const AssistantComponent: React.FC = ({ ); }, [ allSystemPrompts, + blockBotConversation, comments, - connectorPrompt, currentConversation, editingSystemPromptId, + handleOnConversationSelected, handleOnSystemPromptSelectionChange, isSettingsModalVisible, isWelcomeSetup, ]); - if (isFlyoutMode) { - return ( - - {chatHistoryVisible && ( - - - - )} + return ( + + {chatHistoryVisible && ( - - + + )} + + + + - + + + {/* Create portals for each EuiCodeBlock to add the `Investigate in Timeline` action */} + {createCodeBlockPortals()} + + - - + min-height: 100px; + flex: 1; - {/* Create portals for each EuiCodeBlock to add the `Investigate in Timeline` action */} - {createCodeBlockPortals()} - - div { + display: flex; + flex-direction: column; + align-items: stretch; + + > .euiFlyoutBody__banner { + overflow-x: unset; + } - > div { + > .euiFlyoutBody__overflowContent { display: flex; - flex-direction: column; - align-items: stretch; - - > .euiFlyoutBody__banner { - overflow-x: unset; - } - - > .euiFlyoutBody__overflowContent { - display: flex; - flex: 1; - overflow: auto; - } + flex: 1; + overflow: auto; } - `} - banner={ - !isDisabled && - showMissingConnectorCallout && - areConnectorsFetched && ( - 0} - isSettingsModalVisible={isSettingsModalVisible} - setIsSettingsModalVisible={setIsSettingsModalVisible} - isFlyoutMode={isFlyoutMode} - /> - ) } - > - {!isAssistantEnabled ? ( - 0} + isSettingsModalVisible={isSettingsModalVisible} + setIsSettingsModalVisible={setIsSettingsModalVisible} /> - ) : ( - - {flyoutBodyContent} - {disclaimer} - - )} - - + {!isAssistantEnabled ? ( + + } + http={http} + isAssistantEnabled={isAssistantEnabled} + isWelcomeSetup={isWelcomeSetup} + /> + ) : ( + + {flyoutBodyContent} + {disclaimer} + + )} + + + - - {!isDisabled && - Object.keys(promptContexts).length !== selectedPromptContextsCount && ( - - - <> - - {Object.keys(promptContexts).length > 0 && } - - - - )} - - - {Object.keys(selectedPromptContexts).length ? ( - - + {!isDisabled && + Object.keys(promptContexts).length !== selectedPromptContextsCount && ( + + + <> + + {Object.keys(promptContexts).length > 0 && } + - ) : null} + + )} + + {Object.keys(selectedPromptContexts).length ? ( - - - - - {!isDisabled && ( - - + - - )} - - - - - - - ); - } - - return getWrapper( - <> - - {showTitle && ( - - )} + + + - {/* Create portals for each EuiCodeBlock to add the `Investigate in Timeline` action */} - {createCodeBlockPortals()} - - {!isDisabled && !isLoadingAnonymizationFields && !isErrorAnonymizationFields && ( - <> - - {Object.keys(promptContexts).length > 0 && } - - )} - - - - - {' '} - {getWrapper( - <> - {comments} - - {!isDisabled && showMissingConnectorCallout && areConnectorsFetched && ( - <> - - - - 0} - isSettingsModalVisible={isSettingsModalVisible} - setIsSettingsModalVisible={setIsSettingsModalVisible} - isFlyoutMode={isFlyoutMode} - /> - - - + {!isDisabled && ( + + + )} - , - !embeddedLayout - )} - - {disclaimer} - - - - - - {!isDisabled && ( - - )} - - , - embeddedLayout + + + + + + ); }; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/index.test.tsx index 6d421b649a380..e2f55ee89202e 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/index.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/index.test.tsx @@ -39,7 +39,6 @@ const defaultProps: Props = { selectedPromptContexts: {}, setIsSettingsModalVisible: jest.fn(), setSelectedPromptContexts: jest.fn(), - isFlyoutMode: false, allSystemPrompts: [], }; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/index.tsx index 1528435764acd..adf9b7d4aa658 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/index.tsx @@ -31,7 +31,6 @@ export interface Props { setSelectedPromptContexts: React.Dispatch< React.SetStateAction> >; - isFlyoutMode: boolean; allSystemPrompts: PromptResponse[]; } @@ -50,7 +49,6 @@ const PromptEditorComponent: React.FC = ({ selectedPromptContexts, setIsSettingsModalVisible, setSelectedPromptContexts, - isFlyoutMode, allSystemPrompts, }) => { const commentBody = useMemo( @@ -64,17 +62,14 @@ const PromptEditorComponent: React.FC = ({ onSystemPromptSelectionChange={onSystemPromptSelectionChange} isSettingsModalVisible={isSettingsModalVisible} setIsSettingsModalVisible={setIsSettingsModalVisible} - isFlyoutMode={isFlyoutMode} /> )} @@ -90,7 +85,6 @@ const PromptEditorComponent: React.FC = ({ onSystemPromptSelectionChange, isSettingsModalVisible, setIsSettingsModalVisible, - isFlyoutMode, promptContexts, selectedPromptContexts, setSelectedPromptContexts, diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/selected_prompt_contexts/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/selected_prompt_contexts/index.test.tsx index 899ee5ed7488c..873c41731bd20 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/selected_prompt_contexts/index.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/selected_prompt_contexts/index.test.tsx @@ -15,7 +15,6 @@ import type { SelectedPromptContext } from '../../prompt_context/types'; import { Props, SelectedPromptContexts } from '.'; const defaultProps: Props = { - isNewConversation: false, promptContexts: { [mockAlertPromptContext.id]: mockAlertPromptContext, [mockEventPromptContext.id]: mockEventPromptContext, @@ -23,7 +22,6 @@ const defaultProps: Props = { selectedPromptContexts: {}, setSelectedPromptContexts: jest.fn(), currentReplacements: {}, - isFlyoutMode: false, }; const mockSelectedAlertPromptContext: SelectedPromptContext = { @@ -53,61 +51,6 @@ describe('SelectedPromptContexts', () => { }); }); - it('it does NOT render a spacer when isNewConversation is false and selectedPromptContextIds.length is 1', async () => { - render( - - - - ); - - await waitFor(() => { - expect(screen.queryByTestId('spacer')).not.toBeInTheDocument(); - }); - }); - - it('it renders a spacer when isNewConversation is true and selectedPromptContextIds.length is 1', async () => { - render( - - - - ); - - await waitFor(() => { - expect(screen.getByTestId('spacer')).toBeInTheDocument(); - }); - }); - - it('it renders a spacer for each selected prompt context when isNewConversation is false and selectedPromptContextIds.length is 2', async () => { - render( - - - - ); - - await waitFor(() => { - expect(screen.getAllByTestId('spacer')).toHaveLength(2); - }); - }); - it('renders the selected prompt contexts', async () => { const selectedPromptContexts = { [mockAlertPromptContext.id]: mockSelectedAlertPromptContext, diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/selected_prompt_contexts/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/selected_prompt_contexts/index.tsx index d3555b2e2ac86..3a0e6f3ce87d2 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/selected_prompt_contexts/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/selected_prompt_contexts/index.tsx @@ -5,19 +5,10 @@ * 2.0. */ -import { - EuiAccordion, - EuiButtonIcon, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiToolTip, -} from '@elastic/eui'; +import { EuiAccordion, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { isEmpty, omit } from 'lodash/fp'; import React, { useCallback } from 'react'; -// eslint-disable-next-line @kbn/eslint/module_migration -import styled from 'styled-components'; - +import styled from '@emotion/styled'; import { css } from '@emotion/react'; import { euiThemeVars } from '@kbn/ui-theme'; import { Conversation } from '../../../assistant_context/types'; @@ -26,14 +17,12 @@ import type { PromptContext, SelectedPromptContext } from '../../prompt_context/ import * as i18n from './translations'; export interface Props { - isNewConversation: boolean; promptContexts: Record; selectedPromptContexts: Record; setSelectedPromptContexts: React.Dispatch< React.SetStateAction> >; currentReplacements: Conversation['replacements'] | undefined; - isFlyoutMode: boolean; } export const EditorContainer = styled.div<{ @@ -45,20 +34,11 @@ export const EditorContainer = styled.div<{ `; const SelectedPromptContextsComponent: React.FC = ({ - isNewConversation, promptContexts, selectedPromptContexts, setSelectedPromptContexts, currentReplacements, - isFlyoutMode, }) => { - const [accordionState, setAccordionState] = React.useState<'closed' | 'open'>('closed'); - - const onToggle = useCallback( - () => setAccordionState((prev) => (prev === 'open' ? 'closed' : 'open')), - [] - ); - const unselectPromptContext = useCallback( (unselectedId: string) => { setSelectedPromptContexts((prev) => omit(unselectedId, prev)); @@ -71,22 +51,13 @@ const SelectedPromptContextsComponent: React.FC = ({ } return ( - + {Object.keys(selectedPromptContexts) .sort() .map((id) => ( - {!isFlyoutMode && - (isNewConversation || Object.keys(selectedPromptContexts).length > 1) ? ( - - ) : null} = ({ } id={id} - {...(!isFlyoutMode && { onToggle })} paddingSize="s" - {...(isFlyoutMode - ? { - css: css` - background: ${euiThemeVars.euiPageBackgroundColor}; - border-radius: ${euiThemeVars.euiBorderRadius}; + css={css` + background: ${euiThemeVars.euiPageBackgroundColor}; + border-radius: ${euiThemeVars.euiBorderRadius}; - > div:first-child { - color: ${euiThemeVars.euiColorPrimary}; - padding: ${euiThemeVars.euiFormControlPadding}; - } - `, - borders: 'all', - arrowProps: { - color: 'primary', - }, - } - : {})} + > div:first-child { + color: ${euiThemeVars.euiColorPrimary}; + padding: ${euiThemeVars.euiFormControlPadding}; + } + `} + borders={'all'} + arrowProps={{ + color: 'primary', + }} > - {isFlyoutMode ? ( - - ) : ( - - - - )} + ))} diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/helpers.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/helpers.test.tsx index 82b04c60a569c..f13441a3102f9 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/helpers.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/helpers.test.tsx @@ -16,21 +16,21 @@ import { getOptions, getOptionFromPrompt } from './helpers'; describe('helpers', () => { describe('getOptionFromPrompt', () => { it('returns an EuiSuperSelectOption with the correct value', () => { - const option = getOptionFromPrompt({ ...mockSystemPrompt, isFlyoutMode: true }); + const option = getOptionFromPrompt({ ...mockSystemPrompt }); expect(option.value).toBe(mockSystemPrompt.id); }); it('returns an EuiSuperSelectOption with the correct inputDisplay', () => { - const option = getOptionFromPrompt({ ...mockSystemPrompt, isFlyoutMode: false }); + const option = getOptionFromPrompt({ ...mockSystemPrompt }); render(<>{option.inputDisplay}); - expect(screen.getByTestId('systemPromptText')).toHaveTextContent(mockSystemPrompt.content); + expect(screen.getByTestId('systemPromptText')).toHaveTextContent(mockSystemPrompt.name); }); it('shows the expected name in the dropdownDisplay', () => { - const option = getOptionFromPrompt({ ...mockSystemPrompt, isFlyoutMode: true }); + const option = getOptionFromPrompt({ ...mockSystemPrompt }); render({option.dropdownDisplay}); @@ -38,7 +38,7 @@ describe('helpers', () => { }); it('shows the expected prompt content in the dropdownDisplay', () => { - const option = getOptionFromPrompt({ ...mockSystemPrompt, isFlyoutMode: true }); + const option = getOptionFromPrompt({ ...mockSystemPrompt }); render({option.dropdownDisplay}); @@ -51,7 +51,7 @@ describe('helpers', () => { const prompts = [mockSystemPrompt, mockSuperheroSystemPrompt]; const promptIds = prompts.map(({ id }) => id); - const options = getOptions({ prompts, isFlyoutMode: false }); + const options = getOptions({ prompts }); const optionValues = options.map(({ value }) => value); expect(optionValues).toEqual(promptIds); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/helpers.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/helpers.tsx index bd217bb54e9f6..92814927f980a 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/helpers.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/helpers.tsx @@ -8,46 +8,23 @@ import { EuiText, EuiToolTip } from '@elastic/eui'; import type { EuiSuperSelectOption } from '@elastic/eui'; import React from 'react'; -// eslint-disable-next-line @kbn/eslint/module_migration -import styled from 'styled-components'; - -import { css } from '@emotion/react'; +import styled from '@emotion/styled'; import { isEmpty } from 'lodash/fp'; +import { euiThemeVars } from '@kbn/ui-theme'; import { PromptResponse } from '@kbn/elastic-assistant-common'; import { EMPTY_PROMPT } from './translations'; const Strong = styled.strong` - margin-right: ${({ theme }) => theme.eui.euiSizeS}; + margin-right: ${euiThemeVars.euiSizeS}; `; export const getOptionFromPrompt = ({ content, id, name, - showTitles = false, - isFlyoutMode, -}: PromptResponse & { - showTitles?: boolean; - isFlyoutMode: boolean; -}): EuiSuperSelectOption => ({ +}: PromptResponse): EuiSuperSelectOption => ({ value: id, - inputDisplay: isFlyoutMode ? ( - name - ) : ( - - {showTitles ? name : content} - - ), + inputDisplay: {name}, dropdownDisplay: ( <> {name} @@ -64,12 +41,6 @@ export const getOptionFromPrompt = ({ interface GetOptionsProps { prompts: PromptResponse[] | undefined; - showTitles?: boolean; - isFlyoutMode: boolean; } -export const getOptions = ({ - prompts, - showTitles = false, - isFlyoutMode, -}: GetOptionsProps): Array> => - prompts?.map((p) => getOptionFromPrompt({ ...p, showTitles, isFlyoutMode })) ?? []; +export const getOptions = ({ prompts }: GetOptionsProps): Array> => + prompts?.map(getOptionFromPrompt) ?? []; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/index.test.tsx index 34d40852ba505..3b82b1fd0fbe5 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/index.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/index.test.tsx @@ -90,7 +90,6 @@ describe('SystemPrompt', () => { isSettingsModalVisible={isSettingsModalVisible} onSystemPromptSelectionChange={onSystemPromptSelectionChange} setIsSettingsModalVisible={setIsSettingsModalVisible} - isFlyoutMode={false} allSystemPrompts={mockSystemPrompts} /> ); @@ -100,10 +99,6 @@ describe('SystemPrompt', () => { expect(screen.getByTestId('selectSystemPrompt')).toBeInTheDocument(); }); - it('does NOT render the system prompt text', () => { - expect(screen.queryByTestId('systemPromptText')).not.toBeInTheDocument(); - }); - it('does NOT render the edit button', () => { expect(screen.queryByTestId('edit')).not.toBeInTheDocument(); }); @@ -122,26 +117,21 @@ describe('SystemPrompt', () => { isSettingsModalVisible={isSettingsModalVisible} onSystemPromptSelectionChange={onSystemPromptSelectionChange} setIsSettingsModalVisible={setIsSettingsModalVisible} - isFlyoutMode={false} allSystemPrompts={mockSystemPrompts} /> ); }); - it('does NOT render the system prompt select', () => { - expect(screen.queryByTestId('selectSystemPrompt')).not.toBeInTheDocument(); + it('does render the system prompt select', () => { + expect(screen.queryByTestId('selectSystemPrompt')).toBeInTheDocument(); }); it('renders the system prompt text', () => { - expect(screen.getByTestId('systemPromptText')).toHaveTextContent(mockSystemPrompt.content); - }); - - it('renders the edit button', () => { - expect(screen.getByTestId('edit')).toBeInTheDocument(); + expect(screen.getByTestId('systemPromptText')).toHaveTextContent(mockSystemPrompt.name); }); it('renders the clear button', () => { - expect(screen.getByTestId('clear')).toBeInTheDocument(); + expect(screen.getByTestId('clearSystemPrompt')).toBeInTheDocument(); }); }); @@ -158,7 +148,6 @@ describe('SystemPrompt', () => { isSettingsModalVisible={isSettingsModalVisible} onSystemPromptSelectionChange={onSystemPromptSelectionChange} setIsSettingsModalVisible={setIsSettingsModalVisible} - isFlyoutMode={false} allSystemPrompts={mockSystemPrompts} /> @@ -206,7 +195,6 @@ describe('SystemPrompt', () => { isSettingsModalVisible={isSettingsModalVisible} onSystemPromptSelectionChange={onSystemPromptSelectionChange} setIsSettingsModalVisible={setIsSettingsModalVisible} - isFlyoutMode={false} allSystemPrompts={mockSystemPrompts} /> @@ -268,7 +256,6 @@ describe('SystemPrompt', () => { isSettingsModalVisible={isSettingsModalVisible} onSystemPromptSelectionChange={onSystemPromptSelectionChange} setIsSettingsModalVisible={setIsSettingsModalVisible} - isFlyoutMode={false} allSystemPrompts={mockSystemPrompts} /> @@ -337,7 +324,6 @@ describe('SystemPrompt', () => { isSettingsModalVisible={isSettingsModalVisible} onSystemPromptSelectionChange={onSystemPromptSelectionChange} setIsSettingsModalVisible={setIsSettingsModalVisible} - isFlyoutMode={false} allSystemPrompts={mockSystemPrompts} /> @@ -421,7 +407,6 @@ describe('SystemPrompt', () => { isSettingsModalVisible={isSettingsModalVisible} onSystemPromptSelectionChange={onSystemPromptSelectionChange} setIsSettingsModalVisible={setIsSettingsModalVisible} - isFlyoutMode={false} allSystemPrompts={mockSystemPrompts} /> @@ -483,26 +468,6 @@ describe('SystemPrompt', () => { }); }); - it('shows the system prompt select when the edit button is clicked', () => { - render( - - - - ); - - userEvent.click(screen.getByTestId('edit')); - - expect(screen.getByTestId('selectSystemPrompt')).toBeInTheDocument(); - }); - it('shows the system prompt select when system prompt text is clicked', () => { render( @@ -512,7 +477,6 @@ describe('SystemPrompt', () => { isSettingsModalVisible={isSettingsModalVisible} onSystemPromptSelectionChange={onSystemPromptSelectionChange} setIsSettingsModalVisible={setIsSettingsModalVisible} - isFlyoutMode={false} allSystemPrompts={mockSystemPrompts} /> diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/index.tsx index f2808c3e204f1..01fe334eb1f7d 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/index.tsx @@ -5,14 +5,9 @@ * 2.0. */ -import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; - -import { css } from '@emotion/react'; -import { isEmpty } from 'lodash/fp'; import { PromptResponse } from '@kbn/elastic-assistant-common'; import { Conversation } from '../../../..'; -import * as i18n from './translations'; import { SelectSystemPrompt } from './select_system_prompt'; interface Props { @@ -21,7 +16,6 @@ interface Props { isSettingsModalVisible: boolean; onSystemPromptSelectionChange: (systemPromptId: string | undefined) => void; setIsSettingsModalVisible: React.Dispatch>; - isFlyoutMode: boolean; allSystemPrompts: PromptResponse[]; } @@ -31,7 +25,6 @@ const SystemPromptComponent: React.FC = ({ isSettingsModalVisible, onSystemPromptSelectionChange, setIsSettingsModalVisible, - isFlyoutMode, allSystemPrompts, }) => { const selectedPrompt = useMemo(() => { @@ -42,99 +35,24 @@ const SystemPromptComponent: React.FC = ({ } }, [allSystemPrompts, conversation?.apiConfig?.defaultSystemPromptId, editingSystemPromptId]); - const [isEditing, setIsEditing] = React.useState(false); - const handleClearSystemPrompt = useCallback(() => { if (conversation) { onSystemPromptSelectionChange(undefined); } }, [conversation, onSystemPromptSelectionChange]); - const handleEditSystemPrompt = useCallback(() => setIsEditing(true), []); - - if (isFlyoutMode) { - return ( - - ); - } - return ( -
- {selectedPrompt == null || isEditing ? ( - - ) : ( - - - - {isEmpty(selectedPrompt?.content) ? i18n.EMPTY_PROMPT : selectedPrompt?.content} - - - - - - - - - - - - - - - - - - - )} -
+ ); }; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/select_system_prompt/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/select_system_prompt/index.test.tsx index 3796e5b4a81eb..7c8f575cd49d7 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/select_system_prompt/index.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/select_system_prompt/index.test.tsx @@ -48,9 +48,9 @@ const props: Props = { ], conversation: undefined, isSettingsModalVisible: false, + isClearable: true, selectedPrompt: { id: 'default-system-prompt', content: '', name: '', promptType: 'system' }, setIsSettingsModalVisible: jest.fn(), - isFlyoutMode: false, }; const mockUseAssistantContext = { @@ -91,93 +91,27 @@ jest.mock('../../../../assistant_context', () => { describe('SelectSystemPrompt', () => { beforeEach(() => jest.clearAllMocks()); - it('renders the prompt super select when isEditing is true', () => { - const { getByTestId } = render(); + it('renders the prompt super select', () => { + const { getByTestId } = render(); expect(getByTestId(TEST_IDS.PROMPT_SUPERSELECT)).toBeInTheDocument(); }); - it('does NOT render the prompt super select when isEditing is false', () => { - const { queryByTestId } = render(); - - expect(queryByTestId(TEST_IDS.PROMPT_SUPERSELECT)).not.toBeInTheDocument(); - }); - - it('does NOT render the clear system prompt button when isEditing is true', () => { - const { queryByTestId } = render(); - - expect(queryByTestId('clearSystemPrompt')).not.toBeInTheDocument(); - }); - - it('renders the clear system prompt button when isEditing is true AND isClearable is true', () => { - const { getByTestId } = render( - - ); + it('renders the clear system prompt button', () => { + const { getByTestId } = render(); expect(getByTestId('clearSystemPrompt')).toBeInTheDocument(); }); - it('does NOT render the clear system prompt button when isEditing is false', () => { - const { queryByTestId } = render(); - - expect(queryByTestId('clearSystemPrompt')).not.toBeInTheDocument(); - }); - - it('renders the add system prompt button when isEditing is false', () => { - const { getByTestId } = render(); - - expect(getByTestId('addSystemPrompt')).toBeInTheDocument(); - }); - - it('does NOT render the add system prompt button when isEditing is true', () => { - const { queryByTestId } = render(); - - expect(queryByTestId('addSystemPrompt')).not.toBeInTheDocument(); - }); - it('clears the selected system prompt when the clear button is clicked', () => { const clearSelectedSystemPrompt = jest.fn(); const { getByTestId } = render( - + ); userEvent.click(getByTestId('clearSystemPrompt')); expect(clearSelectedSystemPrompt).toHaveBeenCalledTimes(1); }); - - it('hides the select when the clear button is clicked', () => { - const setIsEditing = jest.fn(); - - const { getByTestId } = render( - - ); - - userEvent.click(getByTestId('clearSystemPrompt')); - - expect(setIsEditing).toHaveBeenCalledWith(false); - }); - - it('shows the select when the add button is clicked', () => { - const setIsEditing = jest.fn(); - - const { getByTestId } = render( - - ); - - userEvent.click(getByTestId('addSystemPrompt')); - - expect(setIsEditing).toHaveBeenCalledWith(true); - }); }); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/select_system_prompt/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/select_system_prompt/index.tsx index 0296fa3e636ca..0f10cf6d3063f 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/select_system_prompt/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/select_system_prompt/index.tsx @@ -38,15 +38,11 @@ export interface Props { selectedPrompt: PromptResponse | undefined; clearSelectedSystemPrompt?: () => void; isClearable?: boolean; - isEditing?: boolean; isDisabled?: boolean; isOpen?: boolean; isSettingsModalVisible: boolean; - setIsEditing?: React.Dispatch>; setIsSettingsModalVisible: React.Dispatch>; - showTitles?: boolean; onSystemPromptSelectionChange?: (promptId: string | undefined) => void; - isFlyoutMode: boolean; } const ADD_NEW_SYSTEM_PROMPT = 'ADD_NEW_SYSTEM_PROMPT'; @@ -58,15 +54,11 @@ const SelectSystemPromptComponent: React.FC = ({ selectedPrompt, clearSelectedSystemPrompt, isClearable = false, - isEditing = false, isDisabled = false, isOpen = false, isSettingsModalVisible, onSystemPromptSelectionChange, - setIsEditing, setIsSettingsModalVisible, - showTitles = false, - isFlyoutMode = false, }) => { const { setSelectedSettingsTab } = useAssistantContext(); const { setApiConfig } = useConversation(); @@ -117,10 +109,7 @@ const SelectSystemPromptComponent: React.FC = ({ }, []); // SuperSelect State/Actions - const options = useMemo( - () => getOptions({ prompts: allSystemPrompts, showTitles, isFlyoutMode }), - [allSystemPrompts, showTitles, isFlyoutMode] - ); + const options = useMemo(() => getOptions({ prompts: allSystemPrompts }), [allSystemPrompts]); const onChange = useCallback( (selectedSystemPromptId) => { @@ -134,11 +123,9 @@ const SelectSystemPromptComponent: React.FC = ({ onSystemPromptSelectionChange(selectedSystemPromptId); } setSelectedSystemPrompt(selectedSystemPromptId); - setIsEditing?.(false); }, [ onSystemPromptSelectionChange, - setIsEditing, setIsSettingsModalVisible, setSelectedSettingsTab, setSelectedSystemPrompt, @@ -147,14 +134,8 @@ const SelectSystemPromptComponent: React.FC = ({ const clearSystemPrompt = useCallback(() => { setSelectedSystemPrompt(undefined); - setIsEditing?.(false); clearSelectedSystemPrompt?.(); - }, [clearSelectedSystemPrompt, setIsEditing, setSelectedSystemPrompt]); - - const onShowSelectSystemPrompt = useCallback(() => { - setIsEditing?.(true); - setIsOpenLocal(true); - }, [setIsEditing]); + }, [clearSelectedSystemPrompt, setSelectedSystemPrompt]); return ( = ({ max-width: 100%; `} > - {isEditing && ( - + - - - )} + /> + - {isEditing && isClearable && selectedPrompt && ( + {isClearable && selectedPrompt && ( svg { - width: 8px; - height: 8px; - stroke-width: 2px; - fill: #fff; - stroke: #fff; - } - ` - : undefined - } - /> - - )} - {!isEditing && ( - - svg { + width: 8px; + height: 8px; + stroke-width: 2px; + fill: #fff; + stroke: #fff; + } + `} /> )} diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector.tsx index 2c4826940a7ca..ae5fce935cfe3 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector.tsx @@ -147,9 +147,8 @@ export const SystemPromptSelector: React.FC = React.memo( const renderOption: ( option: SystemPromptSelectorOption, - searchValue: string, - OPTION_CONTENT_CLASSNAME: string - ) => React.ReactNode = (option, searchValue, contentClassName) => { + searchValue: string + ) => React.ReactNode = (option, searchValue) => { const { label, value } = option; return ( isDisabled?: boolean; onPromptSubmit: (value: string) => void; value: string; - isFlyoutMode: boolean; } export const PromptTextArea = forwardRef( - ({ isDisabled = false, value, onPromptSubmit, handlePromptChange, isFlyoutMode }, ref) => { + ({ isDisabled = false, value, onPromptSubmit, handlePromptChange }, ref) => { const onChangeCallback = useCallback( (event: React.ChangeEvent) => { handlePromptChange(event.target.value); @@ -46,8 +45,8 @@ export const PromptTextArea = forwardRef( ( value={value} onChange={onChangeCallback} onKeyDown={onKeyDown} - rows={isFlyoutMode ? 1 : 6} + rows={1} /> ); } diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_selector/quick_prompt_selector.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_selector/quick_prompt_selector.tsx index d29887e8c4f6a..759c6e49e446e 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_selector/quick_prompt_selector.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_selector/quick_prompt_selector.tsx @@ -140,9 +140,8 @@ export const QuickPromptSelector: React.FC = React.memo( const renderOption: ( option: QuickPromptSelectorOption, - searchValue: string, - OPTION_CONTENT_CLASSNAME: string - ) => React.ReactNode = (option, searchValue, contentClassName) => { + searchValue: string + ) => React.ReactNode = (option, searchValue) => { const { color, label, value } = option; return ( ({ + ...jest.requireActual('react-use'), + useMeasure: () => [ + () => {}, + { + width: 500, + }, + ], +})); + jest.mock('../../assistant_context', () => ({ ...jest.requireActual('../../assistant_context'), useAssistantContext: () => mockUseAssistantContext, diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompts.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompts.tsx index c578a58be728d..e910d238ccc5d 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompts.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompts.tsx @@ -27,12 +27,10 @@ import { QUICK_PROMPTS_TAB } from '../settings/const'; export const KNOWLEDGE_BASE_CATEGORY = 'knowledge-base'; -const COUNT_BEFORE_OVERFLOW = 5; interface QuickPromptsProps { setInput: (input: string) => void; setIsSettingsModalVisible: React.Dispatch>; trackPrompt: (prompt: string) => void; - isFlyoutMode: boolean; allPrompts: PromptResponse[]; } @@ -42,7 +40,7 @@ interface QuickPromptsProps { * and localstorage for storing new and edited prompts. */ export const QuickPrompts: React.FC = React.memo( - ({ setInput, setIsSettingsModalVisible, trackPrompt, isFlyoutMode, allPrompts }) => { + ({ setInput, setIsSettingsModalVisible, trackPrompt, allPrompts }) => { const [quickPromptsContainerRef, { width }] = useMeasure(); const { knowledgeBase, promptContexts, setSelectedSettingsTab } = useAssistantContext(); @@ -103,25 +101,15 @@ export const QuickPrompts: React.FC = React.memo( }, [setIsSettingsModalVisible, setSelectedSettingsTab]); const quickPrompts = useMemo(() => { - const visibleCount = isFlyoutMode ? Math.floor(width / 120) : COUNT_BEFORE_OVERFLOW; + const visibleCount = Math.floor(width / 120); const visibleItems = contextFilteredQuickPrompts.slice(0, visibleCount); const overflowItems = contextFilteredQuickPrompts.slice(visibleCount); return { visible: visibleItems, overflow: overflowItems }; - }, [contextFilteredQuickPrompts, isFlyoutMode, width]); + }, [contextFilteredQuickPrompts, width]); return ( - + = React.memo( - ) : ( - - ) + } isOpen={isOverflowPopoverOpen} closePopover={closeOverflowPopover} diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings.test.tsx index 8f4a8680f9c57..9fb8db972e482 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings.test.tsx @@ -55,7 +55,6 @@ const testProps = { selectedConversationId: welcomeConvo.title, onClose, onSave, - isFlyoutMode: false, onConversationSelected, conversations: {}, anonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] }, diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings.tsx index d5bbefe304208..4b46d2b75d0a9 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings.tsx @@ -59,7 +59,6 @@ interface Props { onClose: ( event?: React.KeyboardEvent | React.MouseEvent ) => void; - isFlyoutMode: boolean; onSave: (success: boolean) => Promise; selectedConversationId?: string; onConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => void; @@ -80,7 +79,6 @@ export const AssistantSettings: React.FC = React.memo( onConversationSelected, conversations, conversationsLoaded, - isFlyoutMode, }) => { const { actionTypeRegistry, @@ -338,7 +336,6 @@ export const AssistantSettings: React.FC = React.memo( setAssistantStreamingEnabled={setUpdatedAssistantStreamingEnabled} onSelectedConversationChange={onHandleSelectedConversationChange} http={http} - isFlyoutMode={isFlyoutMode} /> ))} {selectedSettingsTab === QUICK_PROMPTS_TAB && ( diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_button.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_button.test.tsx index 3aab8d1169bfc..0ef76adad9940 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_button.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_button.test.tsx @@ -22,7 +22,6 @@ const testProps = { isSettingsModalVisible: false, selectedConversation: welcomeConvo, setIsSettingsModalVisible, - isFlyoutMode: false, onConversationSelected, conversations: {}, conversationsLoaded: true, diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_button.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_button.tsx index 30f141f219476..0df20b0cd4db2 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_button.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_button.tsx @@ -23,7 +23,6 @@ interface Props { setIsSettingsModalVisible: React.Dispatch>; onConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => void; isDisabled?: boolean; - isFlyoutMode: boolean; conversations: Record; conversationsLoaded: boolean; refetchConversationsState: () => Promise; @@ -42,7 +41,6 @@ export const AssistantSettingsButton: React.FC = React.memo( isSettingsModalVisible, setIsSettingsModalVisible, selectedConversationId, - isFlyoutMode, onConversationSelected, conversations, conversationsLoaded, @@ -92,7 +90,7 @@ export const AssistantSettingsButton: React.FC = React.memo( isDisabled={isDisabled} iconType="gear" size="xs" - {...(isFlyoutMode ? { color: 'text' } : {})} + color="text" /> @@ -103,7 +101,6 @@ export const AssistantSettingsButton: React.FC = React.memo( onConversationSelected={onConversationSelected} onClose={handleCloseModal} onSave={handleSave} - isFlyoutMode={isFlyoutMode} conversations={conversations} conversationsLoaded={conversationsLoaded} /> diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.test.tsx index 15fb05ca1c807..7d70ee5ede730 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.test.tsx @@ -62,7 +62,6 @@ const testProps = { selectedConversation: welcomeConvo, onClose, onSave, - isFlyoutMode: false, onConversationSelected, conversations: {}, anonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] }, diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.tsx index 3f9be4972fe7e..be6370d36e841 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.tsx @@ -49,7 +49,6 @@ interface Props { conversations: Record; conversationsLoaded: boolean; selectedConversation: Conversation; - isFlyoutMode: boolean; refetchConversations: () => void; } @@ -61,7 +60,6 @@ export const AssistantSettingsManagement: React.FC = React.memo( ({ conversations, conversationsLoaded, - isFlyoutMode, refetchConversations, selectedConversation: defaultSelectedConversation, }) => { @@ -304,7 +302,6 @@ export const AssistantSettingsManagement: React.FC = React.memo( conversationsSettingsBulkActions={conversationsSettingsBulkActions} defaultConnector={defaultConnector} handleSave={handleSave} - isFlyoutMode={isFlyoutMode} onCancelClick={onCancelClick} onSelectedConversationChange={onHandleSelectedConversationChange} selectedConversation={selectedConversation} diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/evaluation_settings/evaluation_settings.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/evaluation_settings/evaluation_settings.tsx index fe4d75be04004..71bbab7636a4a 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/evaluation_settings/evaluation_settings.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/evaluation_settings/evaluation_settings.tsx @@ -44,14 +44,10 @@ const DEFAULT_EVAL_TYPES_OPTIONS = [ ]; const DEFAULT_OUTPUT_INDEX = '.kibana-elastic-ai-assistant-evaluation-results'; -interface Props { - onEvaluationSettingsChange?: () => void; -} - /** * Evaluation Settings -- development-only feature for evaluating models */ -export const EvaluationSettings: React.FC = React.memo(({ onEvaluationSettingsChange }) => { +export const EvaluationSettings: React.FC = React.memo(() => { const { actionTypeRegistry, basePath, http, setTraceOptions, traceOptions } = useAssistantContext(); const { data: connectors } = useLoadConnectors({ http }); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_assistant_overlay/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_assistant_overlay/index.tsx index 3396223d192ca..9ccc2cbce815d 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_assistant_overlay/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_assistant_overlay/index.tsx @@ -130,7 +130,7 @@ export const useAssistantOverlay = ( // proxy show / hide calls to assistant context, using our internal prompt context id: // silent:boolean doesn't show the toast notification if the conversation is not found const showAssistantOverlay = useCallback( - async (showOverlay: boolean, silent?: boolean) => { + async (showOverlay: boolean) => { let conversation; if (!isLoading) { conversation = conversationTitle diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/index.tsx index a276aea3ff4ab..4643af5509aeb 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/index.tsx @@ -33,7 +33,6 @@ interface CreateConversationProps { messages?: ClientMessage[]; conversationIds?: string[]; apiConfig?: Conversation['apiConfig']; - isFlyoutMode: boolean; } interface SetApiConfigProps { @@ -126,13 +125,10 @@ export const useConversation = (): UseConversation => { * Create a new conversation with the given conversationId, and optionally add messages */ const getDefaultConversation = useCallback( - ({ cTitle, messages, isFlyoutMode }: CreateConversationProps): Conversation => { + ({ cTitle, messages }: CreateConversationProps): Conversation => { const newConversation: Conversation = cTitle === i18n.WELCOME_CONVERSATION_TITLE - ? { - ...WELCOME_CONVERSATION, - messages: !isFlyoutMode ? WELCOME_CONVERSATION.messages : [], - } + ? WELCOME_CONVERSATION : { ...DEFAULT_CONVERSATION_STATE, id: '', diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/sample_conversations.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/sample_conversations.tsx index 7cdf709192f70..85192f646963c 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/sample_conversations.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/sample_conversations.tsx @@ -13,35 +13,7 @@ export const WELCOME_CONVERSATION: Conversation = { id: '', title: WELCOME_CONVERSATION_TITLE, category: 'assistant', - messages: [ - { - role: 'assistant', - content: i18n.WELCOME_GENERAL, - timestamp: '', - presentation: { - delay: 2 * 1000, - stream: true, - }, - }, - { - role: 'assistant', - content: i18n.WELCOME_GENERAL_2, - timestamp: '', - presentation: { - delay: 1000, - stream: true, - }, - }, - { - role: 'assistant', - content: i18n.WELCOME_GENERAL_3, - timestamp: '', - presentation: { - delay: 1000, - stream: true, - }, - }, - ], + messages: [], replacements: {}, excludeFromLastConversationStorage: true, }; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.tsx index 78336f8a8b03d..65fca75623306 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.tsx @@ -73,7 +73,6 @@ export interface AssistantProviderProps { showAnonymizedValues: boolean; setIsStreaming: (isStreaming: boolean) => void; currentUserAvatar?: UserAvatar; - isFlyoutMode: boolean; }) => EuiCommentProps[]; http: HttpSetup; baseConversations: Record; @@ -114,7 +113,6 @@ export interface UseAssistantContext { showAnonymizedValues: boolean; currentUserAvatar?: UserAvatar; setIsStreaming: (isStreaming: boolean) => void; - isFlyoutMode: boolean; }) => EuiCommentProps[]; http: HttpSetup; knowledgeBase: KnowledgeBaseConfig; @@ -234,9 +232,7 @@ export const AssistantProvider: React.FC = ({ /** * Global Assistant Overlay actions */ - const [showAssistantOverlay, setShowAssistantOverlay] = useState( - (showAssistant) => {} - ); + const [showAssistantOverlay, setShowAssistantOverlay] = useState(() => {}); /** * Settings State diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_missing_callout/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_missing_callout/index.test.tsx index a131e63ae49c3..5465ca19e99de 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_missing_callout/index.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_missing_callout/index.test.tsx @@ -30,7 +30,6 @@ describe('connectorMissingCallout', () => { isConnectorConfigured={false} isSettingsModalVisible={false} setIsSettingsModalVisible={jest.fn()} - isFlyoutMode={false} /> ); @@ -45,7 +44,6 @@ describe('connectorMissingCallout', () => { isConnectorConfigured={true} isSettingsModalVisible={false} setIsSettingsModalVisible={jest.fn()} - isFlyoutMode={false} /> ); @@ -70,7 +68,6 @@ describe('connectorMissingCallout', () => { isConnectorConfigured={true} isSettingsModalVisible={false} setIsSettingsModalVisible={jest.fn()} - isFlyoutMode={false} /> ); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_missing_callout/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_missing_callout/index.tsx index 8853ca0a67d33..26ce2f736ed9a 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_missing_callout/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_missing_callout/index.tsx @@ -20,7 +20,6 @@ interface Props { isConnectorConfigured: boolean; isSettingsModalVisible: boolean; setIsSettingsModalVisible: React.Dispatch>; - isFlyoutMode: boolean; } /** @@ -31,7 +30,7 @@ interface Props { * TODO: Add setting for 'default connector' so we can auto-resolve and not even show this */ export const ConnectorMissingCallout: React.FC = React.memo( - ({ isConnectorConfigured, isSettingsModalVisible, setIsSettingsModalVisible, isFlyoutMode }) => { + ({ isConnectorConfigured, isSettingsModalVisible, setIsSettingsModalVisible }) => { const { assistantAvailability, setSelectedSettingsTab } = useAssistantContext(); const onConversationSettingsClicked = useCallback(() => { @@ -55,13 +54,10 @@ export const ConnectorMissingCallout: React.FC = React.memo( iconType="controlsVertical" size="m" title={i18n.MISSING_CONNECTOR_CALLOUT_TITLE} - css={ - isFlyoutMode && - css` - padding-left: ${euiLightVars.euiPanelPaddingModifiers.paddingMedium} !important; - padding-right: ${euiLightVars.euiPanelPaddingModifiers.paddingMedium} !important; - ` - } + css={css` + padding-left: ${euiLightVars.euiPanelPaddingModifiers.paddingMedium} !important; + padding-right: ${euiLightVars.euiPanelPaddingModifiers.paddingMedium} !important; + `} >

{ beforeEach(() => { jest.clearAllMocks(); }); - it('renders empty selection if no selected connector is provided', () => { + it('renders add new connector button if no selected connector is provided', () => { const { getByTestId } = render( ); - expect(getByTestId('connector-selector')).toBeInTheDocument(); - expect(getByTestId('connector-selector')).toHaveTextContent(''); + fireEvent.click(getByTestId('connector-selector')); + expect(getByTestId('addNewConnectorButton')).toBeInTheDocument(); }); it('renders with provided selected connector', () => { const { getByTestId } = render( diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector/index.tsx index 410ee650c43ef..ad0fffc44e6b5 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector/index.tsx @@ -30,7 +30,6 @@ interface Props { selectedConnectorId?: string; displayFancy?: (displayText: string) => React.ReactNode; setIsOpen?: (isOpen: boolean) => void; - isFlyoutMode: boolean; stats?: AttackDiscoveryStats | null; } @@ -47,7 +46,6 @@ export const ConnectorSelector: React.FC = React.memo( selectedConnectorId, onConnectorSelectionChange, setIsOpen, - isFlyoutMode, stats = null, }) => { const { actionTypeRegistry, http, assistantAvailability } = useAssistantContext(); @@ -177,7 +175,7 @@ export const ConnectorSelector: React.FC = React.memo( return ( <> - {isFlyoutMode && !connectorExists && !connectorOptions.length ? ( + {!connectorExists && !connectorOptions.length ? ( ({ jest.mock('../use_load_connectors', () => ({ useLoadConnectors: jest.fn(() => { return { - data: [], + data: mockConnectors, error: null, isSuccess: true, }; @@ -68,67 +67,61 @@ describe('ConnectorSelectorInline', () => { jest.clearAllMocks(); }); it('renders empty view if no selected conversation is provided', () => { - const { getByText } = render( + const { getByTestId } = render( ); - expect(getByText(i18n.INLINE_CONNECTOR_PLACEHOLDER)).toBeInTheDocument(); + fireEvent.click(getByTestId('connector-selector')); + expect(getByTestId('addNewConnectorButton')).toBeInTheDocument(); }); it('renders empty view if selectedConnectorId is NOT in list of connectors', () => { - const { getByText } = render( + const { getByTestId } = render( ); - expect(getByText(i18n.INLINE_CONNECTOR_PLACEHOLDER)).toBeInTheDocument(); + fireEvent.click(getByTestId('connector-selector')); + expect(getByTestId('addNewConnectorButton')).toBeInTheDocument(); }); - it('Clicking add connector button opens the connector selector', () => { - const { getByTestId, queryByTestId } = render( + it('renders the connector selector', () => { + const { getByTestId } = render( ); - expect(queryByTestId('connector-selector')).not.toBeInTheDocument(); - fireEvent.click(getByTestId('connectorSelectorPlaceholderButton')); expect(getByTestId('connector-selector')).toBeInTheDocument(); }); it('On connector change, update conversation API config', () => { const connectorTwo = mockConnectors[1]; - const { getByTestId, queryByTestId } = render( + const { getByTestId } = render( ); - fireEvent.click(getByTestId('connectorSelectorPlaceholderButton')); fireEvent.click(getByTestId('connector-selector')); fireEvent.click(getByTestId(connectorTwo.id)); - expect(queryByTestId('connector-selector')).not.toBeInTheDocument(); expect(setApiConfig).toHaveBeenCalledWith({ apiConfig: { actionTypeId: '.gen-ai', @@ -151,16 +144,13 @@ describe('ConnectorSelectorInline', () => { ); - fireEvent.click(getByTestId('connectorSelectorPlaceholderButton')); fireEvent.click(getByTestId('connector-selector')); - fireEvent.click(getByTestId('addNewConnectorButton')); expect(getByTestId('connector-selector')).toBeInTheDocument(); expect(setApiConfig).not.toHaveBeenCalled(); }); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector_inline/connector_selector_inline.tsx b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector_inline/connector_selector_inline.tsx index ebf762530af11..19e5db98a74fa 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector_inline/connector_selector_inline.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector_inline/connector_selector_inline.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import React, { useCallback, useState } from 'react'; import { css } from '@emotion/css'; @@ -13,8 +13,6 @@ import { euiThemeVars } from '@kbn/ui-theme'; import type { AttackDiscoveryStats } from '@kbn/elastic-assistant-common'; import { AIConnector, ConnectorSelector } from '../connector_selector'; import { Conversation } from '../../..'; -import { useLoadConnectors } from '../use_load_connectors'; -import * as i18n from '../translations'; import { useAssistantContext } from '../../assistant_context'; import { useConversation } from '../../assistant/use_conversation'; import { getGenAiConfig } from '../helpers'; @@ -25,7 +23,6 @@ interface Props { isDisabled?: boolean; selectedConnectorId?: string; selectedConversation?: Conversation; - isFlyoutMode: boolean; onConnectorIdSelected?: (connectorId: string) => void; onConnectorSelected?: (conversation: Conversation) => void; stats?: AttackDiscoveryStats | null; @@ -53,14 +50,6 @@ const inputDisplayClassName = css` text-overflow: ellipsis; `; -const placeholderButtonClassName = css` - overflow: hidden; - text-overflow: ellipsis; - max-width: 400px; - font-weight: normal; - padding: 0 14px 0 0; -`; - /** * A compact wrapper of the ConnectorSelector component used in the Settings modal. */ @@ -69,29 +58,16 @@ export const ConnectorSelectorInline: React.FC = React.memo( isDisabled = false, selectedConnectorId, selectedConversation, - isFlyoutMode, - onConnectorIdSelected, onConnectorSelected, stats = null, }) => { const [isOpen, setIsOpen] = useState(false); - const { assistantAvailability, http } = useAssistantContext(); + const { assistantAvailability } = useAssistantContext(); const { setApiConfig } = useConversation(); - const { data: aiConnectors } = useLoadConnectors({ - http, - }); - - const selectedConnectorName = - (aiConnectors ?? []).find((c) => c.id === selectedConnectorId)?.name ?? - i18n.INLINE_CONNECTOR_PLACEHOLDER; const localIsDisabled = isDisabled || !assistantAvailability.hasConnectorsReadPrivilege; - const onConnectorClick = useCallback(() => { - setIsOpen(!isOpen); - }, [isOpen]); - const onChange = useCallback( async (connector: AIConnector) => { const connectorId = connector.id; @@ -129,40 +105,6 @@ export const ConnectorSelectorInline: React.FC = React.memo( [selectedConversation, setApiConfig, onConnectorIdSelected, onConnectorSelected] ); - if (isFlyoutMode) { - return ( - - - ( - - {displayText} - - )} - isOpen={isOpen} - isDisabled={localIsDisabled} - selectedConnectorId={selectedConnectorId} - setIsOpen={setIsOpen} - onConnectorSelectionChange={onChange} - isFlyoutMode={isFlyoutMode} - stats={stats} - /> - - - ); - } - return ( = React.memo( responsive={false} > - {isOpen ? ( - ( - - {displayText} - - )} - isOpen - isDisabled={localIsDisabled} - selectedConnectorId={selectedConnectorId} - setIsOpen={setIsOpen} - onConnectorSelectionChange={onChange} - isFlyoutMode={isFlyoutMode} - stats={stats} - /> - ) : ( - - ( + - {selectedConnectorName} - - - )} + {displayText} + + )} + isOpen={isOpen} + isDisabled={localIsDisabled} + selectedConnectorId={selectedConnectorId} + setIsOpen={setIsOpen} + onConnectorSelectionChange={onChange} + stats={stats} + /> ); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_setup/helpers.tsx b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_setup/helpers.tsx deleted file mode 100644 index cb11ca51047f0..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_setup/helpers.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Conversation } from '../../assistant_context/types'; - -/** - * Removes all presentation data from the conversation - * @param conversation - */ -export const clearPresentationData = (conversation: Conversation): Conversation => { - const { messages, ...restConversation } = conversation; - return { - ...restConversation, - messages: messages.map((message) => { - const { presentation, ...restMessages } = message; - return { - ...restMessages, - presentation: undefined, - }; - }), - }; -}; - -/** - * Returns true if the conversation has no presentation data - * @param conversation - */ -export const conversationHasNoPresentationData = (conversation: Conversation): boolean => - !conversation.messages.some((message) => message.presentation !== undefined); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_setup/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_setup/index.test.tsx index cf46b5886a389..b6eaa4578d4a0 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_setup/index.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_setup/index.test.tsx @@ -6,19 +6,15 @@ */ import React from 'react'; -import { useConnectorSetup } from '.'; -import { act, renderHook } from '@testing-library/react-hooks'; import { fireEvent, render } from '@testing-library/react'; import { welcomeConvo } from '../../mock/conversation'; import { TestProviders } from '../../mock/test_providers/test_providers'; -import { EuiCommentList } from '@elastic/eui'; +import { ConnectorSetup } from '.'; -const onSetupComplete = jest.fn(); const onConversationUpdate = jest.fn(); const defaultProps = { conversation: welcomeConvo, - onSetupComplete, onConversationUpdate, }; const newConnector = { actionTypeId: '.gen-ai', name: 'cool name' }; @@ -50,121 +46,40 @@ jest.mock('../../assistant/use_conversation', () => ({ })); jest.spyOn(global, 'clearTimeout'); -describe('useConnectorSetup', () => { +describe('ConnectorSetup', () => { beforeEach(() => { jest.clearAllMocks(); }); - it('should render comments and prompts', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useConnectorSetup(defaultProps), { - wrapper: ({ children }) => {children}, - }); - await waitForNextUpdate(); - expect( - result.current.comments.map((c) => ({ username: c.username, timestamp: c.timestamp })) - ).toEqual([ - { - username: 'You', - timestamp: `at: ${new Date('2024-03-18T18:59:18.174Z').toLocaleString()}`, - }, - { - username: 'Assistant', - timestamp: `at: ${new Date('2024-03-19T18:59:18.174Z').toLocaleString()}`, - }, - ]); - - expect(result.current.prompt.props['data-test-subj']).toEqual('prompt'); + it('should render action type selector', async () => { + const { getByTestId } = render(, { + wrapper: TestProviders, }); + + expect(getByTestId('modal-mock')).toBeInTheDocument(); }); - it('should set api config for each conversation when new connector is saved', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useConnectorSetup(defaultProps), { - wrapper: ({ children }) => {children}, - }); - await waitForNextUpdate(); - const { getByTestId, queryByTestId, rerender } = render(result.current.prompt, { - wrapper: TestProviders, - }); - expect(getByTestId('connectorButton')).toBeInTheDocument(); - expect(queryByTestId('skip-setup-button')).not.toBeInTheDocument(); - fireEvent.click(getByTestId('connectorButton')); - rerender(result.current.prompt); - fireEvent.click(getByTestId('modal-mock')); - expect(setApiConfig).toHaveBeenCalledTimes(1); + it('should set api config for each conversation when new connector is saved', async () => { + const { getByTestId } = render(, { + wrapper: TestProviders, }); + + fireEvent.click(getByTestId('modal-mock')); + expect(setApiConfig).toHaveBeenCalledTimes(1); }); it('should NOT set the api config for each conversation when a new connector is saved and updateConversationsOnSaveConnector is false', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook( - () => - useConnectorSetup({ - ...defaultProps, - updateConversationsOnSaveConnector: false, // <-- don't update the conversations - }), - { - wrapper: ({ children }) => {children}, - } - ); - await waitForNextUpdate(); - const { getByTestId, queryByTestId, rerender } = render(result.current.prompt, { + const { getByTestId } = render( + , + { wrapper: TestProviders, - }); - expect(getByTestId('connectorButton')).toBeInTheDocument(); - expect(queryByTestId('skip-setup-button')).not.toBeInTheDocument(); - fireEvent.click(getByTestId('connectorButton')); + } + ); - rerender(result.current.prompt); - fireEvent.click(getByTestId('modal-mock')); + fireEvent.click(getByTestId('modal-mock')); - expect(setApiConfig).not.toHaveBeenCalled(); - }); - }); - - it('should show skip button if message has presentation data', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook( - () => - useConnectorSetup({ - ...defaultProps, - conversation: { - ...defaultProps.conversation, - messages: [ - { - ...defaultProps.conversation.messages[0], - presentation: { - delay: 0, - stream: false, - }, - }, - ], - }, - }), - { - wrapper: ({ children }) => {children}, - } - ); - await waitForNextUpdate(); - const { getByTestId, queryByTestId } = render(result.current.prompt, { - wrapper: TestProviders, - }); - expect(getByTestId('skip-setup-button')).toBeInTheDocument(); - expect(queryByTestId('connectorButton')).not.toBeInTheDocument(); - }); - }); - it('should call onSetupComplete and setConversations when onHandleMessageStreamingComplete', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useConnectorSetup(defaultProps), { - wrapper: ({ children }) => {children}, - }); - await waitForNextUpdate(); - render(, { - wrapper: TestProviders, - }); - - expect(clearTimeout).toHaveBeenCalled(); - expect(onSetupComplete).toHaveBeenCalled(); - }); + expect(setApiConfig).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_setup/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_setup/index.tsx index 81166bbf90fa1..a27da69709c38 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_setup/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_setup/index.tsx @@ -5,181 +5,44 @@ * 2.0. */ -import React, { useCallback, useMemo, useRef, useState } from 'react'; -import type { EuiCommentProps } from '@elastic/eui'; -import { EuiAvatar, EuiBadge, EuiMarkdownFormat, EuiText, EuiTextAlign } from '@elastic/eui'; -import styled from '@emotion/styled'; -import { css } from '@emotion/react'; +import React, { useCallback, useMemo, useState } from 'react'; import { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public/common/constants'; import { ActionType } from '@kbn/triggers-actions-ui-plugin/public'; import { AddConnectorModal } from '../add_connector_modal'; import { WELCOME_CONVERSATION } from '../../assistant/use_conversation/sample_conversations'; -import { Conversation, ClientMessage } from '../../..'; +import { Conversation } from '../../..'; import { useLoadActionTypes } from '../use_load_action_types'; -import { StreamingText } from '../../assistant/streaming_text'; -import { ConnectorButton } from '../connector_button'; import { useConversation } from '../../assistant/use_conversation'; -import { conversationHasNoPresentationData } from './helpers'; -import * as i18n from '../translations'; import { useAssistantContext } from '../../assistant_context'; import { useLoadConnectors } from '../use_load_connectors'; -import { AssistantAvatar } from '../../assistant/assistant_avatar/assistant_avatar'; import { getGenAiConfig } from '../helpers'; -const ConnectorButtonWrapper = styled.div` - margin-bottom: 10px; -`; - export interface ConnectorSetupProps { conversation?: Conversation; - isFlyoutMode?: boolean; - onSetupComplete?: () => void; onConversationUpdate?: ({ cId, cTitle }: { cId: string; cTitle: string }) => Promise; updateConversationsOnSaveConnector?: boolean; } -export const useConnectorSetup = ({ +export const ConnectorSetup = ({ conversation: defaultConversation, - isFlyoutMode, - onSetupComplete, onConversationUpdate, updateConversationsOnSaveConnector = true, -}: ConnectorSetupProps): { - comments: EuiCommentProps[]; - prompt: React.ReactElement; -} => { +}: ConnectorSetupProps) => { const conversation = useMemo( - () => - defaultConversation || { - ...WELCOME_CONVERSATION, - messages: !isFlyoutMode ? WELCOME_CONVERSATION.messages : [], - }, - [defaultConversation, isFlyoutMode] + () => defaultConversation || WELCOME_CONVERSATION, + [defaultConversation] ); const { setApiConfig } = useConversation(); - const bottomRef = useRef(null); // Access all conversations so we can add connector to all on initial setup const { actionTypeRegistry, http } = useAssistantContext(); - const { - data: connectors, - isSuccess: areConnectorsFetched, - refetch: refetchConnectors, - } = useLoadConnectors({ http }); - const isConnectorConfigured = areConnectorsFetched && !!connectors?.length; + const { refetch: refetchConnectors } = useLoadConnectors({ http }); - const [isConnectorModalVisible, setIsConnectorModalVisible] = useState(false); - const [showAddConnectorButton, setShowAddConnectorButton] = useState(() => { - // If no presentation data on messages, default to showing add connector button so it doesn't delay render and flash on screen - return conversationHasNoPresentationData(conversation); - }); const { data: actionTypes } = useLoadActionTypes({ http }); const [selectedActionType, setSelectedActionType] = useState(null); - const lastConversationMessageIndex = useMemo( - () => conversation.messages.length - 1, - [conversation.messages.length] - ); - - const [currentMessageIndex, setCurrentMessageIndex] = useState( - // If connector is configured or conversation has already been replayed show all messages immediately - isConnectorConfigured || conversationHasNoPresentationData(conversation) - ? lastConversationMessageIndex - : 0 - ); - - const streamingTimeoutRef = useRef(undefined); - - // Once streaming of previous message is complete, proceed to next message - const onHandleMessageStreamingComplete = useCallback(() => { - if (currentMessageIndex === lastConversationMessageIndex) { - clearTimeout(streamingTimeoutRef.current); - return; - } - streamingTimeoutRef.current = window.setTimeout(() => { - bottomRef.current?.scrollIntoView({ block: 'end' }); - return setCurrentMessageIndex(currentMessageIndex + 1); - }, conversation.messages[currentMessageIndex]?.presentation?.delay ?? 0); - return () => clearTimeout(streamingTimeoutRef.current); - }, [conversation.messages, currentMessageIndex, lastConversationMessageIndex]); - - // Show button to add connector after last message has finished streaming - const onHandleLastMessageStreamingComplete = useCallback(() => { - setShowAddConnectorButton(true); - bottomRef.current?.scrollIntoView({ block: 'end' }); - onSetupComplete?.(); - }, [onSetupComplete]); - - // Show button to add connector after last message has finished streaming - const handleSkipSetup = useCallback(() => { - setCurrentMessageIndex(lastConversationMessageIndex); - }, [lastConversationMessageIndex]); - - // Create EuiCommentProps[] from conversation messages - const commentBody = useCallback( - (message: ClientMessage, index: number, length: number) => { - // If timestamp is not set, set it to current time (will update conversation at end of setup) - if ( - conversation.messages[index].timestamp == null || - conversation.messages[index].timestamp.length === 0 - ) { - conversation.messages[index].timestamp = new Date().toISOString(); - } - const isLastMessage = index === length - 1; - const enableStreaming = - (message?.presentation?.stream ?? false) && currentMessageIndex !== length - 1; - return ( - - {(streamedText, isStreamingComplete) => ( - - {streamedText} - - - )} - - ); - }, - [ - conversation.messages, - currentMessageIndex, - onHandleLastMessageStreamingComplete, - onHandleMessageStreamingComplete, - ] - ); - - const comments = useMemo( - () => - conversation.messages.slice(0, currentMessageIndex + 1).map((message, index) => { - const isUser = message.role === 'user'; - const timestamp = `${i18n.CONNECTOR_SETUP_TIMESTAMP_AT}: ${new Date( - message.timestamp - ).toLocaleString()}`; - const commentProps: EuiCommentProps = { - username: isUser ? i18n.CONNECTOR_SETUP_USER_YOU : i18n.CONNECTOR_SETUP_USER_ASSISTANT, - children: commentBody(message, index, conversation.messages.length), - timelineAvatar: ( - - ), - timestamp, - }; - return commentProps; - }), - [commentBody, conversation.messages, currentMessageIndex] - ); - const onSaveConnector = useCallback( async (connector: ActionConnector) => { if (updateConversationsOnSaveConnector) { @@ -204,7 +67,6 @@ export const useConnectorSetup = ({ }); refetchConnectors?.(); - setIsConnectorModalVisible(false); } } else { refetchConnectors?.(); @@ -221,65 +83,17 @@ export const useConnectorSetup = ({ const handleClose = useCallback(() => { setSelectedActionType(null); - setIsConnectorModalVisible(false); }, []); - return { - comments: isFlyoutMode ? [] : comments, - prompt: isFlyoutMode ? ( -

- -
- ) : ( -
- {showAddConnectorButton && ( - - - - )} - {!showAddConnectorButton && ( - - - - {i18n.CONNECTOR_SETUP_SKIP} - - - - )} - {isConnectorModalVisible && ( - - )} -
- ), - }; + return ( + + ); }; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/content/prompts/welcome/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/content/prompts/welcome/translations.ts index 387c1d01422f6..3324d09b50a5a 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/content/prompts/welcome/translations.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/content/prompts/welcome/translations.ts @@ -7,30 +7,6 @@ import { i18n } from '@kbn/i18n'; -export const WELCOME_GENERAL = i18n.translate( - 'xpack.elasticAssistant.securityAssistant.content.prompts.welcome.welcomeGeneralPrompt', - { - defaultMessage: - 'Welcome to your Elastic AI Assistant! I am your 100% open-code portal into your Elastic life. In time, I will be able to answer questions and provide assistance across all your information in Elastic, and oh-so much more. Till then, I hope this early preview will open your mind to the possibilities of what we can create when we work together, in the open. Cheers!', - } -); - -export const WELCOME_GENERAL_2 = i18n.translate( - 'xpack.elasticAssistant.securityAssistant.content.prompts.welcome.welcomeGeneral2Prompt', - { - defaultMessage: - "First things first, we'll need to set up a Generative AI Connector to get this chat experience going! With the Generative AI Connector, you'll be able to configure access to either an OpenAI service or an Amazon Bedrock service, but you better believe you'll be able to deploy your own models within your Elastic Cloud instance and use those here in the future... 😉", - } -); - -export const WELCOME_GENERAL_3 = i18n.translate( - 'xpack.elasticAssistant.securityAssistant.content.prompts.welcome.welcomeGeneral3Prompt', - { - defaultMessage: - 'Go ahead and click the add connector button below to continue the conversation!', - } -); - export const ENTERPRISE = i18n.translate( 'xpack.elasticAssistant.securityAssistant.content.prompts.welcome.enterprisePrompt', { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/context_preview.tsx b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/context_preview.tsx index 91b676c491e47..49d5d0fe4d63d 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/context_preview.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/context_preview.tsx @@ -48,6 +48,7 @@ const SelectedPromptContextPreviewComponent = ({ return ( { rawData: 'test-raw-data', }; - it('renders stats', () => { - render( - - - - ); - - expect(screen.getByTestId('stats')).toBeInTheDocument(); - }); - describe('when rawData is a string (non-anonymized data)', () => { it('renders the ReadOnlyContextViewer when rawData is (non-anonymized data)', () => { render( @@ -61,7 +45,6 @@ describe('DataAnonymizationEditor', () => { selectedPromptContext={mockSelectedPromptContext} setSelectedPromptContexts={jest.fn()} currentReplacements={{}} - isFlyoutMode={false} /> ); @@ -76,7 +59,6 @@ describe('DataAnonymizationEditor', () => { selectedPromptContext={mockSelectedPromptContext} setSelectedPromptContexts={jest.fn()} currentReplacements={{}} - isFlyoutMode={false} /> ); @@ -105,24 +87,17 @@ describe('DataAnonymizationEditor', () => { selectedPromptContext={selectedPromptContextWithAnonymized} setSelectedPromptContexts={setSelectedPromptContexts} currentReplacements={{}} - isFlyoutMode={false} /> ); }); - it('renders the ContextEditor when rawData is anonymized data', () => { - expect(screen.getByTestId('contextEditor')).toBeInTheDocument(); + it('renders the SelectedPromptContextPreview when rawData is anonymized data', () => { + expect(screen.getByTestId('selectedPromptContextPreview')).toBeInTheDocument(); }); it('does NOT render the ReadOnlyContextViewer when rawData is anonymized data', () => { expect(screen.queryByTestId('readOnlyContextViewer')).not.toBeInTheDocument(); }); - - it('calls setSelectedPromptContexts when a field is toggled', () => { - userEvent.click(screen.getAllByTestId('allowed')[0]); // toggle the first field - - expect(setSelectedPromptContexts).toBeCalled(); - }); }); }); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/index.tsx index 1fd0e31c78767..0794ca4330350 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiPanel, EuiSpacer } from '@elastic/eui'; +import { EuiPanel } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; import styled from '@emotion/styled'; import { AnonymizedData } from '@kbn/elastic-assistant-common/impl/data_anonymization/types'; @@ -14,9 +14,7 @@ import { BatchUpdateListItem } from './context_editor/types'; import { getIsDataAnonymizable, updateSelectedPromptContext } from './helpers'; import { ReadOnlyContextViewer } from './read_only_context_viewer'; import { ContextEditorFlyout } from './context_editor_flyout'; -import { ContextEditor } from './context_editor'; import { ReplacementsContextViewer } from './replacements_context_viewer'; -import { Stats } from './stats'; const EditorContainer = styled.div` overflow-x: auto; @@ -28,14 +26,12 @@ export interface Props { React.SetStateAction> >; currentReplacements: AnonymizedData['replacements'] | undefined; - isFlyoutMode: boolean; } const DataAnonymizationEditorComponent: React.FC = ({ selectedPromptContext, setSelectedPromptContexts, currentReplacements, - isFlyoutMode, }) => { const isDataAnonymizable = useMemo( () => getIsDataAnonymizable(selectedPromptContext.rawData), @@ -63,66 +59,27 @@ const DataAnonymizationEditorComponent: React.FC = ({ [selectedPromptContext, setSelectedPromptContexts] ); - if (isFlyoutMode) { - return ( - - - {typeof selectedPromptContext.rawData === 'string' ? ( - selectedPromptContext.replacements != null ? ( - - ) : ( - - ) - ) : ( - - )} - - - ); - } - return ( - - - - - {typeof selectedPromptContext.rawData === 'string' ? ( - selectedPromptContext.replacements != null ? ( - + + {typeof selectedPromptContext.rawData === 'string' ? ( + selectedPromptContext.replacements != null ? ( + + ) : ( + + ) ) : ( - - ) - ) : ( - - )} + + )} + ); }; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/mock/get_anonymized_value/index.ts b/x-pack/packages/kbn-elastic-assistant/impl/mock/get_anonymized_value/index.ts index a6d5c4e5d3972..256f9776c4563 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/mock/get_anonymized_value/index.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/mock/get_anonymized_value/index.ts @@ -5,13 +5,6 @@ * 2.0. */ -import { Replacements } from '@kbn/elastic-assistant-common'; - /** This mock returns the reverse of `value` */ -export const mockGetAnonymizedValue = ({ - currentReplacements, - rawValue, -}: { - currentReplacements: Replacements | undefined; - rawValue: string; -}): string => rawValue.split('').reverse().join(''); +export const mockGetAnonymizedValue = ({ rawValue }: { rawValue: string }): string => + rawValue.split('').reverse().join(''); diff --git a/x-pack/packages/kbn-entities-schema/src/schema/common.ts b/x-pack/packages/kbn-entities-schema/src/schema/common.ts index b0d4b7247d12c..bb9d07b1957f4 100644 --- a/x-pack/packages/kbn-entities-schema/src/schema/common.ts +++ b/x-pack/packages/kbn-entities-schema/src/schema/common.ts @@ -96,3 +96,9 @@ export const identityFieldsSchema = z optional: z.boolean(), }) .or(z.string().transform((value) => ({ field: value, optional: false }))); + +const semVerRegex = new RegExp(/^[0-9]{1,}\.[0-9]{1,}\.[0-9]{1,}$/); +export const semVerSchema = z.string().refine((maybeSemVer) => semVerRegex.test(maybeSemVer), { + message: + 'The string does use the Semantic Versioning (Semver) format of {major}.{minor}.{patch} (e.g., 1.0.0), ensure each part contains only digits.', +}); diff --git a/x-pack/packages/kbn-entities-schema/src/schema/entity_definition.ts b/x-pack/packages/kbn-entities-schema/src/schema/entity_definition.ts index 997a2f7d1fc72..d433cc473a538 100644 --- a/x-pack/packages/kbn-entities-schema/src/schema/entity_definition.ts +++ b/x-pack/packages/kbn-entities-schema/src/schema/entity_definition.ts @@ -13,10 +13,12 @@ import { filterSchema, durationSchema, identityFieldsSchema, + semVerSchema, } from './common'; export const entityDefinitionSchema = z.object({ id: z.string().regex(/^[\w-]+$/), + version: semVerSchema, name: z.string(), description: z.optional(z.string()), type: z.string(), diff --git a/x-pack/packages/ml/trained_models_utils/src/constants/trained_models.ts b/x-pack/packages/ml/trained_models_utils/src/constants/trained_models.ts index 0e19fef36e2e2..49acb6e1d5169 100644 --- a/x-pack/packages/ml/trained_models_utils/src/constants/trained_models.ts +++ b/x-pack/packages/ml/trained_models_utils/src/constants/trained_models.ts @@ -201,6 +201,52 @@ export type InferenceServiceSettings = api_key: string; organization_id: string; url: string; + model_id: string; + }; + } + | { + service: 'mistral'; + service_settings: { + api_key: string; + model: string; + max_input_tokens: string; + rate_limit: { + requests_per_minute: number; + }; + }; + } + | { + service: 'cohere'; + service_settings: { + similarity: string; + dimensions: string; + model_id: string; + embedding_type: string; + }; + } + | { + service: 'azureaistudio'; + service_settings: { + target: string; + provider: string; + embedding_type: string; + }; + } + | { + service: 'azureopenai'; + service_settings: { + resource_name: string; + deployment_id: string; + api_version: string; + }; + } + | { + service: 'googleaistudio'; + service_settings: { + model_id: string; + rate_limit: { + requests_per_minute: number; + }; }; } | { diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/storage_details/helpers.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/storage_details/helpers.ts index 5b3860af44920..3eaf493222cb0 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/storage_details/helpers.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/storage_details/helpers.ts @@ -134,37 +134,34 @@ export const getFlattenedBuckets = ({ if (((isILMAvailable && ilmExplain != null) || !isILMAvailable) && stats != null) { return [ ...acc, - ...Object.entries(stats).reduce( - (validStats, [indexName, indexStats]) => { - const ilmPhase = getIlmPhase(ilmExplain?.[indexName], isILMAvailable); - const isSelectedPhase = - (isILMAvailable && ilmPhase != null && ilmPhasesMap[ilmPhase] != null) || - !isILMAvailable; - - if (isSelectedPhase) { - const incompatible = - results != null && results[indexName] != null - ? results[indexName].incompatible - : undefined; - const sizeInBytes = getSizeInBytes({ indexName, stats }); - const docsCount = getDocsCount({ stats, indexName }); - return [ - ...validStats, - { - ilmPhase, - incompatible, - indexName, - pattern, - sizeInBytes, - docsCount, - }, - ]; - } else { - return validStats; - } - }, - [] - ), + ...Object.entries(stats).reduce((validStats, [indexName]) => { + const ilmPhase = getIlmPhase(ilmExplain?.[indexName], isILMAvailable); + const isSelectedPhase = + (isILMAvailable && ilmPhase != null && ilmPhasesMap[ilmPhase] != null) || + !isILMAvailable; + + if (isSelectedPhase) { + const incompatible = + results != null && results[indexName] != null + ? results[indexName].incompatible + : undefined; + const sizeInBytes = getSizeInBytes({ indexName, stats }); + const docsCount = getDocsCount({ stats, indexName }); + return [ + ...validStats, + { + ilmPhase, + incompatible, + indexName, + pattern, + sizeInBytes, + docsCount, + }, + ]; + } else { + return validStats; + } + }, []), ]; } @@ -232,7 +229,7 @@ export const getLayersMultiDimensional = ({ groupByRollup: (d: Datum) => d.indexName, nodeLabel: (indexName: Datum) => indexName, shape: { - fillColor: (indexName: Key, sortIndex: number, node: Pick) => { + fillColor: (indexName: Key, _sortIndex: number, node: Pick) => { const pattern = getGroupFromPath(node.path) ?? ''; const flattenedBucket = pathToFlattenedBucketMap[`${pattern}${indexName}`]; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/stat_label/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/stat_label/index.tsx index 31b4620fbb5f0..32402b49b570c 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/stat_label/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/stat_label/index.tsx @@ -19,12 +19,11 @@ const Line2 = styled.span` const EMPTY = ' '; interface Props { - color?: string; line1?: string; line2?: string; } -export const StatLabel: React.FC = ({ color, line1 = EMPTY, line2 = EMPTY }) => ( +export const StatLabel: React.FC = ({ line1 = EMPTY, line2 = EMPTY }) => ( <> {line1} {line2} diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/ecs_compliant_tab/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/ecs_compliant_tab/index.tsx index b53567e709eb4..855ef75e80b84 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/ecs_compliant_tab/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/ecs_compliant_tab/index.tsx @@ -26,15 +26,10 @@ const EmptyPromptContainer = styled.div` interface Props { indexName: string; - onAddToNewCase: (markdownComments: string[]) => void; partitionedFieldMetadata: PartitionedFieldMetadata; } -const EcsCompliantTabComponent: React.FC = ({ - indexName, - onAddToNewCase, - partitionedFieldMetadata, -}) => { +const EcsCompliantTabComponent: React.FC = ({ indexName, partitionedFieldMetadata }) => { const emptyPromptBody = useMemo(() => , []); const title = useMemo(() => , []); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/helpers.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/helpers.tsx index 670357c3730f7..8790ab12591b3 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/helpers.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/helpers.tsx @@ -212,11 +212,7 @@ export const getTabs = ({
), content: ( - + ), id: ECS_COMPLIANT_TAB_ID, name: i18n.ECS_COMPLIANT_FIELDS, diff --git a/x-pack/packages/security-solution/side_nav/src/solution_side_nav_panel.styles.ts b/x-pack/packages/security-solution/side_nav/src/solution_side_nav_panel.styles.ts index ca0f592f96a43..f5af4cd05ad24 100644 --- a/x-pack/packages/security-solution/side_nav/src/solution_side_nav_panel.styles.ts +++ b/x-pack/packages/security-solution/side_nav/src/solution_side_nav_panel.styles.ts @@ -69,7 +69,7 @@ export const SolutionSideNavCategoryTitleStyles = (euiTheme: EuiThemeComputed<{} font-weight: ${euiTheme.font.weight.medium}; `; -export const SolutionSideNavPanelLinksGroupStyles = (euiTheme: EuiThemeComputed<{}>) => css` +export const SolutionSideNavPanelLinksGroupStyles = () => css` padding-left: 0; padding-right: 0; `; diff --git a/x-pack/packages/security-solution/side_nav/src/solution_side_nav_panel.tsx b/x-pack/packages/security-solution/side_nav/src/solution_side_nav_panel.tsx index dfe2f643d4783..248121872018b 100644 --- a/x-pack/packages/security-solution/side_nav/src/solution_side_nav_panel.tsx +++ b/x-pack/packages/security-solution/side_nav/src/solution_side_nav_panel.tsx @@ -333,8 +333,7 @@ interface SolutionSideNavPanelItemsProps { */ const SolutionSideNavPanelItems: React.FC = React.memo( function SolutionSideNavPanelItems({ items, onClose }) { - const { euiTheme } = useEuiTheme(); - const panelLinksGroupClassNames = classNames(SolutionSideNavPanelLinksGroupStyles(euiTheme)); + const panelLinksGroupClassNames = classNames(SolutionSideNavPanelLinksGroupStyles()); return ( {items.map((item) => ( diff --git a/x-pack/packages/security/plugin_types_server/index.ts b/x-pack/packages/security/plugin_types_server/index.ts index 1228b9d36f961..21ab0eb2b39af 100644 --- a/x-pack/packages/security/plugin_types_server/index.ts +++ b/x-pack/packages/security/plugin_types_server/index.ts @@ -14,15 +14,6 @@ export type { AuditLogger, } from './src/audit'; export type { - CreateAPIKeyParams, - CreateAPIKeyResult, - CreateRestAPIKeyParams, - GrantAPIKeyResult, - InvalidateAPIKeysParams, - ValidateAPIKeyParams, - CreateRestAPIKeyWithKibanaPrivilegesParams, - CreateCrossClusterAPIKeyParams, - InvalidateAPIKeyResult, APIKeys, AuthenticationServiceStart, UpdateAPIKeyParams, @@ -39,7 +30,6 @@ export type { CheckPrivilegesWithRequest, CheckSavedObjectsPrivilegesWithRequest, CheckPrivilegesDynamicallyWithRequest, - KibanaPrivilegesType, SavedObjectActions, UIActions, CheckPrivilegesPayload, @@ -51,7 +41,6 @@ export type { CheckPrivilegesOptions, CheckUserProfilesPrivilegesPayload, CheckUserProfilesPrivilegesResponse, - ElasticsearchPrivilegesType, CasesActions, CheckPrivileges, AlertingActions, @@ -72,11 +61,30 @@ export type { } from './src/user_profile'; export { - restApiKeySchema, - getRestApiKeyWithKibanaPrivilegesSchema, getUpdateRestApiKeyWithKibanaPrivilegesSchema, - crossClusterApiKeySchema, updateRestApiKeySchema, updateCrossClusterApiKeySchema, } from './src/authentication'; -export { GLOBAL_RESOURCE, elasticsearchRoleSchema, getKibanaRoleSchema } from './src/authorization'; + +export type { + ElasticsearchPrivilegesType, + KibanaPrivilegesType, + APIKeysService, + CreateAPIKeyParams, + CreateAPIKeyResult, + InvalidateAPIKeyResult, + InvalidateAPIKeysParams, + ValidateAPIKeyParams, + CreateRestAPIKeyParams, + CreateRestAPIKeyWithKibanaPrivilegesParams, + CreateCrossClusterAPIKeyParams, + GrantAPIKeyResult, +} from '@kbn/core-security-server'; +export { isCreateRestAPIKeyParams } from '@kbn/core-security-server'; + +export { + restApiKeySchema, + crossClusterApiKeySchema, + getRestApiKeyWithKibanaPrivilegesSchema, +} from './src/authentication'; +export { getKibanaRoleSchema, elasticsearchRoleSchema, GLOBAL_RESOURCE } from './src/authorization'; diff --git a/x-pack/packages/security/plugin_types_server/src/authentication/api_keys/api_keys.ts b/x-pack/packages/security/plugin_types_server/src/authentication/api_keys/api_keys.ts index 2ced5478b46eb..c331802c7f693 100644 --- a/x-pack/packages/security/plugin_types_server/src/authentication/api_keys/api_keys.ts +++ b/x-pack/packages/security/plugin_types_server/src/authentication/api_keys/api_keys.ts @@ -5,153 +5,9 @@ * 2.0. */ -import type { estypes } from '@elastic/elasticsearch'; - -import type { KibanaRequest } from '@kbn/core/server'; -import { schema, TypeOf } from '@kbn/config-schema'; +import { schema } from '@kbn/config-schema'; import { getKibanaRoleSchema, elasticsearchRoleSchema } from '../../authorization'; -export interface APIKeys { - /** - * Determines if API Keys are enabled in Elasticsearch. - */ - areAPIKeysEnabled(): Promise; - - /** - * Determines if Cross-Cluster API Keys are enabled in Elasticsearch. - */ - areCrossClusterAPIKeysEnabled(): Promise; - - /** - * Tries to create an API key for the current user. - * - * Returns newly created API key or `null` if API keys are disabled. - * - * User needs `manage_api_key` privilege to create REST API keys and `manage_security` for Cross-Cluster API keys. - * - * @param request Request instance. - * @param createParams The params to create an API key - */ - create( - request: KibanaRequest, - createParams: CreateAPIKeyParams - ): Promise; - - /** - * Tries to grant an API key for the current user. - * @param request Request instance. - * @param createParams Create operation parameters. - */ - grantAsInternalUser( - request: KibanaRequest, - createParams: CreateRestAPIKeyParams | CreateRestAPIKeyWithKibanaPrivilegesParams - ): Promise; - - /** - * Tries to validate an API key. - * @param apiKeyPrams ValidateAPIKeyParams. - */ - validate(apiKeyPrams: ValidateAPIKeyParams): Promise; - - /** - * Tries to invalidate an API keys. - * @param request Request instance. - * @param params The params to invalidate an API keys. - */ - invalidate( - request: KibanaRequest, - params: InvalidateAPIKeysParams - ): Promise; - - /** - * Tries to invalidate the API keys by using the internal user. - * @param params The params to invalidate the API keys. - */ - invalidateAsInternalUser(params: InvalidateAPIKeysParams): Promise; -} - -export type CreateAPIKeyParams = - | CreateRestAPIKeyParams - | CreateRestAPIKeyWithKibanaPrivilegesParams - | CreateCrossClusterAPIKeyParams; - -/** - * Response of Kibana Create API key endpoint. - */ -export type CreateAPIKeyResult = estypes.SecurityCreateApiKeyResponse; - -export type CreateRestAPIKeyParams = TypeOf; -export type CreateRestAPIKeyWithKibanaPrivilegesParams = TypeOf< - ReturnType ->; -export type CreateCrossClusterAPIKeyParams = TypeOf; - -export interface GrantAPIKeyResult { - /** - * Unique id for this API key - */ - id: string; - /** - * Name for this API key - */ - name: string; - /** - * Generated API key - */ - api_key: string; -} - -/** - * Represents the parameters for validating API Key credentials. - */ -export interface ValidateAPIKeyParams { - /** - * Unique id for this API key - */ - id: string; - - /** - * Generated API Key (secret) - */ - api_key: string; -} - -/** - * Represents the params for invalidating multiple API keys - */ -export interface InvalidateAPIKeysParams { - ids: string[]; -} - -/** - * The return value when invalidating an API key in Elasticsearch. - */ -export interface InvalidateAPIKeyResult { - /** - * The IDs of the API keys that were invalidated as part of the request. - */ - invalidated_api_keys: string[]; - /** - * The IDs of the API keys that were already invalidated. - */ - previously_invalidated_api_keys: string[]; - /** - * The number of errors that were encountered when invalidating the API keys. - */ - error_count: number; - /** - * Details about these errors. This field is not present in the response when error_count is 0. - */ - error_details?: Array<{ - type?: string; - reason?: string; - caused_by?: { - type?: string; - reason?: string; - }; - }>; -} - export const restApiKeySchema = schema.object({ type: schema.maybe(schema.literal('rest')), name: schema.string(), @@ -165,8 +21,11 @@ export const restApiKeySchema = schema.object({ export const getRestApiKeyWithKibanaPrivilegesSchema = ( getBasePrivilegeNames: Parameters[0] ) => - restApiKeySchema.extends({ - role_descriptors: null, + schema.object({ + type: schema.maybe(schema.literal('rest')), + name: schema.string(), + expiration: schema.maybe(schema.string()), + metadata: schema.maybe(schema.object({}, { unknowns: 'allow' })), kibana_role_descriptors: schema.recordOf( schema.string(), schema.object({ @@ -176,9 +35,11 @@ export const getRestApiKeyWithKibanaPrivilegesSchema = ( ), }); -export const crossClusterApiKeySchema = restApiKeySchema.extends({ +export const crossClusterApiKeySchema = schema.object({ type: schema.literal('cross_cluster'), - role_descriptors: null, + name: schema.string(), + expiration: schema.maybe(schema.string()), + metadata: schema.maybe(schema.object({}, { unknowns: 'allow' })), access: schema.object( { search: schema.maybe( @@ -203,41 +64,52 @@ export const crossClusterApiKeySchema = restApiKeySchema.extends({ ), }); -/** - * Response of Kibana Update API key endpoint. - */ -export type UpdateAPIKeyResult = estypes.SecurityUpdateApiKeyResponse; - -/** - * Request body of Kibana Update API key endpoint. - */ -export type UpdateAPIKeyParams = - | UpdateRestAPIKeyParams - | UpdateCrossClusterAPIKeyParams - | UpdateRestAPIKeyWithKibanaPrivilegesParams; - -export const updateRestApiKeySchema = restApiKeySchema.extends({ - name: null, +export const updateRestApiKeySchema = schema.object({ id: schema.string(), + type: schema.maybe(schema.literal('rest')), + expiration: schema.maybe(schema.string()), + role_descriptors: schema.recordOf(schema.string(), schema.object({}, { unknowns: 'allow' }), { + defaultValue: {}, + }), + metadata: schema.maybe(schema.object({}, { unknowns: 'allow' })), }); -export const updateCrossClusterApiKeySchema = crossClusterApiKeySchema.extends({ - name: null, +export const updateCrossClusterApiKeySchema = schema.object({ id: schema.string(), + type: schema.literal('cross_cluster'), + expiration: schema.maybe(schema.string()), + metadata: schema.maybe(schema.object({}, { unknowns: 'allow' })), + access: schema.object( + { + search: schema.maybe( + schema.arrayOf( + schema.object({ + names: schema.arrayOf(schema.string()), + query: schema.maybe(schema.any()), + field_security: schema.maybe(schema.any()), + allow_restricted_indices: schema.maybe(schema.boolean()), + }) + ) + ), + replication: schema.maybe( + schema.arrayOf( + schema.object({ + names: schema.arrayOf(schema.string()), + }) + ) + ), + }, + { unknowns: 'allow' } + ), }); -export type UpdateRestAPIKeyParams = TypeOf; -export type UpdateCrossClusterAPIKeyParams = TypeOf; -export type UpdateRestAPIKeyWithKibanaPrivilegesParams = TypeOf< - ReturnType ->; - export const getUpdateRestApiKeyWithKibanaPrivilegesSchema = ( getBasePrivilegeNames: Parameters[0] ) => - restApiKeySchema.extends({ - role_descriptors: null, - name: null, + schema.object({ + type: schema.maybe(schema.literal('rest')), + expiration: schema.maybe(schema.string()), + metadata: schema.maybe(schema.object({}, { unknowns: 'allow' })), id: schema.string(), kibana_role_descriptors: schema.recordOf( schema.string(), diff --git a/x-pack/packages/security/plugin_types_server/src/authentication/api_keys/index.ts b/x-pack/packages/security/plugin_types_server/src/authentication/api_keys/index.ts index ec36a99b4da63..1673682052554 100644 --- a/x-pack/packages/security/plugin_types_server/src/authentication/api_keys/index.ts +++ b/x-pack/packages/security/plugin_types_server/src/authentication/api_keys/index.ts @@ -5,23 +5,6 @@ * 2.0. */ -export type { - CreateAPIKeyParams, - CreateAPIKeyResult, - InvalidateAPIKeyResult, - InvalidateAPIKeysParams, - ValidateAPIKeyParams, - CreateRestAPIKeyParams, - CreateRestAPIKeyWithKibanaPrivilegesParams, - CreateCrossClusterAPIKeyParams, - GrantAPIKeyResult, - APIKeys, - UpdateAPIKeyParams, - UpdateAPIKeyResult, - UpdateCrossClusterAPIKeyParams, - UpdateRestAPIKeyParams, - UpdateRestAPIKeyWithKibanaPrivilegesParams, -} from './api_keys'; export { crossClusterApiKeySchema, getRestApiKeyWithKibanaPrivilegesSchema, diff --git a/x-pack/packages/security/plugin_types_server/src/authentication/authentication_service.ts b/x-pack/packages/security/plugin_types_server/src/authentication/authentication_service.ts index 6bc5a73113ae3..5d066bb6565ca 100644 --- a/x-pack/packages/security/plugin_types_server/src/authentication/authentication_service.ts +++ b/x-pack/packages/security/plugin_types_server/src/authentication/authentication_service.ts @@ -6,14 +6,13 @@ */ import type { KibanaRequest } from '@kbn/core/server'; -import type { AuthenticatedUser } from '@kbn/core-security-common'; - -import type { APIKeys } from './api_keys'; +import type { AuthenticatedUser } from '@kbn/security-plugin-types-common'; +import type { APIKeysService } from '@kbn/core-security-server'; /** * Authentication services available on the security plugin's start contract. */ export interface AuthenticationServiceStart { - apiKeys: APIKeys; + apiKeys: APIKeysService; getCurrentUser: (request: KibanaRequest) => AuthenticatedUser | null; } diff --git a/x-pack/packages/security/plugin_types_server/src/authentication/index.ts b/x-pack/packages/security/plugin_types_server/src/authentication/index.ts index 6e30f9ebcec24..4a5e7da782baf 100644 --- a/x-pack/packages/security/plugin_types_server/src/authentication/index.ts +++ b/x-pack/packages/security/plugin_types_server/src/authentication/index.ts @@ -5,29 +5,22 @@ * 2.0. */ +export type { AuthenticationServiceStart } from './authentication_service'; + export type { - CreateAPIKeyParams, - CreateAPIKeyResult, - CreateRestAPIKeyParams, - CreateRestAPIKeyWithKibanaPrivilegesParams, - CreateCrossClusterAPIKeyParams, - InvalidateAPIKeyResult, - InvalidateAPIKeysParams, - ValidateAPIKeyParams, - APIKeys, - GrantAPIKeyResult, + APIKeysService as APIKeys, UpdateAPIKeyParams, UpdateAPIKeyResult, UpdateCrossClusterAPIKeyParams, UpdateRestAPIKeyParams, UpdateRestAPIKeyWithKibanaPrivilegesParams, -} from './api_keys'; -export type { AuthenticationServiceStart } from './authentication_service'; +} from '@kbn/core-security-server'; + export { - restApiKeySchema, + crossClusterApiKeySchema, getRestApiKeyWithKibanaPrivilegesSchema, getUpdateRestApiKeyWithKibanaPrivilegesSchema, - crossClusterApiKeySchema, + restApiKeySchema, updateRestApiKeySchema, updateCrossClusterApiKeySchema, } from './api_keys'; diff --git a/x-pack/packages/security/plugin_types_server/src/authorization/index.ts b/x-pack/packages/security/plugin_types_server/src/authorization/index.ts index 54364d7817f31..baeeeddc1fa74 100644 --- a/x-pack/packages/security/plugin_types_server/src/authorization/index.ts +++ b/x-pack/packages/security/plugin_types_server/src/authorization/index.ts @@ -42,7 +42,6 @@ export type { PrivilegeDeprecationsRolesByFeatureIdResponse, } from './deprecations'; export type { AuthorizationMode } from './mode'; -export type { ElasticsearchPrivilegesType, KibanaPrivilegesType } from './role_schema'; export { GLOBAL_RESOURCE } from './constants'; export { elasticsearchRoleSchema, getKibanaRoleSchema } from './role_schema'; diff --git a/x-pack/packages/security/plugin_types_server/tsconfig.json b/x-pack/packages/security/plugin_types_server/tsconfig.json index 04ed00229a3de..2f4ae387ac2b5 100644 --- a/x-pack/packages/security/plugin_types_server/tsconfig.json +++ b/x-pack/packages/security/plugin_types_server/tsconfig.json @@ -10,11 +10,10 @@ "target/**/*" ], "kbn_references": [ - "@kbn/config-schema", "@kbn/core", "@kbn/security-plugin-types-common", "@kbn/core-user-profile-server", "@kbn/core-security-server", - "@kbn/core-security-common" + "@kbn/config-schema", ] } diff --git a/x-pack/plugins/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap b/x-pack/plugins/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap index e1366f7f9c573..05d74f781c434 100644 --- a/x-pack/plugins/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap +++ b/x-pack/plugins/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap @@ -7561,6 +7561,44 @@ Object { ], "type": "alternatives", }, + "scriptType": Object { + "flags": Object { + "default": null, + "error": [Function], + "presence": "optional", + }, + "matches": Array [ + Object { + "schema": Object { + "flags": Object { + "error": [Function], + }, + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + }, + Object { + "schema": Object { + "allow": Array [ + null, + ], + "flags": Object { + "error": [Function], + "only": true, + }, + "type": "any", + }, + }, + ], + "type": "alternatives", + }, }, "type": "object", } diff --git a/x-pack/plugins/aiops/public/plugin.tsx b/x-pack/plugins/aiops/public/plugin.tsx index b1d62c4275180..7d55c3098cfaf 100755 --- a/x-pack/plugins/aiops/public/plugin.tsx +++ b/x-pack/plugins/aiops/public/plugin.tsx @@ -41,7 +41,9 @@ export class AiopsPlugin { registerChangePointChartsAttachment }, [coreStart, pluginStart], ]) => { - if (license.hasAtLeast('platinum')) { + const { canUseAiops } = coreStart.application.capabilities.ml; + + if (license.hasAtLeast('platinum') && canUseAiops) { if (embeddable) { registerEmbeddables(embeddable, core); } diff --git a/x-pack/plugins/data_quality/common/index.ts b/x-pack/plugins/data_quality/common/index.ts index 79bbe59ff35d2..1b174a9efe524 100644 --- a/x-pack/plugins/data_quality/common/index.ts +++ b/x-pack/plugins/data_quality/common/index.ts @@ -13,6 +13,3 @@ export const PLUGIN_NAME = i18n.translate('xpack.dataQuality.name', { }); export { DATA_QUALITY_URL_STATE_KEY, datasetQualityUrlSchemaV1 } from './url_schema'; - -export { DATA_QUALITY_LOCATOR_ID } from './locators'; -export type { DataQualityLocatorParams } from './locators'; diff --git a/x-pack/plugins/data_quality/common/locators/construct_dataset_quality_locator_path.ts b/x-pack/plugins/data_quality/common/locators/construct_dataset_quality_locator_path.ts index 45f58752bd2fc..f5b5b0bf7ce59 100644 --- a/x-pack/plugins/data_quality/common/locators/construct_dataset_quality_locator_path.ts +++ b/x-pack/plugins/data_quality/common/locators/construct_dataset_quality_locator_path.ts @@ -8,9 +8,9 @@ import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/common'; import { ManagementAppLocatorParams } from '@kbn/management-plugin/common/locator'; import { LocatorPublic } from '@kbn/share-plugin/common'; +import { DataQualityLocatorParams } from '@kbn/deeplinks-observability'; import { datasetQualityUrlSchemaV1, DATA_QUALITY_URL_STATE_KEY } from '../url_schema'; import { deepCompactObject } from '../utils/deep_compact_object'; -import { DataQualityLocatorParams } from './types'; interface LocatorPathConstructionParams { locatorParams: DataQualityLocatorParams; @@ -20,7 +20,7 @@ interface LocatorPathConstructionParams { export const constructDatasetQualityLocatorPath = async (params: LocatorPathConstructionParams) => { const { - locatorParams: { filters }, + locatorParams: { filters, flyout }, useHash, managementLocator, } = params; @@ -29,6 +29,7 @@ export const constructDatasetQualityLocatorPath = async (params: LocatorPathCons deepCompactObject({ v: 1, filters, + flyout, }) ); diff --git a/x-pack/plugins/data_quality/common/locators/dataset_quality_locator.ts b/x-pack/plugins/data_quality/common/locators/dataset_quality_locator.ts index 70e4770090ef3..4bf804955b6bc 100644 --- a/x-pack/plugins/data_quality/common/locators/dataset_quality_locator.ts +++ b/x-pack/plugins/data_quality/common/locators/dataset_quality_locator.ts @@ -6,11 +6,8 @@ */ import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public'; -import { - DataQualityLocatorDependencies, - DataQualityLocatorParams, - DATA_QUALITY_LOCATOR_ID, -} from './types'; +import { DataQualityLocatorParams, DATA_QUALITY_LOCATOR_ID } from '@kbn/deeplinks-observability'; +import { DataQualityLocatorDependencies } from './types'; import { constructDatasetQualityLocatorPath } from './construct_dataset_quality_locator_path'; export type DatasetQualityLocator = LocatorPublic; diff --git a/x-pack/plugins/data_quality/common/locators/types.ts b/x-pack/plugins/data_quality/common/locators/types.ts index 57067cd0e482a..786b5e1cf567f 100644 --- a/x-pack/plugins/data_quality/common/locators/types.ts +++ b/x-pack/plugins/data_quality/common/locators/types.ts @@ -7,31 +7,6 @@ import { ManagementAppLocatorParams } from '@kbn/management-plugin/common/locator'; import { LocatorPublic } from '@kbn/share-plugin/common'; -import { SerializableRecord } from '@kbn/utility-types'; - -export const DATA_QUALITY_LOCATOR_ID = 'DATA_QUALITY_LOCATOR'; - -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -type RefreshInterval = { - pause: boolean; - value: number; -}; - -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -type TimeRangeConfig = { - from: string; - to: string; - refresh: RefreshInterval; -}; - -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -type Filters = { - timeRange: TimeRangeConfig; -}; - -export interface DataQualityLocatorParams extends SerializableRecord { - filters?: Filters; -} export interface DataQualityLocatorDependencies { useHash: boolean; diff --git a/x-pack/plugins/data_quality/common/url_schema/url_schema_v1.ts b/x-pack/plugins/data_quality/common/url_schema/url_schema_v1.ts index 6fd5781a217e8..076e1b641b7e2 100644 --- a/x-pack/plugins/data_quality/common/url_schema/url_schema_v1.ts +++ b/x-pack/plugins/data_quality/common/url_schema/url_schema_v1.ts @@ -37,11 +37,11 @@ const datasetRT = rt.intersection([ type: rt.string, name: rt.string, namespace: rt.string, - title: rt.string, }), rt.exact( rt.partial({ integration: integrationRT, + title: rt.string, }) ), ]); diff --git a/x-pack/plugins/data_quality/tsconfig.json b/x-pack/plugins/data_quality/tsconfig.json index 7a904e9f95cda..911c4fbfff557 100644 --- a/x-pack/plugins/data_quality/tsconfig.json +++ b/x-pack/plugins/data_quality/tsconfig.json @@ -25,8 +25,8 @@ "@kbn/core-chrome-browser", "@kbn/features-plugin", "@kbn/share-plugin", - "@kbn/utility-types", "@kbn/deeplinks-management", + "@kbn/deeplinks-observability", "@kbn/ebt-tools", ], "exclude": ["target/**/*"] diff --git a/x-pack/plugins/data_visualizer/kibana.jsonc b/x-pack/plugins/data_visualizer/kibana.jsonc index 1ad88eaea4cb4..84fc98d3fb22f 100644 --- a/x-pack/plugins/data_visualizer/kibana.jsonc +++ b/x-pack/plugins/data_visualizer/kibana.jsonc @@ -37,7 +37,7 @@ "fieldFormats", "uiActions", "lens", - "textBasedLanguages", + "esql", "visualizations" ] } diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx index 4ffe604c3d352..fd65ed3c7dfa6 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx @@ -12,7 +12,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { usePageUrlState } from '@kbn/ml-url-state'; import { FullTimeRangeSelector, DatePickerWrapper } from '@kbn/ml-date-picker'; -import { TextBasedLangEditor } from '@kbn/text-based-languages/public'; +import { TextBasedLangEditor } from '@kbn/esql/public'; import type { AggregateQuery } from '@kbn/es-query'; import { diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/field_stats/field_stats_esql_editor.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/field_stats/field_stats_esql_editor.tsx index bdaee8c1a5ae1..a015d975fdf18 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/field_stats/field_stats_esql_editor.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/field_stats/field_stats_esql_editor.tsx @@ -5,7 +5,7 @@ * 2.0. */ import React, { useRef, useState, useCallback } from 'react'; -import { TextBasedLangEditor } from '@kbn/text-based-languages/public'; +import { TextBasedLangEditor } from '@kbn/esql/public'; import { EuiFlexItem } from '@elastic/eui'; import type { AggregateQuery } from '@kbn/es-query'; diff --git a/x-pack/plugins/data_visualizer/tsconfig.json b/x-pack/plugins/data_visualizer/tsconfig.json index 9616783094354..ed8a3540f6d8a 100644 --- a/x-pack/plugins/data_visualizer/tsconfig.json +++ b/x-pack/plugins/data_visualizer/tsconfig.json @@ -68,7 +68,7 @@ "@kbn/security-plugin", "@kbn/share-plugin", "@kbn/test-jest-helpers", - "@kbn/text-based-languages", + "@kbn/esql", "@kbn/ui-actions-plugin", "@kbn/ui-theme", "@kbn/unified-search-plugin", diff --git a/x-pack/plugins/enterprise_search/common/constants.ts b/x-pack/plugins/enterprise_search/common/constants.ts index 47c4741e41afc..f2be720d1c04c 100644 --- a/x-pack/plugins/enterprise_search/common/constants.ts +++ b/x-pack/plugins/enterprise_search/common/constants.ts @@ -8,7 +8,7 @@ import { ENTERPRISE_SEARCH_APP_ID, ENTERPRISE_SEARCH_CONTENT_APP_ID, - ENTERPRISE_SEARCH_INFERENCE_ENDPOINTS_APP_ID, + ENTERPRISE_SEARCH_RELEVANCE_APP_ID, ENTERPRISE_SEARCH_APPLICATIONS_APP_ID, ENTERPRISE_SEARCH_ANALYTICS_APP_ID, ENTERPRISE_SEARCH_APPSEARCH_APP_ID, @@ -178,7 +178,7 @@ export const VECTOR_SEARCH_PLUGIN = { }; export const INFERENCE_ENDPOINTS_PLUGIN = { - ID: ENTERPRISE_SEARCH_INFERENCE_ENDPOINTS_APP_ID, + ID: ENTERPRISE_SEARCH_RELEVANCE_APP_ID, NAME: i18n.translate('xpack.enterpriseSearch.inferenceEndpoints.productName', { defaultMessage: 'Inference Endpoints', }), diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/licensing_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/licensing_logic.mock.ts index 0ea858d0f2f50..4f91ddf4cb5c6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/licensing_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/licensing_logic.mock.ts @@ -10,6 +10,7 @@ import { licensingMock } from '@kbn/licensing-plugin/public/mocks'; export const mockLicensingValues = { license: licensingMock.createLicense(), hasPlatinumLicense: false, + hasEnterpriseLicense: true, hasGoldLicense: false, isTrial: false, canManageLicense: true, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx index 77454581c61e7..e39f0f0b71f29 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx @@ -40,6 +40,8 @@ import { import { INFERENCE_ENDPOINTS_PATH } from '../../enterprise_search_relevance/routes'; import { KibanaLogic } from '../kibana'; +import { LicensingLogic } from '../licensing'; + import { generateNavLink } from './nav_link_helpers'; /** @@ -51,7 +53,11 @@ import { generateNavLink } from './nav_link_helpers'; export const useEnterpriseSearchNav = (alwaysReturn = false) => { const { isSearchHomepageEnabled, searchHomepage, isSidebarEnabled, productAccess } = useValues(KibanaLogic); + + const { hasEnterpriseLicense } = useValues(LicensingLogic); + const indicesNavItems = useIndicesNav(); + if (!isSidebarEnabled && !alwaysReturn) return undefined; const navItems: Array> = [ @@ -154,25 +160,29 @@ export const useEnterpriseSearchNav = (alwaysReturn = false) => { defaultMessage: 'Build', }), }, - { - id: 'relevance', - items: [ - { - id: 'inference_endpoints', - name: i18n.translate('xpack.enterpriseSearch.nav.inferenceEndpointsTitle', { - defaultMessage: 'Inference Endpoints', - }), - ...generateNavLink({ - shouldNotCreateHref: true, - shouldShowActiveForSubroutes: true, - to: INFERENCE_ENDPOINTS_PLUGIN.URL + INFERENCE_ENDPOINTS_PATH, - }), - }, - ], - name: i18n.translate('xpack.enterpriseSearch.nav.relevanceTitle', { - defaultMessage: 'Relevance', - }), - }, + ...(hasEnterpriseLicense + ? [ + { + id: 'relevance', + items: [ + { + id: 'inference_endpoints', + name: i18n.translate('xpack.enterpriseSearch.nav.inferenceEndpointsTitle', { + defaultMessage: 'Inference Endpoints', + }), + ...generateNavLink({ + shouldNotCreateHref: true, + shouldShowActiveForSubroutes: true, + to: INFERENCE_ENDPOINTS_PLUGIN.URL + INFERENCE_ENDPOINTS_PATH, + }), + }, + ], + name: i18n.translate('xpack.enterpriseSearch.nav.relevanceTitle', { + defaultMessage: 'Relevance', + }), + }, + ] + : []), { id: 'es_getting_started', items: [ diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts index 85a26abeef1e1..743483e96fa31 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts @@ -167,6 +167,38 @@ describe('LicensingLogic', () => { }); }); + describe('hasEnterpriseLicense', () => { + it('is true for enterprise and trial licenses', () => { + updateLicense({ status: 'active', type: 'enterprise' }); + expect(LicensingLogic.values.hasEnterpriseLicense).toEqual(true); + + updateLicense({ status: 'active', type: 'trial' }); + expect(LicensingLogic.values.hasEnterpriseLicense).toEqual(true); + }); + + it('is false if the current license is expired', () => { + updateLicense({ status: 'expired', type: 'enterprise' }); + expect(LicensingLogic.values.hasEnterpriseLicense).toEqual(false); + + updateLicense({ status: 'expired', type: 'trial' }); + expect(LicensingLogic.values.hasEnterpriseLicense).toEqual(false); + }); + + it('is false for licenses below enterprise', () => { + updateLicense({ status: 'active', type: 'gold' }); + expect(LicensingLogic.values.hasEnterpriseLicense).toEqual(false); + + updateLicense({ status: 'active', type: 'platinum' }); + expect(LicensingLogic.values.hasEnterpriseLicense).toEqual(false); + + updateLicense({ status: 'active', type: 'basic' }); + expect(LicensingLogic.values.hasEnterpriseLicense).toEqual(false); + + updateLicense({ status: 'active', type: 'standard' }); + expect(LicensingLogic.values.hasEnterpriseLicense).toEqual(false); + }); + }); + describe('isTrial', () => { it('is true for active trial license', () => { updateLicense({ status: 'active', type: 'trial' }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts index 77a09de2c863f..ab3586f6563c1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts @@ -13,6 +13,7 @@ import { ILicense } from '@kbn/licensing-plugin/public'; interface LicensingValues { license: ILicense | null; licenseSubscription: Subscription | null; + hasEnterpriseLicense: boolean; hasPlatinumLicense: boolean; hasGoldLicense: boolean; isTrial: boolean; @@ -52,6 +53,13 @@ export const LicensingLogic = kea [selectors.license], + (license) => { + const qualifyingLicenses = ['enterprise', 'trial']; + return license?.isActive && qualifyingLicenses.includes(license?.type); + }, + ], hasGoldLicense: [ (selectors) => [selectors.license], (license) => { diff --git a/x-pack/plugins/enterprise_search/public/navigation_tree.ts b/x-pack/plugins/enterprise_search/public/navigation_tree.ts index 8bb6bf70e603b..d5c640fa67b3e 100644 --- a/x-pack/plugins/enterprise_search/public/navigation_tree.ts +++ b/x-pack/plugins/enterprise_search/public/navigation_tree.ts @@ -211,7 +211,7 @@ export const getNavigationTreeDefinition = ({ }), }, { - children: [{ link: 'searchInferenceEndpoints' }], + children: [{ link: 'enterpriseSearchRelevance:inferenceEndpoints' }], id: 'relevance', title: i18n.translate('xpack.enterpriseSearch.searchNav.relevance', { defaultMessage: 'Relevance', diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index 280de2f04356b..552bb43fbd073 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { BehaviorSubject, firstValueFrom } from 'rxjs'; +import { BehaviorSubject, firstValueFrom, Subscription } from 'rxjs'; import { ChartsPluginStart } from '@kbn/charts-plugin/public'; import { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public'; @@ -27,6 +27,7 @@ import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; import { i18n } from '@kbn/i18n'; import type { IndexManagementPluginStart } from '@kbn/index-management'; import { LensPublicStart } from '@kbn/lens-plugin/public'; +import { ILicense } from '@kbn/licensing-plugin/public'; import { LicensingPluginStart } from '@kbn/licensing-plugin/public'; import { MlPluginStart } from '@kbn/ml-plugin/public'; import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'; @@ -84,6 +85,7 @@ export type EnterpriseSearchPublicStart = ReturnType(); @@ -261,7 +264,8 @@ export class EnterpriseSearchPlugin implements Plugin { if (!config.ui?.enabled) { return; } - const { cloud, share } = plugins; + const { cloud, share, licensing } = plugins; + const useSearchHomepage = plugins.searchHomepage && plugins.searchHomepage.isHomepageFeatureEnabled(); @@ -445,29 +449,33 @@ export class EnterpriseSearchPlugin implements Plugin { title: ANALYTICS_PLUGIN.NAME, }); - core.application.register({ - appRoute: INFERENCE_ENDPOINTS_PLUGIN.URL, - category: DEFAULT_APP_CATEGORIES.enterpriseSearch, - deepLinks: relevanceLinks, - euiIconType: INFERENCE_ENDPOINTS_PLUGIN.LOGO, - id: INFERENCE_ENDPOINTS_PLUGIN.ID, - mount: async (params: AppMountParameters) => { - const kibanaDeps = await this.getKibanaDeps(core, params, cloud); - const { chrome, http } = kibanaDeps.core; - chrome.docTitle.change(INFERENCE_ENDPOINTS_PLUGIN.NAME); - - await this.getInitialData(http); - const pluginData = this.getPluginData(); - - const { renderApp } = await import('./applications'); - const { EnterpriseSearchRelevance } = await import( - './applications/enterprise_search_relevance' - ); - - return renderApp(EnterpriseSearchRelevance, kibanaDeps, pluginData); - }, - title: INFERENCE_ENDPOINTS_PLUGIN.NAME, - visibleIn: [], + this.licenseSubscription = licensing?.license$.subscribe((license: ILicense) => { + if (license.isActive && license.hasAtLeast('enterprise')) { + core.application.register({ + appRoute: INFERENCE_ENDPOINTS_PLUGIN.URL, + category: DEFAULT_APP_CATEGORIES.enterpriseSearch, + deepLinks: relevanceLinks, + euiIconType: INFERENCE_ENDPOINTS_PLUGIN.LOGO, + id: INFERENCE_ENDPOINTS_PLUGIN.ID, + mount: async (params: AppMountParameters) => { + const kibanaDeps = await this.getKibanaDeps(core, params, cloud); + const { chrome, http } = kibanaDeps.core; + chrome.docTitle.change(INFERENCE_ENDPOINTS_PLUGIN.NAME); + + await this.getInitialData(http); + const pluginData = this.getPluginData(); + + const { renderApp } = await import('./applications'); + const { EnterpriseSearchRelevance } = await import( + './applications/enterprise_search_relevance' + ); + + return renderApp(EnterpriseSearchRelevance, kibanaDeps, pluginData); + }, + title: INFERENCE_ENDPOINTS_PLUGIN.NAME, + visibleIn: [], + }); + } }); core.application.register({ @@ -645,7 +653,9 @@ export class EnterpriseSearchPlugin implements Plugin { return {}; } - public stop() {} + public stop() { + this.licenseSubscription?.unsubscribe(); + } private updateSideNavDefinition = (items: Partial) => { this.sideNavDynamicItems$.next({ ...this.sideNavDynamicItems$.getValue(), ...items }); diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index 24298c55fdffa..ee403e223305f 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -21,6 +21,7 @@ import { DataPluginStart } from '@kbn/data-plugin/server/plugin'; import { PluginSetupContract as FeaturesPluginSetup } from '@kbn/features-plugin/server'; import { GlobalSearchPluginSetup } from '@kbn/global-search-plugin/server'; import type { GuidedOnboardingPluginSetup } from '@kbn/guided-onboarding-plugin/server'; +import type { LicensingPluginStart } from '@kbn/licensing-plugin/public'; import { LogsSharedPluginSetup } from '@kbn/logs-shared-plugin/server'; import type { MlPluginSetup } from '@kbn/ml-plugin/server'; import { SearchConnectorsPluginSetup } from '@kbn/search-connectors-plugin/server'; @@ -95,6 +96,7 @@ interface PluginsSetup { guidedOnboarding?: GuidedOnboardingPluginSetup; logsShared: LogsSharedPluginSetup; ml?: MlPluginSetup; + licensing: LicensingPluginStart; searchConnectors?: SearchConnectorsPluginSetup; security: SecurityPluginSetup; usageCollection?: UsageCollectionSetup; @@ -148,6 +150,7 @@ export class EnterpriseSearchPlugin implements Plugin { logsShared, customIntegrations, ml, + licensing, guidedOnboarding, cloud, searchConnectors, @@ -262,6 +265,7 @@ export class EnterpriseSearchPlugin implements Plugin { log, enterpriseSearchRequestHandler, ml, + licensing, }; registerConfigDataRoute(dependencies); diff --git a/x-pack/plugins/fleet/common/experimental_features.ts b/x-pack/plugins/fleet/common/experimental_features.ts index 1e6af81b8c445..502d3b603f159 100644 --- a/x-pack/plugins/fleet/common/experimental_features.ts +++ b/x-pack/plugins/fleet/common/experimental_features.ts @@ -27,7 +27,7 @@ const _allowedExperimentalValues = { enablePackagesStateMachine: true, advancedPolicySettings: true, useSpaceAwareness: false, - enableReusableIntegrationPolicies: false, + enableReusableIntegrationPolicies: true, }; /** diff --git a/x-pack/plugins/fleet/cypress/e2e/package_policy_pipelines_and_mappings_real.cy.ts b/x-pack/plugins/fleet/cypress/e2e/package_policy_pipelines_and_mappings_real.cy.ts index 0478c450ae5a2..269c848f998bf 100644 --- a/x-pack/plugins/fleet/cypress/e2e/package_policy_pipelines_and_mappings_real.cy.ts +++ b/x-pack/plugins/fleet/cypress/e2e/package_policy_pipelines_and_mappings_real.cy.ts @@ -82,8 +82,13 @@ describe('Input package create and edit package policy', () => { cy.getBySel(EXISTING_HOSTS_TAB).click(); - cy.getBySel(POLICY_EDITOR.AGENT_POLICY_SELECT).click().get(`#${agentPolicyId}`).click(); - cy.wait(500); // wait for policy id to be set + cy.getBySel(POLICY_EDITOR.AGENT_POLICY_SELECT).click(); + cy.getBySel('agentPolicyMultiItem').each(($el) => { + if ($el.text() === agentPolicyName) { + $el.trigger('click'); + } + }); + cy.wait(1000); // wait for policy id to be set cy.getBySel(CREATE_PACKAGE_POLICY_SAVE_BTN).click(); cy.getBySel(CONFIRM_MODAL.CANCEL_BUTTON).click(); @@ -150,8 +155,13 @@ describe('Integration package with custom dataset create and edit package policy cy.getBySel(EXISTING_HOSTS_TAB).click(); - cy.getBySel(POLICY_EDITOR.AGENT_POLICY_SELECT).click().get(`#${agentPolicyId}`).click(); - cy.wait(500); // wait for policy id to be set + cy.getBySel(POLICY_EDITOR.AGENT_POLICY_SELECT).click(); + cy.getBySel('agentPolicyMultiItem').each(($el) => { + if ($el.text() === agentPolicyName) { + $el.trigger('click'); + } + }); + cy.wait(1000); // wait for policy id to be set cy.getBySel(CREATE_PACKAGE_POLICY_SAVE_BTN).click(); cy.getBySel(CONFIRM_MODAL.CANCEL_BUTTON).click(); @@ -210,8 +220,13 @@ describe('Integration package with fixed dataset create and edit package policy' cy.getBySel(EXISTING_HOSTS_TAB).click(); - cy.getBySel(POLICY_EDITOR.AGENT_POLICY_SELECT).click().get(`#${agentPolicyId}`).click(); - cy.wait(500); // wait for policy id to be set + cy.getBySel(POLICY_EDITOR.AGENT_POLICY_SELECT).click(); + cy.getBySel('agentPolicyMultiItem').each(($el) => { + if ($el.text() === agentPolicyName) { + $el.trigger('click'); + } + }); + cy.wait(1000); // wait for policy id to be set cy.getBySel(CREATE_PACKAGE_POLICY_SAVE_BTN).click(); cy.getBySel(CONFIRM_MODAL.CANCEL_BUTTON).click(); diff --git a/x-pack/plugins/fleet/cypress/screens/integrations.ts b/x-pack/plugins/fleet/cypress/screens/integrations.ts index 7cc6aefdf89ec..1a31d2dc5de31 100644 --- a/x-pack/plugins/fleet/cypress/screens/integrations.ts +++ b/x-pack/plugins/fleet/cypress/screens/integrations.ts @@ -38,7 +38,7 @@ export const SETTINGS = { export const POLICY_EDITOR = { POLICY_NAME_INPUT: 'packagePolicyNameInput', DATASET_SELECT: 'datasetComboBox', - AGENT_POLICY_SELECT: 'agentPolicySelect', + AGENT_POLICY_SELECT: 'agentPolicyMultiSelect', INSPECT_PIPELINES_BTN: 'datastreamInspectPipelineBtn', EDIT_MAPPINGS_BTN: 'datastreamEditMappingsBtn', CREATE_MAPPINGS_BTN: 'datastreamAddCustomComponentTemplateBtn', diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_select_agent_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_select_agent_policy.tsx index 6238a2cc62a07..349105eafee4c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_select_agent_policy.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_select_agent_policy.tsx @@ -101,6 +101,9 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{ updateAgentPolicies([]); } }; + if (isLoading || selectedPolicyIds.length === 0) { + return; + } const agentPoliciesHaveAllSelectedIds = selectedPolicyIds.every((id) => agentPolicies.map((policy) => policy.id).includes(id) ); @@ -110,7 +113,7 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{ setSelectedAgentPolicyError(undefined); updateAgentPolicies(agentPolicies.filter((policy) => selectedPolicyIds.includes(policy.id))); } - }, [selectedPolicyIds, agentPolicies, updateAgentPolicies]); + }, [selectedPolicyIds, agentPolicies, updateAgentPolicies, isLoading]); // Try to select default agent policy useEffect(() => { @@ -192,7 +195,7 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{

@@ -215,7 +218,7 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx index 0054fc71133a2..4109a66a59638 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx @@ -378,7 +378,6 @@ describe('When on the package policy create page', () => { expect(sendCreatePackagePolicy as jest.MockedFunction).toHaveBeenCalledWith({ ...newPackagePolicy, - policy_id: 'agent-policy-1', policy_ids: ['agent-policy-1'], force: false, }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx index e9bbde2520837..2cecdf37f30f5 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx @@ -113,7 +113,7 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ sortable: true, truncateText: true, name: i18n.translate('xpack.fleet.policyDetails.packagePoliciesTable.nameColumnTitle', { - defaultMessage: 'Name', + defaultMessage: 'Integration policy', }), render: (value: string, packagePolicy: InMemoryPackagePolicy) => ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index acaf623afa330..98096d02138f9 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -144,6 +144,13 @@ export const EditPackagePolicyForm = memo<{ const [isFirstLoad, setIsFirstLoad] = useState(true); const [newAgentPolicyName, setNewAgentPolicyName] = useState(); + // make form dirty if new agent policy is selected + useEffect(() => { + if (newAgentPolicyName) { + setIsEdited(true); + } + }, [newAgentPolicyName, setIsEdited]); + const [hasAgentPolicyError, setHasAgentPolicyError] = useState(false); // Retrieve agent count diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx index cc91af6a873a8..aa86607a84ee5 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx @@ -230,7 +230,7 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps { field: 'packagePolicy.policy_ids', name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.agentPolicy', { - defaultMessage: 'Agent policy', + defaultMessage: 'Agent policies', }), truncateText: true, render(id, { agentPolicies, packagePolicy }) { diff --git a/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts b/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts index 8e47180076338..4b7068336bbc5 100644 --- a/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts +++ b/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts @@ -351,9 +351,6 @@ spec: effect: NoSchedule serviceAccountName: elastic-agent hostNetwork: true - # 'hostPID: true' enables the Elastic Security integration to observe all process exec events on the host. - # Sharing the host process ID namespace gives visibility of all processes running on the same host. - hostPID: true dnsPolicy: ClusterFirstWithHostNet containers: - name: elastic-agent @@ -469,7 +466,7 @@ spec: hostPath: path: /var/lib # Mount /etc/machine-id from the host to determine host ID - # Needed for Elastic Security integration + # Needed for Kubernetes node autodiscovery - name: etc-mid hostPath: path: /etc/machine-id diff --git a/x-pack/plugins/lens/kibana.jsonc b/x-pack/plugins/lens/kibana.jsonc index 367260bae0f00..10eb3721414fe 100644 --- a/x-pack/plugins/lens/kibana.jsonc +++ b/x-pack/plugins/lens/kibana.jsonc @@ -56,7 +56,7 @@ "embeddable", "fieldFormats", "charts", - "textBasedLanguages", + "esql", ], "extraPublicDirs": [ "common/constants" diff --git a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx index 5c163df2c0715..98233c7ebffef 100644 --- a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx +++ b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx @@ -26,7 +26,7 @@ import { getLanguageDisplayName, } from '@kbn/es-query'; import type { AggregateQuery, Query } from '@kbn/es-query'; -import { TextBasedLangEditor } from '@kbn/text-based-languages/public'; +import { TextBasedLangEditor } from '@kbn/esql/public'; import { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common'; import { buildExpression } from '../../../editor_frame_service/editor_frame/expression_helpers'; import { MAX_NUM_OF_COLUMNS } from '../../../datasources/text_based/utils'; diff --git a/x-pack/plugins/lens/public/shared_components/toolbar_popover.scss b/x-pack/plugins/lens/public/shared_components/toolbar_popover.scss index c06f13dfc2eb1..823859866eb2a 100644 --- a/x-pack/plugins/lens/public/shared_components/toolbar_popover.scss +++ b/x-pack/plugins/lens/public/shared_components/toolbar_popover.scss @@ -1,3 +1,3 @@ .lnsVisToolbar__popover { - width: 404px; + width: 410px; } diff --git a/x-pack/plugins/lens/public/visualizations/heatmap/toolbar_component.tsx b/x-pack/plugins/lens/public/visualizations/heatmap/toolbar_component.tsx index 327bdef6c5bc4..9d5e081ba351d 100644 --- a/x-pack/plugins/lens/public/visualizations/heatmap/toolbar_component.tsx +++ b/x-pack/plugins/lens/public/visualizations/heatmap/toolbar_component.tsx @@ -60,7 +60,7 @@ export const HeatmapToolbar = memo( +>; + +/** + * Defaults for select optional Metric vis state options + */ +export const metricStateDefaults: Required< + Pick< + MetricVisualizationStateOptionals, + 'titlesTextAlign' | 'valuesTextAlign' | 'iconAlign' | 'valueFontMode' + > +> = { + titlesTextAlign: 'left', + valuesTextAlign: 'right', + iconAlign: 'left', + valueFontMode: 'default', +}; diff --git a/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.test.tsx b/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.test.tsx index 429a681027e63..a239b12deb5be 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.test.tsx @@ -13,7 +13,7 @@ import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { euiLightVars } from '@kbn/ui-theme'; import { CustomPaletteParams, PaletteOutput, PaletteRegistry } from '@kbn/coloring'; import { VisualizationDimensionEditorProps } from '../../types'; -import { MetricVisualizationState } from './visualization'; +import { MetricVisualizationState } from './types'; import { DimensionEditor, DimensionEditorAdditionalSection, @@ -59,6 +59,10 @@ describe('dimension editor', () => { palette, icon: 'tag', showBar: true, + titlesTextAlign: 'left', + valuesTextAlign: 'right', + iconAlign: 'left', + valueFontMode: 'default', trendlineLayerId: 'second', trendlineLayerType: 'metricTrendline', trendlineMetricAccessor: 'trendline-metric-col-id', diff --git a/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.tsx b/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.tsx index 757700fa80938..f040c6dc86fa4 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.tsx @@ -34,14 +34,10 @@ import { isNumericFieldForDatatable } from '../../../common/expressions/datatabl import { applyPaletteParams, PalettePanelContainer } from '../../shared_components'; import type { VisualizationDimensionEditorProps } from '../../types'; import { defaultNumberPaletteParams, defaultPercentagePaletteParams } from './palette_config'; -import { - DEFAULT_MAX_COLUMNS, - getDefaultColor, - MetricVisualizationState, - showingBar, -} from './visualization'; +import { DEFAULT_MAX_COLUMNS, getDefaultColor, showingBar } from './visualization'; import { CollapseSetting } from '../../shared_components/collapse_setting'; import { iconsSet } from './icon_set'; +import { MetricVisualizationState } from './types'; export type SupportingVisType = 'none' | 'bar' | 'trendline'; diff --git a/x-pack/plugins/lens/public/visualizations/metric/suggestions.test.ts b/x-pack/plugins/lens/public/visualizations/metric/suggestions.test.ts index 22d701cee570b..fff365e0e89dd 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/suggestions.test.ts +++ b/x-pack/plugins/lens/public/visualizations/metric/suggestions.test.ts @@ -7,7 +7,7 @@ import { getSuggestions } from './suggestions'; import { LayerTypes } from '@kbn/expression-xy-plugin/public'; -import { MetricVisualizationState } from './visualization'; +import { MetricVisualizationState } from './types'; import { IconChartMetric } from '@kbn/chart-icons'; const metricColumn = { diff --git a/x-pack/plugins/lens/public/visualizations/metric/suggestions.ts b/x-pack/plugins/lens/public/visualizations/metric/suggestions.ts index a4077b4aca450..b405ea646d980 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/suggestions.ts +++ b/x-pack/plugins/lens/public/visualizations/metric/suggestions.ts @@ -8,7 +8,8 @@ import { IconChartMetric } from '@kbn/chart-icons'; import { LayerTypes } from '@kbn/expression-xy-plugin/public'; import type { TableSuggestion, Visualization } from '../../types'; -import { metricLabel, MetricVisualizationState, supportedDataTypes } from './visualization'; +import { MetricVisualizationState } from './types'; +import { metricLabel, supportedDataTypes } from './visualization'; const MAX_BUCKETED_COLUMNS = 1; const MAX_METRIC_COLUMNS = 2; // primary and secondary metric diff --git a/x-pack/plugins/lens/public/visualizations/metric/to_expression.ts b/x-pack/plugins/lens/public/visualizations/metric/to_expression.ts index c37ae2d57544e..d0ff261653e1f 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/to_expression.ts +++ b/x-pack/plugins/lens/public/visualizations/metric/to_expression.ts @@ -17,7 +17,9 @@ import { CollapseArgs, CollapseFunction } from '../../../common/expressions'; import { CollapseExpressionFunction } from '../../../common/expressions/collapse/types'; import { DatasourceLayers } from '../../types'; import { showingBar } from './metric_visualization'; -import { DEFAULT_MAX_COLUMNS, getDefaultColor, MetricVisualizationState } from './visualization'; +import { DEFAULT_MAX_COLUMNS, getDefaultColor } from './visualization'; +import { MetricVisualizationState } from './types'; +import { metricStateDefaults } from './constants'; // TODO - deduplicate with gauges? function computePaletteParams(params: CustomPaletteParams) { @@ -148,6 +150,10 @@ export const toExpression = ( progressDirection: showingBar(state) ? state.progressDirection || LayoutDirection.Vertical : undefined, + titlesTextAlign: state.titlesTextAlign ?? metricStateDefaults.titlesTextAlign, + valuesTextAlign: state.valuesTextAlign ?? metricStateDefaults.valuesTextAlign, + iconAlign: state.iconAlign ?? metricStateDefaults.iconAlign, + valueFontSize: state.valueFontMode ?? metricStateDefaults.valueFontMode, color: state.color || getDefaultColor(state, isMetricNumeric), icon: state.icon, palette: diff --git a/x-pack/plugins/lens/public/visualizations/metric/toolbar.test.tsx b/x-pack/plugins/lens/public/visualizations/metric/toolbar.test.tsx deleted file mode 100644 index 8b4ea8a39f3ab..0000000000000 --- a/x-pack/plugins/lens/public/visualizations/metric/toolbar.test.tsx +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { CustomPaletteParams, PaletteOutput } from '@kbn/coloring'; -import { Toolbar } from './toolbar'; -import { MetricVisualizationState } from './visualization'; -import { createMockFramePublicAPI } from '../../mocks'; -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; - -describe('metric toolbar', () => { - const palette: PaletteOutput = { - type: 'palette', - name: 'foo', - params: { - rangeType: 'percent', - }, - }; - - const fullState: Required = { - layerId: 'first', - layerType: 'data', - metricAccessor: 'metric-col-id', - secondaryMetricAccessor: 'secondary-metric-col-id', - maxAccessor: 'max-metric-col-id', - breakdownByAccessor: 'breakdown-col-id', - collapseFn: 'sum', - subtitle: 'subtitle', - secondaryPrefix: 'extra-text', - progressDirection: 'vertical', - maxCols: 5, - color: 'static-color', - icon: 'compute', - palette, - showBar: true, - trendlineLayerId: 'second', - trendlineLayerType: 'metricTrendline', - trendlineMetricAccessor: 'trendline-metric-col-id', - trendlineSecondaryMetricAccessor: 'trendline-secondary-metric-col-id', - trendlineTimeAccessor: 'trendline-time-col-id', - trendlineBreakdownByAccessor: 'trendline-breakdown-col-id', - }; - - const frame = createMockFramePublicAPI(); - - const mockSetState = jest.fn(); - - const renderToolbar = (state: MetricVisualizationState) => { - return { ...render() }; - }; - - afterEach(() => mockSetState.mockClear()); - - describe('text options', () => { - it('sets a subtitle', async () => { - renderToolbar({ - ...fullState, - breakdownByAccessor: undefined, - }); - const textOptionsButton = screen.getByTestId('lnsLabelsButton'); - textOptionsButton.click(); - - const newSubtitle = 'new subtitle hey'; - const subtitleField = screen.getByDisplayValue('subtitle'); - // cannot use userEvent because the element cannot be clicked on - fireEvent.change(subtitleField, { target: { value: newSubtitle + ' 1' } }); - await waitFor(() => expect(mockSetState).toHaveBeenCalled()); - fireEvent.change(subtitleField, { target: { value: newSubtitle + ' 2' } }); - await waitFor(() => expect(mockSetState).toHaveBeenCalledTimes(2)); - fireEvent.change(subtitleField, { target: { value: newSubtitle + ' 3' } }); - await waitFor(() => expect(mockSetState).toHaveBeenCalledTimes(3)); - expect(mockSetState.mock.calls.map(([state]) => state.subtitle)).toMatchInlineSnapshot(` - Array [ - "new subtitle hey 1", - "new subtitle hey 2", - "new subtitle hey 3", - ] - `); - }); - - it('hides text options when has breakdown by', () => { - renderToolbar({ - ...fullState, - breakdownByAccessor: 'some-accessor', - }); - expect(screen.queryByTestId('lnsLabelsButton')).not.toBeInTheDocument(); - }); - }); -}); diff --git a/x-pack/plugins/lens/public/visualizations/metric/toolbar.tsx b/x-pack/plugins/lens/public/visualizations/metric/toolbar.tsx deleted file mode 100644 index 2e7c1ca285d6e..0000000000000 --- a/x-pack/plugins/lens/public/visualizations/metric/toolbar.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useCallback } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiFlexGroup, EuiFormRow, EuiFieldText } from '@elastic/eui'; -import { useDebouncedValue } from '@kbn/visualization-utils'; -import { VisualizationToolbarProps } from '../../types'; -import { ToolbarPopover } from '../../shared_components'; -import { MetricVisualizationState } from './visualization'; - -export function Toolbar(props: VisualizationToolbarProps) { - const { state, setState } = props; - - const setSubtitle = useCallback( - (prefix: string) => setState({ ...state, subtitle: prefix }), - [setState, state] - ); - - const { inputValue: subtitleInputVal, handleInputChange: handleSubtitleChange } = - useDebouncedValue( - { - onChange: setSubtitle, - value: state.subtitle || '', - }, - { allowFalsyValue: true } - ); - - const hasBreakdownBy = Boolean(state.breakdownByAccessor); - - return ( - - {!hasBreakdownBy && ( - - - handleSubtitleChange(value)} - /> - - - )} - - ); -} diff --git a/x-pack/plugins/lens/public/visualizations/metric/toolbar/index.tsx b/x-pack/plugins/lens/public/visualizations/metric/toolbar/index.tsx new file mode 100644 index 0000000000000..503039f627974 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/metric/toolbar/index.tsx @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { Toolbar } from './toolbar'; diff --git a/x-pack/plugins/lens/public/visualizations/metric/toolbar/label_options_popover.test.tsx b/x-pack/plugins/lens/public/visualizations/metric/toolbar/label_options_popover.test.tsx new file mode 100644 index 0000000000000..1f205c60f6916 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/metric/toolbar/label_options_popover.test.tsx @@ -0,0 +1,94 @@ +/* + * 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 React from 'react'; +import { CustomPaletteParams, PaletteOutput } from '@kbn/coloring'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { MetricVisualizationState } from '../types'; +import { LabelOptionsPopover } from './label_options_popover'; + +describe('LabelOptionsPopover', () => { + const palette: PaletteOutput = { + type: 'palette', + name: 'foo', + params: { + rangeType: 'percent', + }, + }; + + const fullState: Required = { + layerId: 'first', + layerType: 'data', + metricAccessor: 'metric-col-id', + secondaryMetricAccessor: 'secondary-metric-col-id', + maxAccessor: 'max-metric-col-id', + breakdownByAccessor: 'breakdown-col-id', + collapseFn: 'sum', + subtitle: 'subtitle', + secondaryPrefix: 'extra-text', + progressDirection: 'vertical', + maxCols: 5, + color: 'static-color', + icon: 'compute', + palette, + showBar: true, + trendlineLayerId: 'second', + trendlineLayerType: 'metricTrendline', + trendlineMetricAccessor: 'trendline-metric-col-id', + trendlineSecondaryMetricAccessor: 'trendline-secondary-metric-col-id', + trendlineTimeAccessor: 'trendline-time-col-id', + trendlineBreakdownByAccessor: 'trendline-breakdown-col-id', + titlesTextAlign: 'left', + valuesTextAlign: 'right', + iconAlign: 'left', + valueFontMode: 'default', + }; + + const mockSetState = jest.fn(); + + const renderToolbarOptions = (state: MetricVisualizationState) => { + return { + ...render(), + }; + }; + + afterEach(() => mockSetState.mockClear()); + + it('should set a subtitle', async () => { + renderToolbarOptions({ + ...fullState, + breakdownByAccessor: undefined, + }); + const labelOptionsButton = screen.getByTestId('lnsLabelsButton'); + labelOptionsButton.click(); + + const newSubtitle = 'new subtitle hey'; + const subtitleField = screen.getByDisplayValue('subtitle'); + // cannot use userEvent because the element cannot be clicked on + fireEvent.change(subtitleField, { target: { value: newSubtitle + ' 1' } }); + await waitFor(() => expect(mockSetState).toHaveBeenCalled()); + fireEvent.change(subtitleField, { target: { value: newSubtitle + ' 2' } }); + await waitFor(() => expect(mockSetState).toHaveBeenCalledTimes(2)); + fireEvent.change(subtitleField, { target: { value: newSubtitle + ' 3' } }); + await waitFor(() => expect(mockSetState).toHaveBeenCalledTimes(3)); + expect(mockSetState.mock.calls.map(([state]) => state.subtitle)).toMatchInlineSnapshot(` + Array [ + "new subtitle hey 1", + "new subtitle hey 2", + "new subtitle hey 3", + ] + `); + }); + + it('should disable labels options when Metric has breakdown by', () => { + renderToolbarOptions({ + ...fullState, + breakdownByAccessor: 'some-accessor', + }); + expect(screen.getByTestId('lnsLabelsButton')).toBeDisabled(); + }); +}); diff --git a/x-pack/plugins/lens/public/visualizations/metric/toolbar/label_options_popover.tsx b/x-pack/plugins/lens/public/visualizations/metric/toolbar/label_options_popover.tsx new file mode 100644 index 0000000000000..ec7101a8dedb8 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/metric/toolbar/label_options_popover.tsx @@ -0,0 +1,72 @@ +/* + * 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 React, { FC, useCallback } from 'react'; + +import { EuiFormRow, EuiFieldText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { useDebouncedValue } from '@kbn/visualization-utils'; +import { TooltipWrapper } from '@kbn/visualization-utils'; +import { ToolbarPopover } from '../../../shared_components'; +import { MetricVisualizationState } from '../types'; + +export interface LabelOptionsPopoverProps { + state: MetricVisualizationState; + setState: (newState: MetricVisualizationState) => void; +} + +export const LabelOptionsPopover: FC = ({ state, setState }) => { + const setSubtitle = useCallback( + (prefix: string) => setState({ ...state, subtitle: prefix }), + [setState, state] + ); + + const { inputValue: subtitleInputVal, handleInputChange: handleSubtitleChange } = + useDebouncedValue( + { + onChange: setSubtitle, + value: state.subtitle || '', + }, + { allowFalsyValue: true } + ); + + const hasBreakdownBy = Boolean(state.breakdownByAccessor); + + return ( + + + + handleSubtitleChange(value)} + /> + + + + ); +}; diff --git a/x-pack/plugins/lens/public/visualizations/metric/toolbar/toolbar.tsx b/x-pack/plugins/lens/public/visualizations/metric/toolbar/toolbar.tsx new file mode 100644 index 0000000000000..1593d2dce5bf0 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/metric/toolbar/toolbar.tsx @@ -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 React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { VisualizationToolbarProps } from '../../../types'; +import { LabelOptionsPopover } from './label_options_popover'; +import { VisualOptionsPopover } from './visual_options_popover'; +import { MetricVisualizationState } from '../types'; + +export function Toolbar(props: VisualizationToolbarProps) { + const { state, setState } = props; + + return ( + + + + + + + + + ); +} diff --git a/x-pack/plugins/lens/public/visualizations/metric/toolbar/visual_options_popover.test.tsx b/x-pack/plugins/lens/public/visualizations/metric/toolbar/visual_options_popover.test.tsx new file mode 100644 index 0000000000000..06b253dc862b9 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/metric/toolbar/visual_options_popover.test.tsx @@ -0,0 +1,140 @@ +/* + * 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 React from 'react'; +import { CustomPaletteParams, PaletteOutput } from '@kbn/coloring'; +import { render, screen } from '@testing-library/react'; +import { MetricVisualizationState } from '../types'; +import { VisualOptionsPopover } from './visual_options_popover'; +import { EuiButtonGroupTestHarness } from '@kbn/test-eui-helpers'; + +jest.mock('lodash', () => ({ + ...jest.requireActual('lodash'), + debounce: (fn: unknown) => fn, +})); + +describe('VisualOptionsPopover', () => { + const palette: PaletteOutput = { + type: 'palette', + name: 'foo', + params: { + rangeType: 'percent', + }, + }; + + const fullState: Required = { + layerId: 'first', + layerType: 'data', + metricAccessor: 'metric-col-id', + secondaryMetricAccessor: 'secondary-metric-col-id', + maxAccessor: 'max-metric-col-id', + breakdownByAccessor: 'breakdown-col-id', + collapseFn: 'sum', + subtitle: 'subtitle', + secondaryPrefix: 'extra-text', + progressDirection: 'vertical', + maxCols: 5, + color: 'static-color', + icon: 'compute', + palette, + showBar: true, + trendlineLayerId: 'second', + trendlineLayerType: 'metricTrendline', + trendlineMetricAccessor: 'trendline-metric-col-id', + trendlineSecondaryMetricAccessor: 'trendline-secondary-metric-col-id', + trendlineTimeAccessor: 'trendline-time-col-id', + trendlineBreakdownByAccessor: 'trendline-breakdown-col-id', + titlesTextAlign: 'left', + valuesTextAlign: 'right', + iconAlign: 'left', + valueFontMode: 'default', + }; + + const mockSetState = jest.fn(); + + const renderToolbarOptions = (state: MetricVisualizationState) => { + return { + ...render(), + }; + }; + + afterEach(() => mockSetState.mockClear()); + + it('should set titlesTextAlign', async () => { + renderToolbarOptions({ ...fullState }); + const textOptionsButton = screen.getByTestId('lnsVisualOptionsButton'); + textOptionsButton.click(); + + const titlesAlignBtnGroup = new EuiButtonGroupTestHarness('lens-titles-alignment-btn'); + + titlesAlignBtnGroup.select('Right'); + titlesAlignBtnGroup.select('Center'); + titlesAlignBtnGroup.select('Left'); + + expect(mockSetState.mock.calls.map(([s]) => s.titlesTextAlign)).toEqual([ + 'right', + 'center', + 'left', + ]); + }); + + it('should set valuesTextAlign', async () => { + renderToolbarOptions({ ...fullState }); + const textOptionsButton = screen.getByTestId('lnsVisualOptionsButton'); + textOptionsButton.click(); + + const valueAlignBtnGroup = new EuiButtonGroupTestHarness('lens-values-alignment-btn'); + + valueAlignBtnGroup.select('Center'); + valueAlignBtnGroup.select('Left'); + valueAlignBtnGroup.select('Right'); + + expect(mockSetState.mock.calls.map(([s]) => s.valuesTextAlign)).toEqual([ + 'center', + 'left', + 'right', + ]); + }); + + it('should set valueFontMode', async () => { + renderToolbarOptions({ ...fullState }); + const textOptionsButton = screen.getByTestId('lnsVisualOptionsButton'); + textOptionsButton.click(); + + const modeBtnGroup = new EuiButtonGroupTestHarness('lens-value-font-mode-btn'); + + expect(modeBtnGroup.selected.textContent).toBe('Default'); + + modeBtnGroup.select('Fit'); + modeBtnGroup.select('Default'); + + expect(mockSetState.mock.calls.map(([s]) => s.valueFontMode)).toEqual(['fit', 'default']); + }); + + it('should set iconAlign', async () => { + renderToolbarOptions({ ...fullState, icon: 'sortUp' }); + const textOptionsButton = screen.getByTestId('lnsVisualOptionsButton'); + textOptionsButton.click(); + + const iconAlignBtnGroup = new EuiButtonGroupTestHarness('lens-icon-alignment-btn'); + + expect(iconAlignBtnGroup.selected.textContent).toBe('Left'); + + iconAlignBtnGroup.select('Right'); + iconAlignBtnGroup.select('Left'); + + expect(mockSetState.mock.calls.map(([s]) => s.iconAlign)).toEqual(['right', 'left']); + }); + + it.each([undefined, 'empty'])('should hide iconAlign option when icon is %j', async (icon) => { + renderToolbarOptions({ ...fullState, icon }); + const textOptionsButton = screen.getByTestId('lnsVisualOptionsButton'); + textOptionsButton.click(); + + expect(screen.queryByTestId('lens-icon-alignment-btn')).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/lens/public/visualizations/metric/toolbar/visual_options_popover.tsx b/x-pack/plugins/lens/public/visualizations/metric/toolbar/visual_options_popover.tsx new file mode 100644 index 0000000000000..607bba17afc24 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/metric/toolbar/visual_options_popover.tsx @@ -0,0 +1,290 @@ +/* + * 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 React, { FC } from 'react'; + +import { EuiFormRow, EuiIconTip, EuiButtonGroup } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { MetricStyle } from '@elastic/charts'; +import { ToolbarPopover } from '../../../shared_components'; +import { MetricVisualizationState, ValueFontMode } from '../types'; +import { metricStateDefaults } from '../constants'; + +export interface VisualOptionsPopoverProps { + state: MetricVisualizationState; + setState: (newState: MetricVisualizationState) => void; +} + +export const VisualOptionsPopover: FC = ({ state, setState }) => { + return ( + + { + setState({ ...state, titlesTextAlign }); + }} + /> + { + setState({ ...state, valuesTextAlign }); + }} + /> + {state.icon && state.icon !== 'empty' && ( + { + setState({ ...state, iconAlign }); + }} + /> + )} + { + setState({ ...state, valueFontMode: value }); + }} + /> + + ); +}; + +const valueFontModes: Array<{ + id: ValueFontMode; + label: string; +}> = [ + { + id: 'default', + label: i18n.translate('xpack.lens.metric.toolbarVisOptions.default', { + defaultMessage: 'Default', + }), + }, + { + id: 'fit', + label: i18n.translate('xpack.lens.metric.toolbarVisOptions.fit', { + defaultMessage: 'Fit', + }), + }, +]; + +function ValueFontOption({ + value, + onChange, +}: { + value: typeof valueFontModes[number]['id']; + onChange: (mode: ValueFontMode) => void; +}) { + const label = i18n.translate('xpack.lens.metric.toolbarVisOptions.valueFontSize', { + defaultMessage: 'Value fontSize', + }); + + return ( + + {label}{' '} + + + } + > + { + onChange(mode as ValueFontMode); + }} + /> + + ); +} + +const alignmentOptions: Array<{ + id: MetricStyle['titlesTextAlign'] | MetricStyle['valuesTextAlign']; + label: string; +}> = [ + { + id: 'left', + label: i18n.translate('xpack.lens.shared.left', { + defaultMessage: 'Left', + }), + }, + { + id: 'center', + label: i18n.translate('xpack.lens.shared.center', { + defaultMessage: 'Center', + }), + }, + { + id: 'right', + label: i18n.translate('xpack.lens.shared.right', { + defaultMessage: 'Right', + }), + }, +]; + +function TitlesAlignmentOption({ + value, + onChange, +}: { + value: MetricStyle['titlesTextAlign']; + onChange: (alignment: MetricStyle['titlesTextAlign']) => void; +}) { + const label = i18n.translate('xpack.lens.metric.toolbarVisOptions.titlesAlignment', { + defaultMessage: 'Titles alignment', + }); + + return ( + + {label}{' '} + + + } + > + { + onChange(alignment as MetricStyle['titlesTextAlign']); + }} + /> + + ); +} + +function ValuesAlignmentOption({ + value, + onChange, +}: { + value: MetricStyle['valuesTextAlign']; + onChange: (alignment: MetricStyle['valuesTextAlign']) => void; +}) { + const label = i18n.translate('xpack.lens.metric.toolbarVisOptions.valuesAlignment', { + defaultMessage: 'Values alignment', + }); + + return ( + + {label}{' '} + + + } + > + { + onChange(alignment as MetricStyle['valuesTextAlign']); + }} + /> + + ); +} + +const iconAlignmentOptions: Array<{ + id: MetricStyle['titlesTextAlign'] | MetricStyle['valuesTextAlign']; + label: string; +}> = [ + { + id: 'left', + label: i18n.translate('xpack.lens.shared.left', { + defaultMessage: 'Left', + }), + }, + { + id: 'right', + label: i18n.translate('xpack.lens.shared.right', { + defaultMessage: 'Right', + }), + }, +]; + +function IconAlignmentOption({ + value, + onChange, +}: { + value: MetricStyle['iconAlign']; + onChange: (alignment: MetricStyle['iconAlign']) => void; +}) { + const label = i18n.translate('xpack.lens.metric.toolbarVisOptions.iconAlignment', { + defaultMessage: 'Icon alignment', + }); + + return ( + + { + onChange(alignment as MetricStyle['iconAlign']); + }} + /> + + ); +} diff --git a/x-pack/plugins/lens/public/visualizations/metric/types.ts b/x-pack/plugins/lens/public/visualizations/metric/types.ts index d25a4b1b33396..774c1572ce73b 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/types.ts +++ b/x-pack/plugins/lens/public/visualizations/metric/types.ts @@ -5,11 +5,13 @@ * 2.0. */ -import type { LayoutDirection } from '@elastic/charts'; +import type { LayoutDirection, MetricStyle } from '@elastic/charts'; import type { PaletteOutput, CustomPaletteParams } from '@kbn/coloring'; import type { CollapseFunction } from '@kbn/visualizations-plugin/common'; import type { LayerType } from '../../../common/types'; +export type ValueFontMode = Exclude; + export interface MetricVisualizationState { layerId: string; layerType: LayerType; @@ -24,7 +26,12 @@ export interface MetricVisualizationState { secondaryPrefix?: string; progressDirection?: LayoutDirection; showBar?: boolean; + titlesTextAlign?: MetricStyle['titlesTextAlign']; + valuesTextAlign?: MetricStyle['valuesTextAlign']; + iconAlign?: MetricStyle['iconAlign']; + valueFontMode?: ValueFontMode; color?: string; + icon?: string; palette?: PaletteOutput; maxCols?: number; diff --git a/x-pack/plugins/lens/public/visualizations/metric/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/metric/visualization.test.ts index a907fa0cae917..af9039740f3eb 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/metric/visualization.test.ts @@ -19,10 +19,11 @@ import { Visualization, } from '../../types'; import { GROUP_ID } from './constants'; -import { getMetricVisualization, MetricVisualizationState } from './visualization'; +import { getMetricVisualization } from './visualization'; import { themeServiceMock } from '@kbn/core/public/mocks'; import { Ast } from '@kbn/interpreter'; import { LayoutDirection } from '@elastic/charts'; +import { MetricVisualizationState } from './types'; const paletteService = chartPluginMock.createPaletteRegistry(); const theme = themeServiceMock.createStartContract(); @@ -76,6 +77,10 @@ describe('metric visualization', () => { color: 'static-color', palette, showBar: false, + titlesTextAlign: 'left', + valuesTextAlign: 'right', + iconAlign: 'left', + valueFontMode: 'default', }; const fullStateWTrend: Required = { @@ -316,6 +321,9 @@ describe('metric visualization', () => { "icon": Array [ "empty", ], + "iconAlign": Array [ + "left", + ], "inspectorTableId": Array [ "first", ], @@ -353,7 +361,16 @@ describe('metric visualization', () => { "subtitle": Array [ "subtitle", ], + "titlesTextAlign": Array [ + "left", + ], "trendline": Array [], + "valueFontSize": Array [ + "default", + ], + "valuesTextAlign": Array [ + "right", + ], }, "function": "metricVis", "type": "function", @@ -380,6 +397,9 @@ describe('metric visualization', () => { "icon": Array [ "empty", ], + "iconAlign": Array [ + "left", + ], "inspectorTableId": Array [ "first", ], @@ -420,7 +440,16 @@ describe('metric visualization', () => { "subtitle": Array [ "subtitle", ], + "titlesTextAlign": Array [ + "left", + ], "trendline": Array [], + "valueFontSize": Array [ + "default", + ], + "valuesTextAlign": Array [ + "right", + ], }, "function": "metricVis", "type": "function", @@ -778,8 +807,12 @@ describe('metric visualization', () => { expect(visualization.clearLayer(fullState, 'some-id', 'indexPattern1')).toMatchInlineSnapshot(` Object { "icon": "empty", + "iconAlign": "left", "layerId": "first", "layerType": "data", + "titlesTextAlign": "left", + "valueFontMode": "default", + "valuesTextAlign": "right", } `); }); diff --git a/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx b/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx index f60b1da51505d..3a780e5a20f75 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx @@ -7,16 +7,13 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { PaletteOutput, PaletteRegistry, CustomPaletteParams } from '@kbn/coloring'; +import { PaletteRegistry } from '@kbn/coloring'; import { ThemeServiceStart } from '@kbn/core/public'; import { VIS_EVENT_TO_TRIGGER } from '@kbn/visualizations-plugin/public'; -import { LayoutDirection } from '@elastic/charts'; import { euiLightVars, euiThemeVars } from '@kbn/ui-theme'; import { IconChartMetric } from '@kbn/chart-icons'; import { AccessorConfig } from '@kbn/visualization-ui-components'; import { isNumericFieldForDatatable } from '../../../common/expressions/datatable/utils'; -import { CollapseFunction } from '../../../common/expressions'; -import type { LayerType } from '../../../common/types'; import { layerTypes } from '../../../common/layer_types'; import type { FormBasedPersistedState } from '../../datasources/form_based/types'; import { getSuggestions } from './suggestions'; @@ -35,6 +32,7 @@ import { generateId } from '../../id_generator'; import { toExpression } from './to_expression'; import { nonNullable } from '../../utils'; import { METRIC_NUMERIC_MAX } from '../../user_messages_ids'; +import { MetricVisualizationState } from './types'; export const DEFAULT_MAX_COLUMNS = 3; @@ -49,33 +47,6 @@ export const getDefaultColor = (state: MetricVisualizationState, isMetricNumeric : euiThemeVars.euiColorEmptyShade; }; -export interface MetricVisualizationState { - layerId: string; - layerType: LayerType; - metricAccessor?: string; - secondaryMetricAccessor?: string; - maxAccessor?: string; - breakdownByAccessor?: string; - // the dimensions can optionally be single numbers - // computed by collapsing all rows - collapseFn?: CollapseFunction; - subtitle?: string; - secondaryPrefix?: string; - progressDirection?: LayoutDirection; - showBar?: boolean; - color?: string; - icon?: string; - palette?: PaletteOutput; - maxCols?: number; - - trendlineLayerId?: string; - trendlineLayerType?: LayerType; - trendlineTimeAccessor?: string; - trendlineMetricAccessor?: string; - trendlineSecondaryMetricAccessor?: string; - trendlineBreakdownByAccessor?: string; -} - export const supportedDataTypes = new Set(['string', 'boolean', 'number', 'ip', 'date']); const isSupportedMetric = (op: OperationMetadata) => diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/visual_options_popover/index.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/visual_options_popover/index.tsx index b8b244daf85d2..5fd35a780cda5 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/visual_options_popover/index.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/visual_options_popover/index.tsx @@ -79,7 +79,7 @@ export const VisualOptionsPopover: React.FC = ({ return ( { ); } - if (fullLicense) { + if (fullLicense && mlCapabilities.canGetMlInfo) { registerMlUiActions(pluginsSetup.uiActions, core); if (this.enabledFeatures.ad) { diff --git a/x-pack/plugins/observability_solution/dataset_quality/common/data_streams_stats/data_stream_stat.ts b/x-pack/plugins/observability_solution/dataset_quality/common/data_streams_stats/data_stream_stat.ts index 5968c369732de..164a43c625fb1 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/common/data_streams_stats/data_stream_stat.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/common/data_streams_stats/data_stream_stat.ts @@ -17,7 +17,7 @@ export class DataStreamStat { type: DataStreamType; name: DataStreamStatType['name']; namespace: string; - title: string; + title?: string; size?: DataStreamStatType['size']; // total datastream size sizeBytes?: DataStreamStatType['sizeBytes']; // total datastream size lastActivity?: DataStreamStatType['lastActivity']; diff --git a/x-pack/plugins/observability_solution/dataset_quality/common/types/common.ts b/x-pack/plugins/observability_solution/dataset_quality/common/types/common.ts index ca5c4632ec0d3..82d7b64e25f63 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/common/types/common.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/common/types/common.ts @@ -22,6 +22,5 @@ export interface BasicDataStream { name: DataStreamStatType['name']; rawName: string; namespace: string; - title: string; integration?: Integration; } diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/flyout.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/flyout.tsx index 7411067a7317f..7dc455b280444 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/flyout.tsx +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/flyout.tsx @@ -43,15 +43,16 @@ export default function Flyout({ dataset, closeFlyout }: FlyoutProps) { integration, } = useDatasetQualityFlyout(); - const titleAndLinkDetails: BasicDataStream = { + const linkDetails: BasicDataStream = { name: dataset.name, rawName: dataset.rawName, integration: integration?.integrationDetails, type: dataset.type, namespace: dataset.namespace, - title: integration?.integrationDetails?.datasets?.[dataset.name] ?? dataset.name, }; + const title = integration?.integrationDetails?.datasets?.[dataset.name] ?? dataset.name; + const { startTracking } = useDatasetDetailsTelemetry(); useEffect(() => { @@ -70,8 +71,9 @@ export default function Flyout({ dataset, closeFlyout }: FlyoutProps) { ) : ( <>
diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/header.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/header.tsx index 5fc66f79b79ba..10d0c96a057bd 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/header.tsx +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/header.tsx @@ -27,17 +27,19 @@ import { IntegrationIcon } from '../common'; import { BasicDataStream } from '../../../common/types'; export function Header({ - titleAndLinkDetails, + linkDetails, loading, + title, }: { - titleAndLinkDetails: BasicDataStream; + linkDetails: BasicDataStream; loading: boolean; + title: string; }) { - const { integration, title } = titleAndLinkDetails; + const { integration } = linkDetails; const euiShadow = useEuiShadow('s'); const { euiTheme } = useEuiTheme(); const redirectLinkProps = useRedirectLink({ - dataStreamStat: titleAndLinkDetails, + dataStreamStat: linkDetails, telemetry: { page: 'details', navigationSource: NavigationSource.Header, diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/built_in/services.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/built_in/services.ts index 13dcb5b1fe6c8..02a77efda6dfd 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/built_in/services.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/built_in/services.ts @@ -20,6 +20,7 @@ const serviceTransactionFilter = (additionalFilters: string[] = []) => { export const builtInServicesFromLogsEntityDefinition: EntityDefinition = entityDefinitionSchema.parse({ + version: '0.1.0', id: `${BUILT_IN_ID_PREFIX}services_from_ecs_data`, name: 'Services from ECS data', description: diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/create_and_install_ingest_pipeline.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/create_and_install_ingest_pipeline.ts index ce8515a5e31db..23feb84fa5336 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/create_and_install_ingest_pipeline.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/create_and_install_ingest_pipeline.ts @@ -28,6 +28,9 @@ export async function createAndInstallHistoryIngestPipeline( esClient.ingest.putPipeline({ id: historyId, processors: historyProcessors, + _meta: { + definitionVersion: definition.version, + }, }), { logger } ); @@ -51,6 +54,9 @@ export async function createAndInstallLatestIngestPipeline( esClient.ingest.putPipeline({ id: latestId, processors: latestProcessors, + _meta: { + definitionVersion: definition.version, + }, }), { logger } ); diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/fixtures/entity_definition.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/fixtures/entity_definition.ts index b0d47a9044d4a..137560df13385 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/fixtures/entity_definition.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/fixtures/entity_definition.ts @@ -8,6 +8,7 @@ import { entityDefinitionSchema } from '@kbn/entities-schema'; export const entityDefinition = entityDefinitionSchema.parse({ id: 'admin-console-services', + version: '999.999.999', name: 'Services for Admin Console', type: 'service', indexPatterns: ['kbn-data-forge-fake_stack.*'], diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_history_processors.test.ts.snap b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_history_processors.test.ts.snap index c646694ffef5e..eb74937a7e8c8 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_history_processors.test.ts.snap +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_history_processors.test.ts.snap @@ -20,6 +20,18 @@ Array [ "value": "admin-console-services", }, }, + Object { + "set": Object { + "field": "entity.definitionVersion", + "value": "999.999.999", + }, + }, + Object { + "set": Object { + "field": "entity.schemaVersion", + "value": "v1", + }, + }, Object { "set": Object { "field": "entity.displayName", diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_latest_processors.test.ts.snap b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_latest_processors.test.ts.snap index 33d8504d937b2..c66bd8337da47 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_latest_processors.test.ts.snap +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_latest_processors.test.ts.snap @@ -20,6 +20,18 @@ Array [ "value": "admin-console-services", }, }, + Object { + "set": Object { + "field": "entity.definitionVersion", + "value": "999.999.999", + }, + }, + Object { + "set": Object { + "field": "entity.schemaVersion", + "value": "v1", + }, + }, Object { "script": Object { "source": "if (ctx.entity?.metadata?.tags.data != null) { diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.ts index eea33d9adda79..b273be780910b 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.ts @@ -6,6 +6,7 @@ */ import { EntityDefinition } from '@kbn/entities-schema'; +import { ENTITY_SCHEMA_VERSION_V1 } from '../../../../common/constants_entities'; import { generateHistoryIndexName } from '../helpers/generate_component_id'; function createIdTemplate(definition: EntityDefinition) { @@ -62,6 +63,18 @@ export function generateHistoryProcessors(definition: EntityDefinition) { value: definition.id, }, }, + { + set: { + field: 'entity.definitionVersion', + value: definition.version, + }, + }, + { + set: { + field: 'entity.schemaVersion', + value: ENTITY_SCHEMA_VERSION_V1, + }, + }, { set: { field: 'entity.displayName', diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_latest_processors.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_latest_processors.ts index 1574659723601..056fb043ab05c 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_latest_processors.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_latest_processors.ts @@ -6,6 +6,7 @@ */ import { EntityDefinition } from '@kbn/entities-schema'; +import { ENTITY_SCHEMA_VERSION_V1 } from '../../../../common/constants_entities'; import { generateLatestIndexName } from '../helpers/generate_component_id'; function mapDestinationToPainless(destination: string) { @@ -56,6 +57,18 @@ export function generateLatestProcessors(definition: EntityDefinition) { value: definition.id, }, }, + { + set: { + field: 'entity.definitionVersion', + value: definition.version, + }, + }, + { + set: { + field: 'entity.schemaVersion', + value: ENTITY_SCHEMA_VERSION_V1, + }, + }, ...(definition.staticFields != null ? Object.keys(definition.staticFields).map((field) => ({ set: { field, value: definition.staticFields![field] }, diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/install_entity_definition.test.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/install_entity_definition.test.ts index a63c9f1262b60..676d3809c1cca 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/install_entity_definition.test.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/install_entity_definition.test.ts @@ -38,10 +38,16 @@ const assertHasCreatedDefinition = ( expect(esClient.ingest.putPipeline).toBeCalledWith({ id: generateHistoryIngestPipelineId(builtInServicesFromLogsEntityDefinition), processors: expect.anything(), + _meta: { + definitionVersion: '0.1.0', + }, }); expect(esClient.ingest.putPipeline).toBeCalledWith({ id: generateLatestIngestPipelineId(builtInServicesFromLogsEntityDefinition), processors: expect.anything(), + _meta: { + definitionVersion: '0.1.0', + }, }); expect(esClient.transform.putTransform).toBeCalledTimes(2); diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/__snapshots__/generate_history_transform.test.ts.snap b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/__snapshots__/generate_history_transform.test.ts.snap index bbea064c9b7ec..961f57045a5a2 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/__snapshots__/generate_history_transform.test.ts.snap +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/__snapshots__/generate_history_transform.test.ts.snap @@ -2,6 +2,9 @@ exports[`generateHistoryTransform(definition) should generate a valid latest transform 1`] = ` Object { + "_meta": Object { + "definitionVersion": "999.999.999", + }, "defer_validation": true, "dest": Object { "index": ".entities.v1.history.noop", diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/__snapshots__/generate_latest_transform.test.ts.snap b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/__snapshots__/generate_latest_transform.test.ts.snap index d9dff5aa54c2e..aa0b2c7e871d2 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/__snapshots__/generate_latest_transform.test.ts.snap +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/__snapshots__/generate_latest_transform.test.ts.snap @@ -2,6 +2,9 @@ exports[`generateLatestTransform(definition) should generate a valid latest transform 1`] = ` Object { + "_meta": Object { + "definitionVersion": "999.999.999", + }, "defer_validation": true, "dest": Object { "index": ".entities.v1.latest.noop", diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_history_transform.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_history_transform.ts index 2fd430f142b72..4467c2425976c 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_history_transform.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_history_transform.ts @@ -34,6 +34,9 @@ export function generateHistoryTransform( return { transform_id: generateHistoryTransformId(definition), + _meta: { + definitionVersion: definition.version, + }, defer_validation: true, source: { index: definition.indexPatterns, diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_latest_transform.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_latest_transform.ts index 9f19f083291f2..e52aa10e0820d 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_latest_transform.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_latest_transform.ts @@ -25,6 +25,9 @@ export function generateLatestTransform( ): TransformPutTransformRequest { return { transform_id: generateLatestTransformId(definition), + _meta: { + definitionVersion: definition.version, + }, defer_validation: true, source: { index: `${generateHistoryIndexName(definition)}.*`, diff --git a/x-pack/plugins/observability_solution/entity_manager/server/saved_objects/entity_definition.ts b/x-pack/plugins/observability_solution/entity_manager/server/saved_objects/entity_definition.ts index 5ddba63b7ad07..b4bf24580d632 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/saved_objects/entity_definition.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/saved_objects/entity_definition.ts @@ -18,6 +18,7 @@ export const entityDefinition: SavedObjectsType = { dynamic: false, properties: { id: { type: 'keyword' }, + version: { type: 'keyword' }, name: { type: 'text' }, description: { type: 'text' }, type: { type: 'keyword' }, @@ -37,4 +38,16 @@ export const entityDefinition: SavedObjectsType = { return `EntityDefinition: [${savedObject.attributes.name}]`; }, }, + modelVersions: { + '1': { + changes: [ + { + type: 'mappings_addition', + addedMappings: { + version: { type: 'keyword' }, + }, + }, + ], + }, + }, }; diff --git a/x-pack/plugins/observability_solution/entity_manager/server/templates/components/entity.ts b/x-pack/plugins/observability_solution/entity_manager/server/templates/components/entity.ts index 88c01b113c0e4..fd7e82749f069 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/templates/components/entity.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/templates/components/entity.ts @@ -41,6 +41,14 @@ export const entitiesEntityComponentTemplateConfig: ClusterPutComponentTemplateR ignore_above: 1024, type: 'keyword', }, + definitionVersion: { + ignore_above: 1024, + type: 'keyword', + }, + schemaVersion: { + ignore_above: 1024, + type: 'keyword', + }, lastSeenTimestamp: { type: 'date', }, diff --git a/x-pack/plugins/observability_solution/exploratory_view/kibana.jsonc b/x-pack/plugins/observability_solution/exploratory_view/kibana.jsonc index 9fadcd2d68ca3..4061de177e427 100644 --- a/x-pack/plugins/observability_solution/exploratory_view/kibana.jsonc +++ b/x-pack/plugins/observability_solution/exploratory_view/kibana.jsonc @@ -1,7 +1,7 @@ { "type": "plugin", "id": "@kbn/exploratory-view-plugin", - "owner": "@elastic/obs-ux-infra_services-team", + "owner": "@elastic/obs-ux-management-team", "plugin": { "id": "exploratoryView", "server": false, diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/dataset_quality_link.tsx b/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/dataset_quality_link.tsx index 24782cd2ab2bb..6610db470014b 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/dataset_quality_link.tsx +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/dataset_quality_link.tsx @@ -12,7 +12,7 @@ import { BrowserUrlService } from '@kbn/share-plugin/public'; import { MatchedStateFromActor } from '@kbn/xstate-utils'; import { useActor } from '@xstate/react'; import React from 'react'; -import { DataQualityLocatorParams, DATA_QUALITY_LOCATOR_ID } from '@kbn/data-quality-plugin/common'; +import { DataQualityLocatorParams, DATA_QUALITY_LOCATOR_ID } from '@kbn/deeplinks-observability'; import { datasetQualityLinkTitle } from '../../common/translations'; import { ObservabilityLogsExplorerService, diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/tsconfig.json b/x-pack/plugins/observability_solution/observability_logs_explorer/tsconfig.json index 446c237e257eb..26db8756fb9a1 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/tsconfig.json +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/tsconfig.json @@ -49,7 +49,6 @@ "@kbn/es-query", "@kbn/core-analytics-browser", "@kbn/react-hooks", - "@kbn/data-quality-plugin", "@kbn/ebt", ], "exclude": [ diff --git a/x-pack/plugins/observability_solution/synthetics/kibana.jsonc b/x-pack/plugins/observability_solution/synthetics/kibana.jsonc index 90076811dc75b..3e0715e48abb1 100644 --- a/x-pack/plugins/observability_solution/synthetics/kibana.jsonc +++ b/x-pack/plugins/observability_solution/synthetics/kibana.jsonc @@ -1,7 +1,7 @@ { "type": "plugin", "id": "@kbn/synthetics-plugin", - "owner": "@elastic/obs-ux-infra_services-team", + "owner": "@elastic/obs-ux-management-team", "description": "This plugin visualizes data from Synthetics and Heartbeat, and integrates with other Observability solutions.", "plugin": { "id": "synthetics", diff --git a/x-pack/plugins/observability_solution/uptime/kibana.jsonc b/x-pack/plugins/observability_solution/uptime/kibana.jsonc index 3e27bac31cba6..b45d8b78bc9cc 100644 --- a/x-pack/plugins/observability_solution/uptime/kibana.jsonc +++ b/x-pack/plugins/observability_solution/uptime/kibana.jsonc @@ -1,7 +1,7 @@ { "type": "plugin", "id": "@kbn/uptime-plugin", - "owner": "@elastic/obs-ux-infra_services-team", + "owner": "@elastic/obs-ux-management-team", "description": "This plugin visualizes data from Heartbeat, and integrates with other Observability solutions.", "plugin": { "id": "uptime", diff --git a/x-pack/plugins/osquery/common/api/live_query/create_live_query.gen.ts b/x-pack/plugins/osquery/common/api/live_query/create_live_query.gen.ts index a0635327aaf30..e435a79acdab4 100644 --- a/x-pack/plugins/osquery/common/api/live_query/create_live_query.gen.ts +++ b/x-pack/plugins/osquery/common/api/live_query/create_live_query.gen.ts @@ -40,6 +40,3 @@ export const CreateLiveQueryRequestBody = z.object({ event_ids: z.array(z.string()).optional(), metadata: z.object({}).nullable().optional(), }); - -export type SuccessResponse = z.infer; -export const SuccessResponse = z.object({}); diff --git a/x-pack/plugins/osquery/common/api/live_query/create_live_query.schema.yaml b/x-pack/plugins/osquery/common/api/live_query/create_live_query.schema.yaml index 4cae65cab8b82..1e40158c638d7 100644 --- a/x-pack/plugins/osquery/common/api/live_query/create_live_query.schema.yaml +++ b/x-pack/plugins/osquery/common/api/live_query/create_live_query.schema.yaml @@ -47,7 +47,3 @@ components: metadata: type: object nullable: true - SuccessResponse: - type: object - properties: {} - # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/live_query/find_live_query.gen.ts b/x-pack/plugins/osquery/common/api/live_query/find_live_query.gen.ts index 4860fae7cb7ca..313bb98064a50 100644 --- a/x-pack/plugins/osquery/common/api/live_query/find_live_query.gen.ts +++ b/x-pack/plugins/osquery/common/api/live_query/find_live_query.gen.ts @@ -32,6 +32,3 @@ export const FindLiveQueryRequestQuery = z.object({ sort: SortOrUndefined.optional(), sortOrder: SortOrderOrUndefined.optional(), }); - -export type SuccessResponse = z.infer; -export const SuccessResponse = z.object({}); diff --git a/x-pack/plugins/osquery/common/api/live_query/find_live_query.schema.yaml b/x-pack/plugins/osquery/common/api/live_query/find_live_query.schema.yaml index 730051c5a329d..50589ecc9fc28 100644 --- a/x-pack/plugins/osquery/common/api/live_query/find_live_query.schema.yaml +++ b/x-pack/plugins/osquery/common/api/live_query/find_live_query.schema.yaml @@ -4,13 +4,6 @@ info: version: '2023-10-31' paths: { } components: - parameters: - FindLiveQueryRequestQueryParameter: - name: query - in: query - required: true - schema: - $ref: '#/components/schemas/FindLiveQueryRequestQuery' schemas: FindLiveQueryRequestQuery: type: object @@ -25,7 +18,3 @@ components: $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/SortOrUndefined' sortOrder: $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/SortOrderOrUndefined' - SuccessResponse: - type: object - properties: {} - # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/live_query/get_live_query_details.gen.ts b/x-pack/plugins/osquery/common/api/live_query/get_live_query_details.gen.ts deleted file mode 100644 index 503b7f79f5a71..0000000000000 --- a/x-pack/plugins/osquery/common/api/live_query/get_live_query_details.gen.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/* - * NOTICE: Do not edit this file manually. - * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. - * - * info: - * title: Get Live Query Details Schema - * version: 2023-10-31 - */ - -import { z } from 'zod'; - -export type SuccessResponse = z.infer; -export const SuccessResponse = z.object({}); diff --git a/x-pack/plugins/osquery/common/api/live_query/get_live_query_details.schema.yaml b/x-pack/plugins/osquery/common/api/live_query/get_live_query_details.schema.yaml deleted file mode 100644 index 8bc32f55162f4..0000000000000 --- a/x-pack/plugins/osquery/common/api/live_query/get_live_query_details.schema.yaml +++ /dev/null @@ -1,24 +0,0 @@ -openapi: 3.0.0 -info: - title: Get Live Query Details Schema - version: '2023-10-31' -paths: { } -components: - parameters: - GetLiveQueryDetailsRequestParameter: - name: id - in: path - required: true - schema: - $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/Id' - GetLiveQueryDetailsRequestQueryParameter: - name: query - in: query - schema: - type: object - additionalProperties: true - schemas: - SuccessResponse: - type: object - properties: {} - # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/live_query/get_live_query_results.gen.ts b/x-pack/plugins/osquery/common/api/live_query/get_live_query_results.gen.ts index 171755c49941e..b1ef683502bc3 100644 --- a/x-pack/plugins/osquery/common/api/live_query/get_live_query_results.gen.ts +++ b/x-pack/plugins/osquery/common/api/live_query/get_live_query_results.gen.ts @@ -39,6 +39,3 @@ export const GetLiveQueryResultsRequestParams = z.object({ id: Id.optional(), actionId: Id.optional(), }); - -export type SuccessResponse = z.infer; -export const SuccessResponse = z.object({}); diff --git a/x-pack/plugins/osquery/common/api/live_query/get_live_query_results.schema.yaml b/x-pack/plugins/osquery/common/api/live_query/get_live_query_results.schema.yaml index 6036820ef022b..9d946d6fe821d 100644 --- a/x-pack/plugins/osquery/common/api/live_query/get_live_query_results.schema.yaml +++ b/x-pack/plugins/osquery/common/api/live_query/get_live_query_results.schema.yaml @@ -4,19 +4,6 @@ info: version: '2023-10-31' paths: { } components: - parameters: - GetLiveQueryRequestResultsQueryParameter: - name: query - in: query - required: true - schema: - $ref: '#/components/schemas/GetLiveQueryResultsRequestQuery' - GetLiveQueryRequestResultsParameter: - name: query - in: path - required: true - schema: - $ref: '#/components/schemas/GetLiveQueryResultsRequestParams' schemas: GetLiveQueryResultsRequestQuery: type: object @@ -38,8 +25,3 @@ components: $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/Id' actionId: $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/Id' - - SuccessResponse: - type: object - properties: {} - # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/live_query/live_queries.gen.ts b/x-pack/plugins/osquery/common/api/live_query/live_queries.gen.ts new file mode 100644 index 0000000000000..03c760cf90f76 --- /dev/null +++ b/x-pack/plugins/osquery/common/api/live_query/live_queries.gen.ts @@ -0,0 +1,88 @@ +/* + * 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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Live Queries Schema + * version: 2023-10-31 + */ + +import { z } from 'zod'; + +import { FindLiveQueryRequestQuery } from './find_live_query.gen'; +import { DefaultSuccessResponse, Id } from '../model/schema/common_attributes.gen'; +import { CreateLiveQueryRequestBody } from './create_live_query.gen'; +import { + GetLiveQueryResultsRequestQuery, + GetLiveQueryResultsRequestParams, +} from './get_live_query_results.gen'; + +export type OsqueryCreateLiveQueryRequestBody = z.infer; +export const OsqueryCreateLiveQueryRequestBody = CreateLiveQueryRequestBody; +export type OsqueryCreateLiveQueryRequestBodyInput = z.input< + typeof OsqueryCreateLiveQueryRequestBody +>; + +export type OsqueryCreateLiveQueryResponse = z.infer; +export const OsqueryCreateLiveQueryResponse = DefaultSuccessResponse; +export type OsqueryFindLiveQueriesRequestQuery = z.infer; +export const OsqueryFindLiveQueriesRequestQuery = z.object({ + query: FindLiveQueryRequestQuery, +}); +export type OsqueryFindLiveQueriesRequestQueryInput = z.input< + typeof OsqueryFindLiveQueriesRequestQuery +>; + +export type OsqueryFindLiveQueriesResponse = z.infer; +export const OsqueryFindLiveQueriesResponse = DefaultSuccessResponse; +export type OsqueryGetLiveQueryDetailsRequestQuery = z.infer< + typeof OsqueryGetLiveQueryDetailsRequestQuery +>; +export const OsqueryGetLiveQueryDetailsRequestQuery = z.object({ + query: z.object({}), +}); +export type OsqueryGetLiveQueryDetailsRequestQueryInput = z.input< + typeof OsqueryGetLiveQueryDetailsRequestQuery +>; + +export type OsqueryGetLiveQueryDetailsRequestParams = z.infer< + typeof OsqueryGetLiveQueryDetailsRequestParams +>; +export const OsqueryGetLiveQueryDetailsRequestParams = z.object({ + id: Id, +}); +export type OsqueryGetLiveQueryDetailsRequestParamsInput = z.input< + typeof OsqueryGetLiveQueryDetailsRequestParams +>; + +export type OsqueryGetLiveQueryDetailsResponse = z.infer; +export const OsqueryGetLiveQueryDetailsResponse = DefaultSuccessResponse; +export type OsqueryGetLiveQueryResultsRequestQuery = z.infer< + typeof OsqueryGetLiveQueryResultsRequestQuery +>; +export const OsqueryGetLiveQueryResultsRequestQuery = z.object({ + query: GetLiveQueryResultsRequestQuery, +}); +export type OsqueryGetLiveQueryResultsRequestQueryInput = z.input< + typeof OsqueryGetLiveQueryResultsRequestQuery +>; + +export type OsqueryGetLiveQueryResultsRequestParams = z.infer< + typeof OsqueryGetLiveQueryResultsRequestParams +>; +export const OsqueryGetLiveQueryResultsRequestParams = z.object({ + query: GetLiveQueryResultsRequestParams, +}); +export type OsqueryGetLiveQueryResultsRequestParamsInput = z.input< + typeof OsqueryGetLiveQueryResultsRequestParams +>; + +export type OsqueryGetLiveQueryResultsResponse = z.infer; +export const OsqueryGetLiveQueryResultsResponse = DefaultSuccessResponse; diff --git a/x-pack/plugins/osquery/common/api/live_query/live_queries.schema.yaml b/x-pack/plugins/osquery/common/api/live_query/live_queries.schema.yaml index feff5dd665f33..ae7c9e4d50f2d 100644 --- a/x-pack/plugins/osquery/common/api/live_query/live_queries.schema.yaml +++ b/x-pack/plugins/osquery/common/api/live_query/live_queries.schema.yaml @@ -6,17 +6,32 @@ paths: /api/osquery/live_queries: get: summary: Find live queries + operationId: OsqueryFindLiveQueries + x-codegen-enabled: true + x-labels: + - ess + - serverless parameters: - - $ref: './find_live_query.schema.yaml#/components/parameters/FindLiveQueryRequestQueryParameter' + - name: query + in: query + required: true + schema: + $ref: './find_live_query.schema.yaml#/components/schemas/FindLiveQueryRequestQuery' responses: '200': description: OK content: application/json: schema: - $ref: './find_live_query.schema.yaml#/components/schemas/SuccessResponse' + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/DefaultSuccessResponse' + post: summary: Create a live query + operationId: OsqueryCreateLiveQuery + x-codegen-enabled: true + x-labels: + - ess + - serverless requestBody: required: true content: @@ -29,30 +44,59 @@ paths: content: application/json: schema: - $ref: './create_live_query.schema.yaml#/components/schemas/SuccessResponse' + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/DefaultSuccessResponse' + /api/osquery/live_queries/{id}: get: summary: Get live query details + operationId: OsqueryGetLiveQueryDetails + x-codegen-enabled: true + x-labels: + - ess + - serverless parameters: - - $ref: './get_live_query_details.schema.yaml#/components/parameters/GetLiveQueryDetailsRequestQueryParameter' - - $ref: './get_live_query_details.schema.yaml#/components/parameters/GetLiveQueryDetailsRequestParameter' + - name: id + in: path + required: true + schema: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/Id' + - name: query + in: query + schema: + type: object + additionalProperties: true responses: '200': description: OK content: application/json: schema: - $ref: './get_live_query_details.schema.yaml#/components/schemas/SuccessResponse' + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/DefaultSuccessResponse' + /api/osquery/live_queries/{id}/results/{actionId}: - get: - summary: Get live query results - parameters: - - $ref: './get_live_query_results.schema.yaml#/components/parameters/GetLiveQueryRequestResultsQueryParameter' - - $ref: './get_live_query_results.schema.yaml#/components/parameters/GetLiveQueryRequestResultsParameter' - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: './get_live_query_results.schema.yaml#/components/schemas/SuccessResponse' + get: + summary: Get live query results + operationId: OsqueryGetLiveQueryResults + x-codegen-enabled: true + x-labels: + - ess + - serverless + parameters: + - name: query + in: query + required: true + schema: + $ref: './get_live_query_results.schema.yaml#/components/schemas/GetLiveQueryResultsRequestQuery' + - name: query + in: path + required: true + schema: + $ref: './get_live_query_results.schema.yaml#/components/schemas/GetLiveQueryResultsRequestParams' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/DefaultSuccessResponse' + diff --git a/x-pack/plugins/osquery/common/api/model/schema/common_attributes.gen.ts b/x-pack/plugins/osquery/common/api/model/schema/common_attributes.gen.ts index f57cd361a0dd2..71cfeda094ff9 100644 --- a/x-pack/plugins/osquery/common/api/model/schema/common_attributes.gen.ts +++ b/x-pack/plugins/osquery/common/api/model/schema/common_attributes.gen.ts @@ -178,3 +178,6 @@ export const SortOrderOrUndefined = z.union([z.string().nullable(), z.unknown()] export type Shards = z.infer; export const Shards = z.object({}).catchall(z.number()); + +export type DefaultSuccessResponse = z.infer; +export const DefaultSuccessResponse = z.object({}); diff --git a/x-pack/plugins/osquery/common/api/model/schema/common_attributes.schema.yaml b/x-pack/plugins/osquery/common/api/model/schema/common_attributes.schema.yaml index ba236b8601e7d..9f24757ee300c 100644 --- a/x-pack/plugins/osquery/common/api/model/schema/common_attributes.schema.yaml +++ b/x-pack/plugins/osquery/common/api/model/schema/common_attributes.schema.yaml @@ -241,11 +241,15 @@ components: SortOrderOrUndefined: oneOf: - - type: string - nullable: true - - enum: [ asc, desc ] + - type: string + nullable: true + - enum: [ asc, desc ] Shards: type: object additionalProperties: type: number + + DefaultSuccessResponse: + type: object + properties: { } diff --git a/x-pack/plugins/osquery/common/api/packs/create_pack.gen.ts b/x-pack/plugins/osquery/common/api/packs/create_pack.gen.ts index 54cd665b25750..1269df0f65053 100644 --- a/x-pack/plugins/osquery/common/api/packs/create_pack.gen.ts +++ b/x-pack/plugins/osquery/common/api/packs/create_pack.gen.ts @@ -34,6 +34,3 @@ export const CreatePacksRequestBody = z.object({ shards: Shards.optional(), queries: ObjectQueries.optional(), }); - -export type SuccessResponse = z.infer; -export const SuccessResponse = z.object({}); diff --git a/x-pack/plugins/osquery/common/api/packs/create_pack.schema.yaml b/x-pack/plugins/osquery/common/api/packs/create_pack.schema.yaml index da04d037b1d56..cac8b877bac1a 100644 --- a/x-pack/plugins/osquery/common/api/packs/create_pack.schema.yaml +++ b/x-pack/plugins/osquery/common/api/packs/create_pack.schema.yaml @@ -20,7 +20,4 @@ components: $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/Shards' queries: $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/ObjectQueries' - SuccessResponse: - type: object - properties: {} - # Define properties for the success response if needed + diff --git a/x-pack/plugins/osquery/common/api/packs/delete_packs.gen.ts b/x-pack/plugins/osquery/common/api/packs/delete_packs.gen.ts index 4efab4f488a1d..525950384cc2f 100644 --- a/x-pack/plugins/osquery/common/api/packs/delete_packs.gen.ts +++ b/x-pack/plugins/osquery/common/api/packs/delete_packs.gen.ts @@ -22,6 +22,3 @@ export type DeletePacksRequestQuery = z.infer; export const DeletePacksRequestQuery = z.object({ id: PackId.optional(), }); - -export type SuccessResponse = z.infer; -export const SuccessResponse = z.object({}); diff --git a/x-pack/plugins/osquery/common/api/packs/delete_packs.schema.yaml b/x-pack/plugins/osquery/common/api/packs/delete_packs.schema.yaml index 3286aa0b1bb7a..b26c5e415450a 100644 --- a/x-pack/plugins/osquery/common/api/packs/delete_packs.schema.yaml +++ b/x-pack/plugins/osquery/common/api/packs/delete_packs.schema.yaml @@ -4,20 +4,9 @@ info: version: '2023-10-31' paths: { } components: - parameters: - DeletePacksRequestQueryParameter: - name: query - in: path - required: true - schema: - $ref: '#/components/schemas/DeletePacksRequestQuery' schemas: DeletePacksRequestQuery: type: object properties: id: $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/PackId' - SuccessResponse: - type: object - properties: {} - # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/packs/find_packs.gen.ts b/x-pack/plugins/osquery/common/api/packs/find_packs.gen.ts index 9ff3e8c6ae0df..dcad0cb0c9757 100644 --- a/x-pack/plugins/osquery/common/api/packs/find_packs.gen.ts +++ b/x-pack/plugins/osquery/common/api/packs/find_packs.gen.ts @@ -30,6 +30,3 @@ export const FindPacksRequestQuery = z.object({ sort: SortOrUndefined.optional(), sortOrder: SortOrderOrUndefined.optional(), }); - -export type SuccessResponse = z.infer; -export const SuccessResponse = z.object({}); diff --git a/x-pack/plugins/osquery/common/api/packs/find_packs.schema.yaml b/x-pack/plugins/osquery/common/api/packs/find_packs.schema.yaml index 4cd1c222bc6d2..c93d9249b7299 100644 --- a/x-pack/plugins/osquery/common/api/packs/find_packs.schema.yaml +++ b/x-pack/plugins/osquery/common/api/packs/find_packs.schema.yaml @@ -4,13 +4,6 @@ info: version: '2023-10-31' paths: { } components: - parameters: - FindPacksRequestQueryParameter: - name: query - in: query - required: true - schema: - $ref: '#/components/schemas/FindPacksRequestQuery' schemas: FindPacksRequestQuery: type: object @@ -23,7 +16,3 @@ components: $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/SortOrUndefined' sortOrder: $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/SortOrderOrUndefined' - SuccessResponse: - type: object - properties: {} - # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/packs/packs.gen.ts b/x-pack/plugins/osquery/common/api/packs/packs.gen.ts new file mode 100644 index 0000000000000..26d5e8536b884 --- /dev/null +++ b/x-pack/plugins/osquery/common/api/packs/packs.gen.ts @@ -0,0 +1,74 @@ +/* + * 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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Packs Schema + * version: 2023-10-31 + */ + +import { z } from 'zod'; + +import { FindPacksRequestQuery } from './find_packs.gen'; +import { DefaultSuccessResponse } from '../model/schema/common_attributes.gen'; +import { CreatePacksRequestBody } from './create_pack.gen'; +import { ReadPacksRequestQuery } from './read_packs.gen'; +import { DeletePacksRequestQuery } from './delete_packs.gen'; +import { UpdatePacksRequestBody, UpdatePacksRequestParams } from './update_packs.gen'; + +export type OsqueryCreatePacksRequestBody = z.infer; +export const OsqueryCreatePacksRequestBody = CreatePacksRequestBody; +export type OsqueryCreatePacksRequestBodyInput = z.input; + +export type OsqueryCreatePacksResponse = z.infer; +export const OsqueryCreatePacksResponse = DefaultSuccessResponse; + +export type OsqueryDeletePacksRequestParams = z.infer; +export const OsqueryDeletePacksRequestParams = z.object({ + query: DeletePacksRequestQuery, +}); +export type OsqueryDeletePacksRequestParamsInput = z.input; + +export type OsqueryDeletePacksResponse = z.infer; +export const OsqueryDeletePacksResponse = DefaultSuccessResponse; +export type OsqueryFindPacksRequestQuery = z.infer; +export const OsqueryFindPacksRequestQuery = z.object({ + query: FindPacksRequestQuery, +}); +export type OsqueryFindPacksRequestQueryInput = z.input; + +export type OsqueryFindPacksResponse = z.infer; +export const OsqueryFindPacksResponse = DefaultSuccessResponse; + +export type OsqueryGetPacksDetailsRequestParams = z.infer< + typeof OsqueryGetPacksDetailsRequestParams +>; +export const OsqueryGetPacksDetailsRequestParams = z.object({ + query: ReadPacksRequestQuery, +}); +export type OsqueryGetPacksDetailsRequestParamsInput = z.input< + typeof OsqueryGetPacksDetailsRequestParams +>; + +export type OsqueryGetPacksDetailsResponse = z.infer; +export const OsqueryGetPacksDetailsResponse = DefaultSuccessResponse; + +export type OsqueryUpdatePacksRequestParams = z.infer; +export const OsqueryUpdatePacksRequestParams = z.object({ + query: UpdatePacksRequestParams, +}); +export type OsqueryUpdatePacksRequestParamsInput = z.input; + +export type OsqueryUpdatePacksRequestBody = z.infer; +export const OsqueryUpdatePacksRequestBody = UpdatePacksRequestBody; +export type OsqueryUpdatePacksRequestBodyInput = z.input; + +export type OsqueryUpdatePacksResponse = z.infer; +export const OsqueryUpdatePacksResponse = DefaultSuccessResponse; diff --git a/x-pack/plugins/osquery/common/api/packs/packs.schema.yaml b/x-pack/plugins/osquery/common/api/packs/packs.schema.yaml index 006e0ebd75286..bb2078e334649 100644 --- a/x-pack/plugins/osquery/common/api/packs/packs.schema.yaml +++ b/x-pack/plugins/osquery/common/api/packs/packs.schema.yaml @@ -6,17 +6,31 @@ paths: /api/osquery/packs: get: summary: Find packs + operationId: OsqueryFindPacks + x-codegen-enabled: true + x-labels: + - ess + - serverless parameters: - - $ref: './find_packs.schema.yaml#/components/parameters/FindPacksRequestQueryParameter' + - name: query + in: query + required: true + schema: + $ref: './find_packs.schema.yaml#/components/schemas/FindPacksRequestQuery' responses: '200': description: OK content: application/json: schema: - $ref: './find_packs.schema.yaml#/components/schemas/SuccessResponse' + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/DefaultSuccessResponse' post: summary: Create a packs + operationId: OsqueryCreatePacks + x-codegen-enabled: true + x-labels: + - ess + - serverless requestBody: required: true content: @@ -29,32 +43,55 @@ paths: content: application/json: schema: - $ref: './create_pack.schema.yaml#/components/schemas/SuccessResponse' + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/DefaultSuccessResponse' /api/osquery/packs/{id}: get: summary: Get packs details + operationId: OsqueryGetPacksDetails + x-codegen-enabled: true + x-labels: + - ess + - serverless parameters: - - $ref: './read_packs.schema.yaml#/components/parameters/ReadPacksRequestQueryParameter' + - name: query + in: path + required: true + schema: + $ref: './read_packs.schema.yaml#/components/schemas/ReadPacksRequestQuery' responses: '200': description: OK content: application/json: schema: - $ref: './read_packs.schema.yaml#/components/schemas/SuccessResponse' + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/DefaultSuccessResponse' delete: summary: Delete packs + operationId: OsqueryDeletePacks + x-codegen-enabled: true + x-labels: + - ess + - serverless parameters: - - $ref: './delete_packs.schema.yaml#/components/parameters/DeletePacksRequestQueryParameter' + - name: query + in: path + required: true + schema: + $ref: './delete_packs.schema.yaml#/components/schemas/DeletePacksRequestQuery' responses: '200': description: OK content: application/json: schema: - $ref: './find_packs.schema.yaml#/components/schemas/SuccessResponse' + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/DefaultSuccessResponse' put: summary: Update packs + operationId: OsqueryUpdatePacks + x-codegen-enabled: true + x-labels: + - ess + - serverless requestBody: required: true content: @@ -62,11 +99,15 @@ paths: schema: $ref: './update_packs.schema.yaml#/components/schemas/UpdatePacksRequestBody' parameters: - - $ref: './update_packs.schema.yaml#/components/parameters/UpdatePacksRequestQueryParameter' + - name: query + in: path + required: true + schema: + $ref: './update_packs.schema.yaml#/components/schemas/UpdatePacksRequestParams' responses: '200': description: OK content: application/json: schema: - $ref: './update_packs.schema.yaml#/components/schemas/SuccessResponse' + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/DefaultSuccessResponse' diff --git a/x-pack/plugins/osquery/common/api/packs/read_packs.gen.ts b/x-pack/plugins/osquery/common/api/packs/read_packs.gen.ts index bd125eb3acf05..2a315b1604db0 100644 --- a/x-pack/plugins/osquery/common/api/packs/read_packs.gen.ts +++ b/x-pack/plugins/osquery/common/api/packs/read_packs.gen.ts @@ -22,6 +22,3 @@ export type ReadPacksRequestQuery = z.infer; export const ReadPacksRequestQuery = z.object({ id: PackId.optional(), }); - -export type SuccessResponse = z.infer; -export const SuccessResponse = z.object({}); diff --git a/x-pack/plugins/osquery/common/api/packs/read_packs.schema.yaml b/x-pack/plugins/osquery/common/api/packs/read_packs.schema.yaml index 8cfe415848c92..de068fca1259e 100644 --- a/x-pack/plugins/osquery/common/api/packs/read_packs.schema.yaml +++ b/x-pack/plugins/osquery/common/api/packs/read_packs.schema.yaml @@ -4,20 +4,9 @@ info: version: '2023-10-31' paths: { } components: - parameters: - ReadPacksRequestQueryParameter: - name: query - in: path - required: true - schema: - $ref: '#/components/schemas/ReadPacksRequestQuery' schemas: ReadPacksRequestQuery: type: object properties: id: $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/PackId' - SuccessResponse: - type: object - properties: {} - # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/packs/update_packs.gen.ts b/x-pack/plugins/osquery/common/api/packs/update_packs.gen.ts index 92321233cd0d0..de75ec2e3ebc2 100644 --- a/x-pack/plugins/osquery/common/api/packs/update_packs.gen.ts +++ b/x-pack/plugins/osquery/common/api/packs/update_packs.gen.ts @@ -39,6 +39,3 @@ export const UpdatePacksRequestBody = z.object({ shards: Shards.optional(), queries: ObjectQueries.optional(), }); - -export type SuccessResponse = z.infer; -export const SuccessResponse = z.object({}); diff --git a/x-pack/plugins/osquery/common/api/packs/update_packs.schema.yaml b/x-pack/plugins/osquery/common/api/packs/update_packs.schema.yaml index 0b0510b4773ab..5a10ec7a73da4 100644 --- a/x-pack/plugins/osquery/common/api/packs/update_packs.schema.yaml +++ b/x-pack/plugins/osquery/common/api/packs/update_packs.schema.yaml @@ -4,13 +4,6 @@ info: version: '2023-10-31' paths: { } components: - parameters: - UpdatePacksRequestQueryParameter: - name: query - in: path - required: true - schema: - $ref: '#/components/schemas/UpdatePacksRequestParams' schemas: UpdatePacksRequestParams: type: object @@ -32,7 +25,3 @@ components: $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/Shards' queries: $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/ObjectQueries' - SuccessResponse: - type: object - properties: {} - # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/saved_query/delete_saved_query.gen.ts b/x-pack/plugins/osquery/common/api/saved_query/delete_saved_query.gen.ts index da550104fb6f1..daaee3e18c65c 100644 --- a/x-pack/plugins/osquery/common/api/saved_query/delete_saved_query.gen.ts +++ b/x-pack/plugins/osquery/common/api/saved_query/delete_saved_query.gen.ts @@ -22,6 +22,3 @@ export type DeleteSavedQueryRequestQuery = z.infer; -export const SuccessResponse = z.object({}); diff --git a/x-pack/plugins/osquery/common/api/saved_query/delete_saved_query.schema.yaml b/x-pack/plugins/osquery/common/api/saved_query/delete_saved_query.schema.yaml index 7a180c542aa23..7c301cee02242 100644 --- a/x-pack/plugins/osquery/common/api/saved_query/delete_saved_query.schema.yaml +++ b/x-pack/plugins/osquery/common/api/saved_query/delete_saved_query.schema.yaml @@ -4,20 +4,9 @@ info: version: '2023-10-31' paths: { } components: - parameters: - DeleteSavedQueryRequestQueryParameter: - name: query - in: path - required: true - schema: - $ref: '#/components/schemas/DeleteSavedQueryRequestQuery' schemas: DeleteSavedQueryRequestQuery: type: object properties: id: $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/SavedQueryId' - SuccessResponse: - type: object - properties: {} - # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/saved_query/find_saved_query.gen.ts b/x-pack/plugins/osquery/common/api/saved_query/find_saved_query.gen.ts index 0c875aa8fa187..2773d1f5e210d 100644 --- a/x-pack/plugins/osquery/common/api/saved_query/find_saved_query.gen.ts +++ b/x-pack/plugins/osquery/common/api/saved_query/find_saved_query.gen.ts @@ -30,6 +30,3 @@ export const FindSavedQueryRequestQuery = z.object({ sort: SortOrUndefined.optional(), sortOrder: SortOrderOrUndefined.optional(), }); - -export type SuccessResponse = z.infer; -export const SuccessResponse = z.object({}); diff --git a/x-pack/plugins/osquery/common/api/saved_query/find_saved_query.schema.yaml b/x-pack/plugins/osquery/common/api/saved_query/find_saved_query.schema.yaml index dbebf003a4696..ba11befc7af28 100644 --- a/x-pack/plugins/osquery/common/api/saved_query/find_saved_query.schema.yaml +++ b/x-pack/plugins/osquery/common/api/saved_query/find_saved_query.schema.yaml @@ -4,13 +4,6 @@ info: version: '2023-10-31' paths: { } components: - parameters: - FindSavedQueryRequestQueryParameter: - name: query - in: query - required: true - schema: - $ref: '#/components/schemas/FindSavedQueryRequestQuery' schemas: FindSavedQueryRequestQuery: type: object @@ -23,7 +16,3 @@ components: $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/SortOrUndefined' sortOrder: $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/SortOrderOrUndefined' - SuccessResponse: - type: object - properties: {} - # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/saved_query/read_saved_query.gen.ts b/x-pack/plugins/osquery/common/api/saved_query/read_saved_query.gen.ts index 33708f3fc4f3c..d397135a87286 100644 --- a/x-pack/plugins/osquery/common/api/saved_query/read_saved_query.gen.ts +++ b/x-pack/plugins/osquery/common/api/saved_query/read_saved_query.gen.ts @@ -22,6 +22,3 @@ export type ReadSavedQueryRequestQuery = z.infer; -export const SuccessResponse = z.object({}); diff --git a/x-pack/plugins/osquery/common/api/saved_query/read_saved_query.schema.yaml b/x-pack/plugins/osquery/common/api/saved_query/read_saved_query.schema.yaml index a5fed00a37e0c..7f2db7145e673 100644 --- a/x-pack/plugins/osquery/common/api/saved_query/read_saved_query.schema.yaml +++ b/x-pack/plugins/osquery/common/api/saved_query/read_saved_query.schema.yaml @@ -4,20 +4,9 @@ info: version: '2023-10-31' paths: { } components: - parameters: - ReadSavedQueryRequestQueryParameter: - name: query - in: path - required: true - schema: - $ref: '#/components/schemas/ReadSavedQueryRequestQuery' schemas: ReadSavedQueryRequestQuery: type: object properties: id: $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/SavedQueryId' - SuccessResponse: - type: object - properties: {} - # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/saved_query/saved_query.gen.ts b/x-pack/plugins/osquery/common/api/saved_query/saved_query.gen.ts new file mode 100644 index 0000000000000..033dab65762dd --- /dev/null +++ b/x-pack/plugins/osquery/common/api/saved_query/saved_query.gen.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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Saved Queries Schema + * version: 2023-10-31 + */ + +import { z } from 'zod'; + +import { FindSavedQueryRequestQuery } from './find_saved_query.gen'; +import { DefaultSuccessResponse } from '../model/schema/common_attributes.gen'; +import { CreateSavedQueryRequestBody } from './create_saved_query.gen'; +import { ReadSavedQueryRequestQuery } from './read_saved_query.gen'; +import { DeleteSavedQueryRequestQuery } from './delete_saved_query.gen'; +import { + UpdateSavedQueryRequestBody, + UpdateSavedQueryRequestParams, +} from './update_saved_query.gen'; + +export type OsqueryCreateSavedQueryRequestBody = z.infer; +export const OsqueryCreateSavedQueryRequestBody = CreateSavedQueryRequestBody; +export type OsqueryCreateSavedQueryRequestBodyInput = z.input< + typeof OsqueryCreateSavedQueryRequestBody +>; + +export type OsqueryCreateSavedQueryResponse = z.infer; +export const OsqueryCreateSavedQueryResponse = DefaultSuccessResponse; + +export type OsqueryDeleteSavedQueryRequestParams = z.infer< + typeof OsqueryDeleteSavedQueryRequestParams +>; +export const OsqueryDeleteSavedQueryRequestParams = z.object({ + query: DeleteSavedQueryRequestQuery, +}); +export type OsqueryDeleteSavedQueryRequestParamsInput = z.input< + typeof OsqueryDeleteSavedQueryRequestParams +>; + +export type OsqueryDeleteSavedQueryResponse = z.infer; +export const OsqueryDeleteSavedQueryResponse = DefaultSuccessResponse; +export type OsqueryFindSavedQueriesRequestQuery = z.infer< + typeof OsqueryFindSavedQueriesRequestQuery +>; +export const OsqueryFindSavedQueriesRequestQuery = z.object({ + query: FindSavedQueryRequestQuery, +}); +export type OsqueryFindSavedQueriesRequestQueryInput = z.input< + typeof OsqueryFindSavedQueriesRequestQuery +>; + +export type OsqueryFindSavedQueriesResponse = z.infer; +export const OsqueryFindSavedQueriesResponse = DefaultSuccessResponse; + +export type OsqueryGetSavedQueryDetailsRequestParams = z.infer< + typeof OsqueryGetSavedQueryDetailsRequestParams +>; +export const OsqueryGetSavedQueryDetailsRequestParams = z.object({ + query: ReadSavedQueryRequestQuery, +}); +export type OsqueryGetSavedQueryDetailsRequestParamsInput = z.input< + typeof OsqueryGetSavedQueryDetailsRequestParams +>; + +export type OsqueryGetSavedQueryDetailsResponse = z.infer< + typeof OsqueryGetSavedQueryDetailsResponse +>; +export const OsqueryGetSavedQueryDetailsResponse = DefaultSuccessResponse; + +export type OsqueryUpdateSavedQueryRequestParams = z.infer< + typeof OsqueryUpdateSavedQueryRequestParams +>; +export const OsqueryUpdateSavedQueryRequestParams = z.object({ + query: UpdateSavedQueryRequestParams, +}); +export type OsqueryUpdateSavedQueryRequestParamsInput = z.input< + typeof OsqueryUpdateSavedQueryRequestParams +>; + +export type OsqueryUpdateSavedQueryRequestBody = z.infer; +export const OsqueryUpdateSavedQueryRequestBody = UpdateSavedQueryRequestBody; +export type OsqueryUpdateSavedQueryRequestBodyInput = z.input< + typeof OsqueryUpdateSavedQueryRequestBody +>; + +export type OsqueryUpdateSavedQueryResponse = z.infer; +export const OsqueryUpdateSavedQueryResponse = DefaultSuccessResponse; diff --git a/x-pack/plugins/osquery/common/api/saved_query/saved_query.schema.yaml b/x-pack/plugins/osquery/common/api/saved_query/saved_query.schema.yaml index d8cef82ac7103..62ae77a83a94b 100644 --- a/x-pack/plugins/osquery/common/api/saved_query/saved_query.schema.yaml +++ b/x-pack/plugins/osquery/common/api/saved_query/saved_query.schema.yaml @@ -6,17 +6,31 @@ paths: /api/osquery/saved_queries: get: summary: Find saved queries + operationId: OsqueryFindSavedQueries + x-codegen-enabled: true + x-labels: + - ess + - serverless parameters: - - $ref: './find_saved_query.schema.yaml#/components/parameters/FindSavedQueryRequestQueryParameter' + - name: query + in: query + required: true + schema: + $ref: './find_saved_query.schema.yaml#/components/schemas/FindSavedQueryRequestQuery' responses: '200': description: OK content: application/json: schema: - $ref: './find_saved_query.schema.yaml#/components/schemas/SuccessResponse' + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/DefaultSuccessResponse' post: summary: Create a saved query + operationId: OsqueryCreateSavedQuery + x-codegen-enabled: true + x-labels: + - ess + - serverless requestBody: required: true content: @@ -29,44 +43,71 @@ paths: content: application/json: schema: - $ref: './create_saved_query.schema.yaml#/components/schemas/SuccessResponse' + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/DefaultSuccessResponse' /api/osquery/saved_queries/{id}: get: summary: Get saved query details + operationId: OsqueryGetSavedQueryDetails + x-codegen-enabled: true + x-labels: + - ess + - serverless parameters: - - $ref: './read_saved_query.schema.yaml#/components/parameters/ReadSavedQueryRequestQueryParameter' + - name: query + in: path + required: true + schema: + $ref: './read_saved_query.schema.yaml#/components/schemas/ReadSavedQueryRequestQuery' responses: '200': description: OK content: application/json: schema: - $ref: './read_saved_query.schema.yaml#/components/schemas/SuccessResponse' + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/DefaultSuccessResponse' delete: - summary: Delete saved query - parameters: - - $ref: './delete_saved_query.schema.yaml#/components/parameters/DeleteSavedQueryRequestQueryParameter' - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: './find_saved_query.schema.yaml#/components/schemas/SuccessResponse' + summary: Delete saved query + operationId: OsqueryDeleteSavedQuery + x-codegen-enabled: true + x-labels: + - ess + - serverless + parameters: + - name: query + in: path + required: true + schema: + $ref: './delete_saved_query.schema.yaml#/components/schemas/DeleteSavedQueryRequestQuery' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/DefaultSuccessResponse' put: - summary: Update saved query - requestBody: - required: true - content: - application/json: - schema: - $ref: './update_saved_query.schema.yaml#/components/schemas/UpdateSavedQueryRequestBody' - parameters: - - $ref: './update_saved_query.schema.yaml#/components/parameters/UpdateSavedQueryRequestQueryParameter' - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: './update_saved_query.schema.yaml#/components/schemas/SuccessResponse' + summary: Update saved query + operationId: OsqueryUpdateSavedQuery + x-codegen-enabled: true + x-labels: + - ess + - serverless + requestBody: + required: true + content: + application/json: + schema: + $ref: './update_saved_query.schema.yaml#/components/schemas/UpdateSavedQueryRequestBody' + parameters: + - name: query + in: path + required: true + schema: + $ref: './update_saved_query.schema.yaml#/components/schemas/UpdateSavedQueryRequestParams' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/DefaultSuccessResponse' diff --git a/x-pack/plugins/osquery/common/api/saved_query/update_saved_query.gen.ts b/x-pack/plugins/osquery/common/api/saved_query/update_saved_query.gen.ts index 70417f2880de2..b2e5a48c4aaca 100644 --- a/x-pack/plugins/osquery/common/api/saved_query/update_saved_query.gen.ts +++ b/x-pack/plugins/osquery/common/api/saved_query/update_saved_query.gen.ts @@ -44,6 +44,3 @@ export const UpdateSavedQueryRequestBody = z.object({ snapshot: SnapshotOrUndefined.optional(), removed: RemovedOrUndefined.optional(), }); - -export type SuccessResponse = z.infer; -export const SuccessResponse = z.object({}); diff --git a/x-pack/plugins/osquery/common/api/saved_query/update_saved_query.schema.yaml b/x-pack/plugins/osquery/common/api/saved_query/update_saved_query.schema.yaml index b91359b5bbeef..9f33e8bfda83d 100644 --- a/x-pack/plugins/osquery/common/api/saved_query/update_saved_query.schema.yaml +++ b/x-pack/plugins/osquery/common/api/saved_query/update_saved_query.schema.yaml @@ -4,13 +4,6 @@ info: version: '2023-10-31' paths: { } components: - parameters: - UpdateSavedQueryRequestQueryParameter: - name: query - in: path - required: true - schema: - $ref: '#/components/schemas/UpdateSavedQueryRequestParams' schemas: UpdateSavedQueryRequestParams: type: object @@ -38,7 +31,3 @@ components: $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/SnapshotOrUndefined' removed: $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/RemovedOrUndefined' - SuccessResponse: - type: object - properties: {} - # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/cypress/e2e/all/add_integration.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/add_integration.cy.ts index 6b847fb396967..005747c79c7fe 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/add_integration.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/add_integration.cy.ts @@ -39,7 +39,8 @@ import { } from '../../tasks/integrations'; import { ServerlessRoleName } from '../../support/roles'; -describe('ALL - Add Integration', { tags: ['@ess', '@serverless'] }, () => { +// Failing: See https://github.com/elastic/kibana/issues/170593 +describe.skip('ALL - Add Integration', { tags: ['@ess', '@serverless'] }, () => { let savedQueryId: string; before(() => { diff --git a/x-pack/plugins/osquery/cypress/e2e/all/packs_integration.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/packs_integration.cy.ts index ccbd119aab3a7..f940807345034 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/packs_integration.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/packs_integration.cy.ts @@ -40,7 +40,9 @@ import { cleanupPack, cleanupAgentPolicy } from '../../tasks/api_fixtures'; import { request } from '../../tasks/common'; import { ServerlessRoleName } from '../../support/roles'; -describe('ALL - Packs', { tags: ['@ess', '@serverless'] }, () => { +// Failing: See https://github.com/elastic/kibana/issues/171279 +// Failing: See https://github.com/elastic/kibana/issues/180424 +describe.skip('ALL - Packs', { tags: ['@ess', '@serverless'] }, () => { const integration = 'Osquery Manager'; describe( diff --git a/x-pack/plugins/search_inference_endpoints/common/translations.ts b/x-pack/plugins/search_inference_endpoints/common/translations.ts index e58829812829b..8171b8bba0254 100644 --- a/x-pack/plugins/search_inference_endpoints/common/translations.ts +++ b/x-pack/plugins/search_inference_endpoints/common/translations.ts @@ -14,6 +14,10 @@ export const INFERENCE_ENDPOINT_LABEL = i18n.translate( } ); +export const CANCEL = i18n.translate('xpack.searchInferenceEndpoints.cancel', { + defaultMessage: 'Cancel', +}); + export const MANAGE_INFERENCE_ENDPOINTS_LABEL = i18n.translate( 'xpack.searchInferenceEndpoints.allInferenceEndpoints.description', { @@ -94,3 +98,63 @@ export const FORBIDDEN_TO_ACCESS_TRAINED_MODELS = i18n.translate( defaultMessage: 'Forbidden to access trained models', } ); + +export const COPY_ID_ACTION_LABEL = i18n.translate( + 'xpack.searchInferenceEndpoints.actions.copyID', + { + defaultMessage: 'Copy endpoint ID', + } +); + +export const COPY_ID_ACTION_SUCCESS = i18n.translate( + 'xpack.searchInferenceEndpoints.actions.copyIDSuccess', + { + defaultMessage: 'Inference endpoint ID copied!', + } +); + +export const ENDPOINT_ADDED_SUCCESS = i18n.translate( + 'xpack.searchInferenceEndpoints.actions.endpointAddedSuccess', + { + defaultMessage: 'Endpoint added', + } +); + +export const ENDPOINT_CREATION_FAILED = i18n.translate( + 'xpack.searchInferenceEndpoints.actions.endpointAddedFailure', + { + defaultMessage: 'Endpoint creation failed', + } +); + +export const ENDPOINT_ADDED_SUCCESS_DESCRIPTION = (endpointId: string) => + i18n.translate('xpack.searchInferenceEndpoints.actions.endpointAddedSuccessDescription', { + defaultMessage: 'The inference endpoint "{endpointId}" was added.', + values: { endpointId }, + }); + +export const DELETE_ACTION_LABEL = i18n.translate( + 'xpack.searchInferenceEndpoints.actions.deleteSingleEndpoint', + { + defaultMessage: 'Delete endpoint', + } +); + +export const ENDPOINT = i18n.translate('xpack.searchInferenceEndpoints.endpoint', { + defaultMessage: 'Endpoint', +}); + +export const SERVICE_PROVIDER = i18n.translate('xpack.searchInferenceEndpoints.serviceProvider', { + defaultMessage: 'Service', +}); + +export const TASK_TYPE = i18n.translate('xpack.searchInferenceEndpoints.taskType', { + defaultMessage: 'Type', +}); + +export const TRAINED_MODELS_STAT_GATHER_FAILED = i18n.translate( + 'xpack.searchInferenceEndpoints.actions.trainedModelsStatGatherFailed', + { + defaultMessage: 'Failed to retrieve trained model statistics', + } +); diff --git a/x-pack/plugins/search_inference_endpoints/common/types.ts b/x-pack/plugins/search_inference_endpoints/common/types.ts index ef29d987309f8..e6529de4e3a64 100644 --- a/x-pack/plugins/search_inference_endpoints/common/types.ts +++ b/x-pack/plugins/search_inference_endpoints/common/types.ts @@ -7,6 +7,7 @@ export enum APIRoutes { GET_INFERENCE_ENDPOINTS = '/internal/inference_endpoints/endpoints', + DELETE_INFERENCE_ENDPOINT = '/internal/inference_endpoint/endpoints/{type}/{id}', } export interface SearchInferenceEndpointsConfigType { diff --git a/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/azure_ai_studio.svg b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/azure_ai_studio.svg new file mode 100644 index 0000000000000..405e182a10394 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/azure_ai_studio.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/azure_open_ai.svg b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/azure_open_ai.svg new file mode 100644 index 0000000000000..122c0c65af13c --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/azure_open_ai.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/cohere.svg b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/cohere.svg new file mode 100644 index 0000000000000..69953809fec35 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/cohere.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/elastic.svg b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/elastic.svg new file mode 100644 index 0000000000000..e763c2e2f2ab6 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/elastic.svg @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/google_ai_studio.svg b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/google_ai_studio.svg new file mode 100644 index 0000000000000..b6e34ae15c9e4 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/google_ai_studio.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/hugging_face.svg b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/hugging_face.svg new file mode 100644 index 0000000000000..87ac70c5a18f4 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/hugging_face.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/mistral.svg b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/mistral.svg new file mode 100644 index 0000000000000..f62258a327594 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/mistral.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/open_ai.svg b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/open_ai.svg new file mode 100644 index 0000000000000..9ddc8f8fd63b8 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/open_ai.svg @@ -0,0 +1,3 @@ + + + diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/constants.ts b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/constants.ts index 1b7e72149fd43..b3fd13dc5383a 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/constants.ts +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/constants.ts @@ -8,8 +8,9 @@ import { SortFieldInferenceEndpoint, QueryParams, - AlInferenceEndpointsTableState, + AllInferenceEndpointsTableState, SortOrder, + FilterOptions, } from './types'; export const DEFAULT_TABLE_ACTIVE_PAGE = 1; @@ -22,6 +23,12 @@ export const DEFAULT_QUERY_PARAMS: QueryParams = { sortOrder: SortOrder.asc, }; -export const DEFAULT_INFERENCE_ENDPOINTS_TABLE_STATE: AlInferenceEndpointsTableState = { +export const DEFAULT_FILTER_OPTIONS: FilterOptions = { + provider: [], + type: [], +}; + +export const DEFAULT_INFERENCE_ENDPOINTS_TABLE_STATE: AllInferenceEndpointsTableState = { + filterOptions: DEFAULT_FILTER_OPTIONS, queryParams: DEFAULT_QUERY_PARAMS, }; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/multi_select_filter.test.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/multi_select_filter.test.tsx new file mode 100644 index 0000000000000..87b984c26d3ea --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/multi_select_filter.test.tsx @@ -0,0 +1,66 @@ +/* + * 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 React from 'react'; +import { render, fireEvent, waitFor } from '@testing-library/react'; +import { MultiSelectFilter, MultiSelectFilterOption } from './multi_select_filter'; +import '@testing-library/jest-dom/extend-expect'; + +describe('MultiSelectFilter', () => { + const options: MultiSelectFilterOption[] = [ + { key: '1', label: 'Option 1', checked: 'off' }, + { key: '2', label: 'Option 2', checked: 'on' }, + { key: '3', label: 'Option 3', checked: 'off' }, + ]; + + it('should render the filter button with the provided label', () => { + const { getByText } = render( + {}} options={options} buttonLabel="Filter Options" /> + ); + expect(getByText('Filter Options')).toBeInTheDocument(); + }); + + it('should toggle the popover when the filter button is clicked', async () => { + const { getByText, queryByText } = render( + {}} options={options} buttonLabel="Filter Options" /> + ); + fireEvent.click(getByText('Filter Options')); + expect(queryByText('Option 1')).toBeInTheDocument(); + fireEvent.click(getByText('Filter Options')); + await waitFor(() => { + expect(queryByText('Option 1')).not.toBeInTheDocument(); + }); + }); + + it('should render the provided options', async () => { + const { getByText } = render( + {}} options={options} buttonLabel="Filter Options" /> + ); + + fireEvent.click(getByText('Filter Options')); + + await waitFor(() => { + expect(getByText('Option 1')).toBeInTheDocument(); + expect(getByText('Option 2')).toBeInTheDocument(); + expect(getByText('Option 3')).toBeInTheDocument(); + }); + }); + + it('should call the onChange function with the updated options when an option is clicked', async () => { + const onChange = jest.fn(); + const { getByText } = render( + + ); + + fireEvent.click(getByText('Filter Options')); + fireEvent.click(getByText('Option 1')); + + await waitFor(() => { + expect(onChange).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/multi_select_filter.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/multi_select_filter.tsx new file mode 100644 index 0000000000000..84883c4e85432 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/multi_select_filter.tsx @@ -0,0 +1,108 @@ +/* + * 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 { + EuiFilterButton, + EuiFilterGroup, + EuiPopover, + EuiPopoverTitle, + EuiSelectable, + EuiSpacer, + EuiText, + EuiTextColor, + useEuiTheme, +} from '@elastic/eui'; +import { css } from '@emotion/react'; +import React, { useState } from 'react'; +import * as i18n from './translations'; + +export interface MultiSelectFilterOption { + key: string; + label: string; + checked?: 'on' | 'off'; +} + +interface UseFilterParams { + buttonLabel?: string; + onChange: (newOptions: MultiSelectFilterOption[]) => void; + options: MultiSelectFilterOption[]; + renderOption?: (option: MultiSelectFilterOption) => React.ReactNode; + selectedOptionKeys?: string[]; +} + +export const MultiSelectFilter: React.FC = ({ + buttonLabel, + onChange, + options: rawOptions, + selectedOptionKeys = [], + renderOption, +}) => { + const { euiTheme } = useEuiTheme(); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const toggleIsPopoverOpen = () => setIsPopoverOpen((prevValue) => !prevValue); + const options: MultiSelectFilterOption[] = rawOptions.map(({ key, label }) => ({ + label, + key, + checked: selectedOptionKeys.includes(key) ? 'on' : undefined, + })); + + return ( + + 0} + numActiveFilters={selectedOptionKeys.length} + aria-label={buttonLabel} + > + + {buttonLabel} + + + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + panelPaddingSize="none" + repositionOnScroll + > + + {(list, search) => ( +
+ {search} +
+ {i18n.OPTIONS(options.length)} +
+ + {list} +
+ )} +
+
+
+ ); +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/service_provider_filter.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/service_provider_filter.tsx new file mode 100644 index 0000000000000..3d7f9568428ef --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/service_provider_filter.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { SERVICE_PROVIDERS } from '../render_table_columns/render_service_provider/service_provider'; +import type { FilterOptions, ServiceProviderKeys } from '../types'; +import { MultiSelectFilter, MultiSelectFilterOption } from './multi_select_filter'; +import * as i18n from './translations'; + +interface Props { + optionKeys: ServiceProviderKeys[]; + onChange: (newFilterOptions: Partial) => void; +} + +const options = Object.entries(SERVICE_PROVIDERS).map(([key, { name }]) => ({ + key, + label: name, +})); + +export const ServiceProviderFilter: React.FC = ({ optionKeys, onChange }) => { + const filterId: string = 'provider'; + const onSystemFilterChange = (newOptions: MultiSelectFilterOption[]) => { + onChange({ + [filterId]: newOptions + .filter((option) => option.checked === 'on') + .map((option) => option.key), + }); + }; + + return ( + option.label} + selectedOptionKeys={optionKeys} + /> + ); +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/task_type_filter.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/task_type_filter.tsx new file mode 100644 index 0000000000000..389757b833db6 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/task_type_filter.tsx @@ -0,0 +1,42 @@ +/* + * 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 React from 'react'; +import { FilterOptions, TaskTypes } from '../types'; +import { MultiSelectFilter, MultiSelectFilterOption } from './multi_select_filter'; +import * as i18n from './translations'; + +interface Props { + optionKeys: TaskTypes[]; + onChange: (newFilterOptions: Partial) => void; +} + +const options = Object.values(TaskTypes).map((option) => ({ + key: option, + label: option, +})); + +export const TaskTypeFilter: React.FC = ({ optionKeys, onChange }) => { + const filterId: string = 'type'; + const onSystemFilterChange = (newOptions: MultiSelectFilterOption[]) => { + onChange({ + [filterId]: newOptions + .filter((option) => option.checked === 'on') + .map((option) => option.key), + }); + }; + + return ( + option.label} + selectedOptionKeys={optionKeys} + /> + ); +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/translations.ts b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/translations.ts new file mode 100644 index 0000000000000..f373e8f5d5a46 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/translations.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export { SERVICE_PROVIDER, TASK_TYPE } from '../../../../common/translations'; + +export const EMPTY_FILTER_MESSAGE = i18n.translate( + 'xpack.searchInferenceEndpoints.filter.emptyMessage', + { + defaultMessage: 'No options', + } +); +export const OPTIONS = (totalCount: number) => + i18n.translate('xpack.searchInferenceEndpoints.filter.options', { + defaultMessage: '{totalCount, plural, one {# option} other {# options}}', + values: { totalCount }, + }); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/copy_id/use_copy_id_action.test.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/copy_id/use_copy_id_action.test.tsx new file mode 100644 index 0000000000000..1445e0c41c574 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/copy_id/use_copy_id_action.test.tsx @@ -0,0 +1,73 @@ +/* + * 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 { renderReactTestingLibraryWithI18n as render } from '@kbn/test-jest-helpers'; +import React from 'react'; +import { useKibana } from '../../../../../../hooks/use_kibana'; +import { useCopyIDAction } from './use_copy_id_action'; + +const mockInferenceEndpoint = { + deployment: 'not_applicable', + endpoint: { + model_id: 'hugging-face-embeddings', + task_type: 'text_embedding', + service: 'hugging_face', + service_settings: { + dimensions: 768, + rate_limit: { + requests_per_minute: 3000, + }, + }, + task_settings: {}, + }, + provider: 'hugging_face', + type: 'text_embedding', +} as any; + +Object.defineProperty(navigator, 'clipboard', { + value: { + writeText: jest.fn().mockResolvedValue(undefined), + }, + configurable: true, +}); + +const mockOnActionSuccess = jest.fn(); + +jest.mock('../../../../../../hooks/use_kibana', () => ({ + useKibana: jest.fn(), +})); + +const addSuccess = jest.fn(); + +(useKibana as jest.Mock).mockImplementation(() => ({ + services: { + notifications: { + toasts: { + addSuccess, + }, + }, + }, +})); + +describe('useCopyIDAction hook', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders the label with correct text', () => { + const TestComponent = () => { + const { getAction } = useCopyIDAction({ onActionSuccess: mockOnActionSuccess }); + const action = getAction(mockInferenceEndpoint); + return
{action}
; + }; + + const { getByTestId } = render(); + const labelElement = getByTestId('inference-endpoints-action-copy-id-label'); + + expect(labelElement).toHaveTextContent('Copy endpoint ID'); + }); +}); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/copy_id/use_copy_id_action.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/copy_id/use_copy_id_action.tsx new file mode 100644 index 0000000000000..b43b308af068a --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/copy_id/use_copy_id_action.tsx @@ -0,0 +1,44 @@ +/* + * 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 { EuiContextMenuItem, EuiCopy, EuiIcon } from '@elastic/eui'; +import React from 'react'; +import * as i18n from '../../../../../../../common/translations'; +import { useKibana } from '../../../../../../hooks/use_kibana'; +import { InferenceEndpointUI } from '../../../../types'; +import { UseCopyIDActionProps } from '../types'; + +export const useCopyIDAction = ({ onActionSuccess }: UseCopyIDActionProps) => { + const { + services: { notifications }, + } = useKibana(); + const toasts = notifications?.toasts; + + const getAction = (inferenceEndpoint: InferenceEndpointUI) => { + return ( + + {(copy) => ( + } + onClick={() => { + copy(); + onActionSuccess(); + toasts?.addSuccess({ title: i18n.COPY_ID_ACTION_SUCCESS }); + }} + size="s" + > + {i18n.COPY_ID_ACTION_LABEL} + + )} + + ); + }; + + return { getAction }; +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/index.test.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/index.test.tsx new file mode 100644 index 0000000000000..e6f4594e2d0cd --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/index.test.tsx @@ -0,0 +1,41 @@ +/* + * 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 { render, fireEvent, screen } from '@testing-library/react'; +import React from 'react'; +import { ConfirmDeleteEndpointModal } from '.'; +import * as i18n from './translations'; + +describe('ConfirmDeleteEndpointModal', () => { + const mockOnCancel = jest.fn(); + const mockOnConfirm = jest.fn(); + + beforeEach(() => { + render(); + }); + + it('renders the modal with correct texts', () => { + expect(screen.getByText(i18n.DELETE_TITLE)).toBeInTheDocument(); + expect(screen.getByText(i18n.CONFIRM_DELETE_WARNING)).toBeInTheDocument(); + expect(screen.getByText(i18n.CANCEL)).toBeInTheDocument(); + expect(screen.getByText(i18n.DELETE_ACTION_LABEL)).toBeInTheDocument(); + }); + + it('calls onCancel when the cancel button is clicked', () => { + fireEvent.click(screen.getByText(i18n.CANCEL)); + expect(mockOnCancel).toHaveBeenCalled(); + }); + + it('calls onConfirm when the delete button is clicked', () => { + fireEvent.click(screen.getByText(i18n.DELETE_ACTION_LABEL)); + expect(mockOnConfirm).toHaveBeenCalled(); + }); + + it('has the delete button focused by default', () => { + expect(document.activeElement).toHaveTextContent(i18n.DELETE_ACTION_LABEL); + }); +}); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/index.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/index.tsx new file mode 100644 index 0000000000000..e650192a66dc7 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/index.tsx @@ -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 React from 'react'; +import { EuiConfirmModal } from '@elastic/eui'; +import * as i18n from './translations'; + +interface ConfirmDeleteEndpointModalProps { + onCancel: () => void; + onConfirm: () => void; +} + +export const ConfirmDeleteEndpointModal: React.FC = ({ + onCancel, + onConfirm, +}) => { + return ( + + {i18n.CONFIRM_DELETE_WARNING} + + ); +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/translations.ts b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/translations.ts new file mode 100644 index 0000000000000..6b2dff7d8b8f1 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/translations.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +export * from '../../../../../../../../common/translations'; + +export const DELETE_TITLE = i18n.translate( + 'xpack.searchInferenceEndpoints.confirmDeleteEndpoint.title', + { + defaultMessage: 'Delete inference endpoint', + } +); + +export const CONFIRM_DELETE_WARNING = i18n.translate( + 'xpack.searchInferenceEndpoints.confirmDeleteEndpoint.confirmQuestion', + { + defaultMessage: + 'Deleting an active endpoint will cause operations targeting associated semantic_text fields and inference pipelines to fail.', + } +); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/use_delete_action.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/use_delete_action.tsx new file mode 100644 index 0000000000000..1a2e5cdb5b51f --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/use_delete_action.tsx @@ -0,0 +1,55 @@ +/* + * 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 { EuiContextMenuItem, EuiIcon } from '@elastic/eui'; +import React, { useCallback, useState } from 'react'; +import * as i18n from '../../../../../../../common/translations'; +import { useDeleteEndpoint } from '../../../../../../hooks/use_delete_endpoint'; +import { InferenceEndpointUI } from '../../../../types'; +import type { UseActionProps } from '../types'; + +export const useDeleteAction = ({ onActionSuccess }: UseActionProps) => { + const [isModalVisible, setIsModalVisible] = useState(false); + const [endpointToBeDeleted, setEndpointToBeDeleted] = useState(null); + const onCloseModal = useCallback(() => setIsModalVisible(false), []); + const openModal = useCallback( + (selectedEndpoint: InferenceEndpointUI) => { + onActionSuccess(); + setIsModalVisible(true); + setEndpointToBeDeleted(selectedEndpoint); + }, + [onActionSuccess] + ); + + const { mutate: deleteEndpoint } = useDeleteEndpoint(); + + const onConfirmDeletion = useCallback(() => { + onCloseModal(); + if (!endpointToBeDeleted) { + return; + } + + deleteEndpoint({ + type: endpointToBeDeleted.type, + id: endpointToBeDeleted.endpoint.model_id, + }); + }, [deleteEndpoint, onCloseModal, endpointToBeDeleted]); + + const getAction = (selectedEndpoint: InferenceEndpointUI) => { + return ( + } + onClick={() => openModal(selectedEndpoint)} + > + {i18n.DELETE_ACTION_LABEL} + + ); + }; + + return { getAction, isModalVisible, onConfirmDeletion, onCloseModal }; +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/types.ts b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/types.ts new file mode 100644 index 0000000000000..a80ccae703b7a --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/types.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface UseActionProps { + onActionSuccess: () => void; +} + +export type UseCopyIDActionProps = Pick; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/use_actions.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/use_actions.tsx new file mode 100644 index 0000000000000..f76d147c68646 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/use_actions.tsx @@ -0,0 +1,79 @@ +/* + * 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 { EuiTableComputedColumnType } from '@elastic/eui'; +import { EuiButtonIcon, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; +import React, { useCallback, useState } from 'react'; +import { InferenceEndpointUI } from '../../types'; +import { useCopyIDAction } from './actions/copy_id/use_copy_id_action'; +import { ConfirmDeleteEndpointModal } from './actions/delete/confirm_delete_endpoint'; +import { useDeleteAction } from './actions/delete/use_delete_action'; + +export const ActionColumn: React.FC<{ interfaceEndpoint: InferenceEndpointUI }> = ({ + interfaceEndpoint, +}) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const tooglePopover = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]); + const closePopover = useCallback(() => setIsPopoverOpen(false), []); + + const copyIDAction = useCopyIDAction({ + onActionSuccess: closePopover, + }); + + const deleteAction = useDeleteAction({ + onActionSuccess: closePopover, + }); + + const items = [ + copyIDAction.getAction(interfaceEndpoint), + deleteAction.getAction(interfaceEndpoint), + ]; + + return ( + <> + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downLeft" + > + + + {deleteAction.isModalVisible ? ( + + ) : null} + + ); +}; + +interface UseBulkActionsReturnValue { + actions: EuiTableComputedColumnType; +} + +export const useActions = (): UseBulkActionsReturnValue => { + return { + actions: { + align: 'right', + render: (interfaceEndpoint: InferenceEndpointUI) => { + return ; + }, + width: '165px', + }, + }; +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_deployment_status/deployment_status.test.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_deployment_status/deployment_status.test.tsx new file mode 100644 index 0000000000000..59414cd6c90ef --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_deployment_status/deployment_status.test.tsx @@ -0,0 +1,27 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import React from 'react'; +import { DeploymentStatus } from './deployment_status'; +import { DeploymentStatusEnum } from '../../types'; + +describe('DeploymentStatus component', () => { + it.each([[DeploymentStatusEnum.deployed, DeploymentStatusEnum.notDeployed]])( + 'renders with %s status, expects %s color, and correct data-test-subj attribute', + (status) => { + render(); + const healthComponent = screen.getByTestId(`table-column-deployment-${status}`); + expect(healthComponent).toBeInTheDocument(); + } + ); + + it('does not render when status is notApplicable', () => { + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_deployment_status/deployment_status.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_deployment_status/deployment_status.tsx new file mode 100644 index 0000000000000..61e6e620d2485 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_deployment_status/deployment_status.tsx @@ -0,0 +1,52 @@ +/* + * 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 React from 'react'; +import { EuiIcon, EuiToolTip } from '@elastic/eui'; +import { DeploymentStatusEnum } from '../../types'; +import * as i18n from './translations'; + +interface DeploymentStatusProps { + status: DeploymentStatusEnum; +} + +export const DeploymentStatus: React.FC = ({ status }) => { + if (status === DeploymentStatusEnum.notApplicable) { + return null; + } + + let statusColor: string; + let type: string; + let tooltip: string; + + switch (status) { + case DeploymentStatusEnum.deployed: + statusColor = 'success'; + type = 'dot'; + tooltip = i18n.MODEL_DEPLOYED; + break; + case DeploymentStatusEnum.notDeployed: + statusColor = 'warning'; + type = 'warning'; + tooltip = i18n.MODEL_NOT_DEPLOYED; + break; + case DeploymentStatusEnum.notDeployable: + statusColor = 'danger'; + type = 'dot'; + tooltip = i18n.MODEL_FAILED_TO_BE_DEPLOYED; + } + + return ( + + + + ); +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_deployment_status/translations.ts b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_deployment_status/translations.ts new file mode 100644 index 0000000000000..9a5448f5bf0d2 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_deployment_status/translations.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const MODEL_DEPLOYED = i18n.translate( + 'xpack.searchInferenceEndpoints.deploymentStatus.tooltip.modelDeployed', + { + defaultMessage: 'Model is deployed', + } +); + +export const MODEL_NOT_DEPLOYED = i18n.translate( + 'xpack.searchInferenceEndpoints.deploymentStatus.tooltip.modelNotDeployed', + { + defaultMessage: 'Model is not deployed', + } +); + +export const MODEL_FAILED_TO_BE_DEPLOYED = i18n.translate( + 'xpack.searchInferenceEndpoints.deploymentStatus.tooltip.modelFailedToBeDeployed', + { + defaultMessage: 'Model can not be deployed', + } +); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_endpoint/endpoint_info.test.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_endpoint/endpoint_info.test.tsx new file mode 100644 index 0000000000000..c92c01240425c --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_endpoint/endpoint_info.test.tsx @@ -0,0 +1,257 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import React from 'react'; +import { EndpointInfo } from './endpoint_info'; + +describe('RenderEndpoint component tests', () => { + describe('with cohere service', () => { + const mockEndpoint = { + model_id: 'cohere-2', + service: 'cohere', + service_settings: { + similarity: 'cosine', + dimensions: 384, + model_id: 'embed-english-light-v3.0', + rate_limit: { + requests_per_minute: 10000, + }, + embedding_type: 'byte', + }, + task_settings: {}, + } as any; + + it('renders the component with endpoint details for Cohere service', () => { + render(); + + expect(screen.getByText('cohere-2')).toBeInTheDocument(); + expect(screen.getByText('byte')).toBeInTheDocument(); + expect(screen.getByText('embed-english-light-v3.0')).toBeInTheDocument(); + }); + + it('does not render model_id badge if serviceSettings.model_id is not provided for Cohere service', () => { + const modifiedEndpoint = { + ...mockEndpoint, + service_settings: { ...mockEndpoint.service_settings, model_id: undefined }, + }; + render(); + + expect(screen.queryByText('embed-english-light-v3.0')).not.toBeInTheDocument(); + }); + + it('renders only model_id if other settings are not provided for Cohere service', () => { + const modifiedEndpoint = { + ...mockEndpoint, + service_settings: { model_id: 'embed-english-light-v3.0' }, + }; + render(); + + expect(screen.getByText('embed-english-light-v3.0')).toBeInTheDocument(); + expect(screen.queryByText(',')).not.toBeInTheDocument(); + }); + }); + + describe('with elasticsearch service', () => { + const mockEndpoint = { + model_id: 'model-123', + service: 'elasticsearch', + service_settings: { + num_allocations: 5, + num_threads: 10, + model_id: 'settings-model-123', + }, + } as any; + + it('renders the component with endpoint model_id and model settings', () => { + render(); + + expect(screen.getByText('model-123')).toBeInTheDocument(); + expect(screen.getByText('settings-model-123')).toBeInTheDocument(); + expect(screen.getByText('Threads: 10 | Allocations: 5')).toBeInTheDocument(); + }); + + it('renders the component with only model_id if num_threads and num_allocations are not provided', () => { + const modifiedSettings = { + ...mockEndpoint.service_settings, + num_threads: undefined, + num_allocations: undefined, + }; + const modifiedEndpoint = { ...mockEndpoint, service_settings: modifiedSettings }; + render(); + + expect(screen.getByText('model-123')).toBeInTheDocument(); + expect(screen.getByText('settings-model-123')).toBeInTheDocument(); + expect(screen.queryByText('Threads: 10 | Allocations: 5')).not.toBeInTheDocument(); + }); + }); + + describe('with azureaistudio service', () => { + const mockEndpoint = { + model_id: 'azure-ai-1', + service: 'azureaistudio', + service_settings: { + target: 'westus', + provider: 'microsoft_phi', + endpoint_type: 'realtime', + }, + } as any; + + it('renders the component with endpoint details', () => { + render(); + + expect(screen.getByText('azure-ai-1')).toBeInTheDocument(); + expect(screen.getByText('microsoft_phi, realtime, westus')).toBeInTheDocument(); + }); + + it('renders correctly when some service settings are missing', () => { + const modifiedEndpoint = { + ...mockEndpoint, + service_settings: { target: 'westus', provider: 'microsoft_phi' }, + }; + render(); + + expect(screen.getByText('microsoft_phi, westus')).toBeInTheDocument(); + }); + + it('does not render a comma when only one service setting is provided', () => { + const modifiedEndpoint = { + ...mockEndpoint, + service_settings: { target: 'westus' }, + }; + render(); + + expect(screen.getByText('westus')).toBeInTheDocument(); + expect(screen.queryByText(',')).not.toBeInTheDocument(); + }); + + it('renders nothing related to service settings when all are missing', () => { + const modifiedEndpoint = { + ...mockEndpoint, + service_settings: {}, + }; + render(); + + expect(screen.getByText('azure-ai-1')).toBeInTheDocument(); + expect(screen.queryByText('westus')).not.toBeInTheDocument(); + expect(screen.queryByText('microsoft_phi')).not.toBeInTheDocument(); + expect(screen.queryByText('realtime')).not.toBeInTheDocument(); + }); + }); + + describe('with azureopenai service', () => { + const mockEndpoint = { + model_id: 'azure-openai-1', + service: 'azureopenai', + service_settings: { + resource_name: 'resource-xyz', + deployment_id: 'deployment-123', + api_version: 'v1', + }, + } as any; + + it('renders the component with all required endpoint details', () => { + render(); + + expect(screen.getByText('azure-openai-1')).toBeInTheDocument(); + expect(screen.getByText('resource-xyz, deployment-123, v1')).toBeInTheDocument(); + }); + }); + + describe('with mistral service', () => { + const mockEndpoint = { + model_id: 'mistral-ai-1', + service: 'mistral', + service_settings: { + model: 'model-xyz', + max_input_tokens: 512, + rate_limit: { + requests_per_minute: 1000, + }, + }, + } as any; + + it('renders the component with endpoint details', () => { + render(); + + expect(screen.getByText('mistral-ai-1')).toBeInTheDocument(); + expect(screen.getByText('model-xyz')).toBeInTheDocument(); + expect(screen.getByText('max_input_tokens: 512, rate_limit: 1000')).toBeInTheDocument(); + }); + + it('renders correctly when some service settings are missing', () => { + const modifiedEndpoint = { + ...mockEndpoint, + service_settings: { + model: 'model-xyz', + max_input_tokens: 512, + }, + }; + render(); + + expect(screen.getByText('max_input_tokens: 512')).toBeInTheDocument(); + }); + + it('does not render a comma when only one service setting is provided', () => { + const modifiedEndpoint = { + ...mockEndpoint, + service_settings: { model: 'model-xyz' }, + }; + render(); + + expect(screen.getByText('model-xyz')).toBeInTheDocument(); + expect(screen.queryByText(',')).not.toBeInTheDocument(); + }); + + it('renders nothing related to service settings when all are missing', () => { + const modifiedEndpoint = { + ...mockEndpoint, + service_settings: {}, + }; + render(); + + expect(screen.getByText('mistral-ai-1')).toBeInTheDocument(); + expect(screen.queryByText('model-xyz')).not.toBeInTheDocument(); + expect(screen.queryByText('max_input_tokens: 512')).not.toBeInTheDocument(); + expect(screen.queryByText('rate_limit: 1000')).not.toBeInTheDocument(); + }); + }); + + describe('with googleaistudio service', () => { + const mockEndpoint = { + model_id: 'google-ai-1', + service: 'googleaistudio', + service_settings: { + model_id: 'model-abc', + rate_limit: { + requests_per_minute: 500, + }, + }, + } as any; + + it('renders the component with endpoint details', () => { + render(); + + expect(screen.getByText('model-abc')).toBeInTheDocument(); + expect(screen.getByText('rate_limit: 500')).toBeInTheDocument(); + }); + + it('renders correctly when rate limit is missing', () => { + const modifiedEndpoint = { + ...mockEndpoint, + service_settings: { + model_id: 'model-abc', + }, + }; + + render(); + + expect(screen.getByText('model-abc')).toBeInTheDocument(); + expect(screen.queryByText('Rate limit:')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_endpoint/endpoint_info.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_endpoint/endpoint_info.tsx new file mode 100644 index 0000000000000..ae482b609346f --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_endpoint/endpoint_info.tsx @@ -0,0 +1,164 @@ +/* + * 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 React from 'react'; +import { InferenceAPIConfigResponse } from '@kbn/ml-trained-models-utils'; +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { ServiceProviderKeys } from '../../types'; +import { ModelBadge } from './model_badge'; +import * as i18n from './translations'; + +export interface EndpointInfoProps { + endpoint: InferenceAPIConfigResponse; +} + +export const EndpointInfo: React.FC = ({ endpoint }) => { + return ( + + + {endpoint.model_id} + + + + + + ); +}; + +export const EndpointModelInfo: React.FC = ({ endpoint }) => { + const serviceSettings = endpoint.service_settings; + const modelId = + 'model_id' in serviceSettings + ? serviceSettings.model_id + : 'model' in serviceSettings + ? serviceSettings.model + : undefined; + + return ( + + {modelId && ( + + + + )} + + + {endpointModelAtrributes(endpoint)} + + + + ); +}; + +function endpointModelAtrributes(endpoint: InferenceAPIConfigResponse) { + switch (endpoint.service) { + case ServiceProviderKeys.elser: + case ServiceProviderKeys.elasticsearch: + return elasticsearchAttributes(endpoint); + case ServiceProviderKeys.cohere: + return cohereAttributes(endpoint); + case ServiceProviderKeys.hugging_face: + return huggingFaceAttributes(endpoint); + case ServiceProviderKeys.openai: + return openAIAttributes(endpoint); + case ServiceProviderKeys.azureaistudio: + return azureOpenAIStudioAttributes(endpoint); + case ServiceProviderKeys.azureopenai: + return azureOpenAIAttributes(endpoint); + case ServiceProviderKeys.mistral: + return mistralAttributes(endpoint); + case ServiceProviderKeys.googleaistudio: + return googleAIStudioAttributes(endpoint); + default: + return null; + } +} + +function elasticsearchAttributes(endpoint: InferenceAPIConfigResponse) { + const serviceSettings = endpoint.service_settings; + + const numAllocations = + 'num_allocations' in serviceSettings ? serviceSettings.num_allocations : undefined; + const numThreads = 'num_threads' in serviceSettings ? serviceSettings.num_threads : undefined; + + return `${numThreads ? i18n.THREADS(numThreads) : ''}${ + numThreads && numAllocations ? ' | ' : '' + }${numAllocations ? i18n.ALLOCATIONS(numAllocations) : ''}`; +} + +function cohereAttributes(endpoint: InferenceAPIConfigResponse) { + const serviceSettings = endpoint.service_settings; + const embeddingType = + 'embedding_type' in serviceSettings ? serviceSettings.embedding_type : undefined; + + const taskSettings = endpoint.task_settings; + const inputType = 'input_type' in taskSettings ? taskSettings.input_type : undefined; + const truncate = 'truncate' in taskSettings ? taskSettings.truncate : undefined; + + return [embeddingType, inputType, truncate && `truncate: ${truncate}`].filter(Boolean).join(', '); +} + +function huggingFaceAttributes(endpoint: InferenceAPIConfigResponse) { + const serviceSettings = endpoint.service_settings; + const url = 'url' in serviceSettings ? serviceSettings.url : null; + + return url; +} + +function openAIAttributes(endpoint: InferenceAPIConfigResponse) { + const serviceSettings = endpoint.service_settings; + const url = 'url' in serviceSettings ? serviceSettings.url : null; + + return url; +} + +function azureOpenAIStudioAttributes(endpoint: InferenceAPIConfigResponse) { + const serviceSettings = endpoint.service_settings; + const provider = 'provider' in serviceSettings ? serviceSettings.provider : undefined; + const endpointType = + 'endpoint_type' in serviceSettings ? serviceSettings.endpoint_type : undefined; + const target = 'target' in serviceSettings ? serviceSettings.target : undefined; + + return [provider, endpointType, target].filter(Boolean).join(', '); +} + +function azureOpenAIAttributes(endpoint: InferenceAPIConfigResponse) { + const serviceSettings = endpoint.service_settings; + + const resourceName = + 'resource_name' in serviceSettings ? serviceSettings.resource_name : undefined; + const deploymentId = + 'deployment_id' in serviceSettings ? serviceSettings.deployment_id : undefined; + const apiVersion = 'api_version' in serviceSettings ? serviceSettings.api_version : undefined; + + return [resourceName, deploymentId, apiVersion].filter(Boolean).join(', '); +} + +function mistralAttributes(endpoint: InferenceAPIConfigResponse) { + const serviceSettings = endpoint.service_settings; + + const maxInputTokens = + 'max_input_tokens' in serviceSettings ? serviceSettings.max_input_tokens : undefined; + const rateLimit = + 'rate_limit' in serviceSettings ? serviceSettings.rate_limit.requests_per_minute : undefined; + + return [ + maxInputTokens && `max_input_tokens: ${maxInputTokens}`, + rateLimit && `rate_limit: ${rateLimit}`, + ] + .filter(Boolean) + .join(', '); +} + +function googleAIStudioAttributes(endpoint: InferenceAPIConfigResponse) { + const serviceSettings = endpoint.service_settings; + + const rateLimit = + 'rate_limit' in serviceSettings ? serviceSettings.rate_limit.requests_per_minute : undefined; + + return rateLimit && `rate_limit: ${rateLimit}`; +} diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_endpoint/model_badge.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_endpoint/model_badge.tsx new file mode 100644 index 0000000000000..e4b241abd8199 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_endpoint/model_badge.tsx @@ -0,0 +1,21 @@ +/* + * 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 React from 'react'; +import { EuiBadge, useEuiTheme } from '@elastic/eui'; + +interface ModelBadgeProps { + model?: string; +} + +export const ModelBadge: React.FC = ({ model }) => { + const { euiTheme } = useEuiTheme(); + + if (!model) return null; + + return {model}; +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_endpoint/translations.ts b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_endpoint/translations.ts new file mode 100644 index 0000000000000..52705999e8b44 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_endpoint/translations.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const THREADS = (numThreads: number) => + i18n.translate('xpack.searchInferenceEndpoints.elasticsearch.threads', { + defaultMessage: 'Threads: {numThreads}', + values: { numThreads }, + }); + +export const ALLOCATIONS = (numAllocations: number) => + i18n.translate('xpack.searchInferenceEndpoints.elasticsearch.allocations', { + defaultMessage: 'Allocations: {numAllocations}', + values: { numAllocations }, + }); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_service_provider/service_provider.test.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_service_provider/service_provider.test.tsx new file mode 100644 index 0000000000000..a592569abb0aa --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_service_provider/service_provider.test.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { ServiceProvider } from './service_provider'; +import { ServiceProviderKeys } from '../../types'; + +jest.mock('../../../../assets/images/providers/elastic.svg', () => 'elasticIcon.svg'); +jest.mock('../../../../assets/images/providers/hugging_face.svg', () => 'huggingFaceIcon.svg'); +jest.mock('../../../../assets/images/providers/cohere.svg', () => 'cohereIcon.svg'); +jest.mock('../../../../assets/images/providers/open_ai.svg', () => 'openAIIcon.svg'); + +describe('ServiceProvider component', () => { + it('renders Hugging Face icon and name when providerKey is hugging_face', () => { + render(); + expect(screen.getByText('Hugging Face')).toBeInTheDocument(); + const icon = screen.getByTestId('table-column-service-provider-hugging_face'); + expect(icon).toBeInTheDocument(); + }); + + it('renders Open AI icon and name when providerKey is openai', () => { + render(); + expect(screen.getByText('OpenAI')).toBeInTheDocument(); + const icon = screen.getByTestId('table-column-service-provider-openai'); + expect(icon).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_service_provider/service_provider.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_service_provider/service_provider.tsx new file mode 100644 index 0000000000000..c4f09213158ce --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_service_provider/service_provider.tsx @@ -0,0 +1,83 @@ +/* + * 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 { EuiIcon } from '@elastic/eui'; +import React from 'react'; +import elasticIcon from '../../../../assets/images/providers/elastic.svg'; +import huggingFaceIcon from '../../../../assets/images/providers/hugging_face.svg'; +import cohereIcon from '../../../../assets/images/providers/cohere.svg'; +import openAIIcon from '../../../../assets/images/providers/open_ai.svg'; +import azureAIStudioIcon from '../../../../assets/images/providers/azure_ai_studio.svg'; +import azureOpenAIIcon from '../../../../assets/images/providers/azure_open_ai.svg'; +import googleAIStudioIcon from '../../../../assets/images/providers/google_ai_studio.svg'; +import mistralIcon from '../../../../assets/images/providers/mistral.svg'; +import { ServiceProviderKeys } from '../../types'; + +interface ServiceProviderProps { + providerKey: ServiceProviderKeys; +} + +interface ServiceProviderRecord { + icon: string; + name: string; +} + +export const SERVICE_PROVIDERS: Record = { + [ServiceProviderKeys.azureaistudio]: { + icon: azureAIStudioIcon, + name: 'Azure AI Studio', + }, + [ServiceProviderKeys.azureopenai]: { + icon: azureOpenAIIcon, + name: 'Azure OpenAI', + }, + [ServiceProviderKeys.cohere]: { + icon: cohereIcon, + name: 'Cohere', + }, + [ServiceProviderKeys.elasticsearch]: { + icon: elasticIcon, + name: 'Elasticsearch', + }, + [ServiceProviderKeys.elser]: { + icon: elasticIcon, + name: 'ELSER', + }, + [ServiceProviderKeys.googleaistudio]: { + icon: googleAIStudioIcon, + name: 'Google AI Studio', + }, + [ServiceProviderKeys.hugging_face]: { + icon: huggingFaceIcon, + name: 'Hugging Face', + }, + [ServiceProviderKeys.mistral]: { + icon: mistralIcon, + name: 'Mistral', + }, + [ServiceProviderKeys.openai]: { + icon: openAIIcon, + name: 'OpenAI', + }, +}; + +export const ServiceProvider: React.FC = ({ providerKey }) => { + const provider = SERVICE_PROVIDERS[providerKey]; + + return provider ? ( + <> + + {provider.name} + + ) : ( + {providerKey} + ); +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_task_type/task_type.test.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_task_type/task_type.test.tsx new file mode 100644 index 0000000000000..c81ce80a619e5 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_task_type/task_type.test.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { TaskType } from './task_type'; +import { TaskTypes } from '../../types'; + +describe('TaskType component', () => { + it.each([ + [TaskTypes.completion, 'completion'], + [TaskTypes.sparse_embedding, 'sparse_embedding'], + [TaskTypes.text_embedding, 'text_embedding'], + ])('renders the task type badge for %s', (taskType, expected) => { + render(); + const badge = screen.getByTestId(`table-column-task-type-${taskType}`); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveTextContent(expected); + }); + + it('returns null when type is null', () => { + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_task_type/task_type.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_task_type/task_type.tsx new file mode 100644 index 0000000000000..c294255961748 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_task_type/task_type.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiBadge } from '@elastic/eui'; +import React from 'react'; +import { TaskTypes } from '../../types'; + +interface TaskTypeProps { + type?: TaskTypes; +} + +export const TaskType: React.FC = ({ type }) => { + if (type != null) { + return ( + + {type} + + ); + } + + return null; +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/table_columns.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/table_columns.tsx new file mode 100644 index 0000000000000..caca4449e02fa --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/table_columns.tsx @@ -0,0 +1,79 @@ +/* + * 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 { InferenceAPIConfigResponse } from '@kbn/ml-trained-models-utils'; +import React from 'react'; +import type { HorizontalAlignment } from '@elastic/eui'; +import * as i18n from '../../../../common/translations'; +import { useActions } from './render_actions/use_actions'; +import { EndpointInfo } from './render_endpoint/endpoint_info'; +import { ServiceProvider } from './render_service_provider/service_provider'; +import { TaskType } from './render_task_type/task_type'; +import { DeploymentStatus } from './render_deployment_status/deployment_status'; +import { DeploymentStatusEnum, ServiceProviderKeys, TaskTypes } from '../types'; + +export const useTableColumns = () => { + const { actions } = useActions(); + const deploymentAlignment: HorizontalAlignment = 'center'; + + const TABLE_COLUMNS = [ + { + field: 'deployment', + name: '', + render: (deployment: DeploymentStatusEnum) => { + if (deployment != null) { + return ; + } + + return null; + }, + width: '64px', + align: deploymentAlignment, + }, + { + field: 'endpoint', + name: i18n.ENDPOINT, + render: (endpoint: InferenceAPIConfigResponse) => { + if (endpoint != null) { + return ; + } + + return null; + }, + sortable: true, + }, + { + field: 'provider', + name: i18n.SERVICE_PROVIDER, + render: (provider: ServiceProviderKeys) => { + if (provider != null) { + return ; + } + + return null; + }, + sortable: false, + width: '265px', + }, + { + field: 'type', + name: i18n.TASK_TYPE, + render: (type: TaskTypes) => { + if (type != null) { + return ; + } + + return null; + }, + sortable: false, + width: '265px', + }, + actions, + ]; + + return TABLE_COLUMNS; +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/search/table_search.test.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/search/table_search.test.tsx new file mode 100644 index 0000000000000..b7d1dbcacfd71 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/search/table_search.test.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render, screen, fireEvent } from '@testing-library/react'; +import { TableSearch } from './table_search'; +import React from 'react'; + +describe('TableSearchComponent', () => { + const mockSetSearchKey = jest.fn(); + + it('renders correctly', () => { + render(); + expect(screen.getByRole('searchbox')).toBeInTheDocument(); + }); + + it('input value matches searchKey prop', () => { + render(); + expect(screen.getByRole('searchbox')).toHaveValue('test'); + }); + + it('calls setSearchKey on input change', () => { + render(); + fireEvent.change(screen.getByRole('searchbox'), { target: { value: 'new search' } }); + expect(mockSetSearchKey).toHaveBeenCalledWith('new search'); + }); +}); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/search/table_search.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/search/table_search.tsx new file mode 100644 index 0000000000000..086dcb9656d83 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/search/table_search.tsx @@ -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 { EuiFieldSearch } from '@elastic/eui'; +import React, { useCallback } from 'react'; + +interface TableSearchComponentProps { + searchKey: string; + setSearchKey: React.Dispatch>; +} + +export const TableSearch: React.FC = ({ searchKey, setSearchKey }) => { + const onSearch = useCallback( + (newSearch) => { + const trimSearch = newSearch.trim(); + setSearchKey(trimSearch); + }, + [setSearchKey] + ); + + return ( + setSearchKey(e.target.value)} + onSearch={onSearch} + value={searchKey} + /> + ); +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/table_columns.ts b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/table_columns.ts deleted file mode 100644 index 39e957f908684..0000000000000 --- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/table_columns.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const TABLE_COLUMNS = [ - { - field: 'endpoint', - name: i18n.translate('xpack.searchInferenceEndpoints.inferenceEndpoints.table.endpoint', { - defaultMessage: 'Endpoint', - }), - sortable: true, - width: '50%', - }, - { - field: 'provider', - name: i18n.translate('xpack.searchInferenceEndpoints.inferenceEndpoints.table.provider', { - defaultMessage: 'Provider', - }), - sortable: false, - width: '110px', - }, - { - field: 'type', - name: i18n.translate('xpack.searchInferenceEndpoints.inferenceEndpoints.table.type', { - defaultMessage: 'Type', - }), - sortable: false, - width: '90px', - }, -]; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.test.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.test.tsx index 091ff8270dd63..91a2ea959fdec 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.test.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.test.tsx @@ -36,6 +36,12 @@ const inferenceEndpoints = [ }, ] as InferenceAPIConfigResponse[]; +jest.mock('../../hooks/use_delete_endpoint', () => ({ + useDeleteEndpoint: () => ({ + mutate: jest.fn().mockImplementation(() => Promise.resolve()), // Mock implementation of the mutate function + }), +})); + describe('When the tabular page is loaded', () => { beforeEach(() => { render(); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.tsx index 2fb84dc4b99de..ec19ad49ad477 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.tsx @@ -5,28 +5,87 @@ * 2.0. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { InferenceAPIConfigResponse } from '@kbn/ml-trained-models-utils'; +import { extractErrorProperties } from '@kbn/ml-error-utils'; +import * as i18n from '../../../common/translations'; import { useTableData } from '../../hooks/use_table_data'; +import { FilterOptions } from './types'; + +import { DeploymentStatusEnum } from './types'; import { useAllInferenceEndpointsState } from '../../hooks/use_all_inference_endpoints_state'; import { EndpointsTable } from './endpoints_table'; -import { TABLE_COLUMNS } from './table_columns'; +import { ServiceProviderFilter } from './filter/service_provider_filter'; +import { TaskTypeFilter } from './filter/task_type_filter'; +import { TableSearch } from './search/table_search'; +import { useTableColumns } from './render_table_columns/table_columns'; +import { useKibana } from '../../hooks/use_kibana'; interface TabularPageProps { inferenceEndpoints: InferenceAPIConfigResponse[]; } export const TabularPage: React.FC = ({ inferenceEndpoints }) => { - const { queryParams, setQueryParams } = useAllInferenceEndpointsState(); + const [searchKey, setSearchKey] = React.useState(''); + const [deploymentStatus, setDeploymentStatus] = React.useState< + Record + >({}); + const { queryParams, setQueryParams, filterOptions, setFilterOptions } = + useAllInferenceEndpointsState(); + + const { + services: { ml, notifications }, + } = useKibana(); + + const onFilterChangedCallback = useCallback( + (newFilterOptions: Partial) => { + setFilterOptions(newFilterOptions); + }, + [setFilterOptions] + ); + + useEffect(() => { + const fetchDeploymentStatus = async () => { + const trainedModelStats = await ml?.mlApi?.trainedModels.getTrainedModelStats(); + if (trainedModelStats) { + const newDeploymentStatus = trainedModelStats?.trained_model_stats.reduce( + (acc, modelStat) => { + if (modelStat.model_id) { + acc[modelStat.model_id] = + modelStat?.deployment_stats?.state === 'started' + ? DeploymentStatusEnum.deployed + : DeploymentStatusEnum.notDeployed; + } + return acc; + }, + {} as Record + ); + setDeploymentStatus(newDeploymentStatus); + } + }; + + fetchDeploymentStatus().catch((error) => { + const errorObj = extractErrorProperties(error); + notifications?.toasts?.addError(errorObj.message ? new Error(error.message) : error, { + title: i18n.TRAINED_MODELS_STAT_GATHER_FAILED, + }); + }); + }, [ml, notifications]); const { paginatedSortedTableData, pagination, sorting } = useTableData( inferenceEndpoints, - queryParams + queryParams, + filterOptions, + searchKey, + deploymentStatus ); + const tableColumns = useTableColumns(); + const handleTableChange = useCallback( ({ page, sort }) => { const newQueryParams = { @@ -46,12 +105,32 @@ export const TabularPage: React.FC = ({ inferenceEndpoints }) ); return ( - + + + + + + + + + + + + + + + + + + ); }; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/types.ts b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/types.ts index 4afba1dda110a..4a83ac401c89a 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/types.ts +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/types.ts @@ -5,8 +5,28 @@ * 2.0. */ +import { InferenceAPIConfigResponse } from '@kbn/ml-trained-models-utils'; export const INFERENCE_ENDPOINTS_TABLE_PER_PAGE_VALUES = [10, 25, 50, 100]; +export enum ServiceProviderKeys { + azureopenai = 'azureopenai', + azureaistudio = 'azureaistudio', + cohere = 'cohere', + elasticsearch = 'elasticsearch', + elser = 'elser', + googleaistudio = 'googleaistudio', + hugging_face = 'hugging_face', + mistral = 'mistral', + openai = 'openai', +} + +export enum TaskTypes { + completion = 'completion', + rerank = 'rerank', + sparse_embedding = 'sparse_embedding', + text_embedding = 'text_embedding', +} + export enum SortFieldInferenceEndpoint { endpoint = 'endpoint', } @@ -25,7 +45,13 @@ export interface QueryParams extends SortingParams { perPage: number; } -export interface AlInferenceEndpointsTableState { +export interface FilterOptions { + provider: ServiceProviderKeys[]; + type: TaskTypes[]; +} + +export interface AllInferenceEndpointsTableState { + filterOptions: FilterOptions; queryParams: QueryParams; } @@ -34,8 +60,16 @@ export interface EuiBasicTableSortTypes { field: string; } +export enum DeploymentStatusEnum { + deployed = 'deployed', + notDeployed = 'not_deployed', + notDeployable = 'not_deployable', + notApplicable = 'not_applicable', +} + export interface InferenceEndpointUI { - endpoint: string; + deployment: DeploymentStatusEnum; + endpoint: InferenceAPIConfigResponse; provider: string; type: string; } diff --git a/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/add_empty_prompt.tsx b/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/add_empty_prompt.tsx index 69d74724016d6..ee858f7a8b640 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/add_empty_prompt.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/add_empty_prompt.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { EuiButton, - EuiEmptyPrompt, + EuiPageTemplate, EuiFlexGroup, EuiFlexItem, EuiImage, @@ -20,8 +20,7 @@ import * as i18n from '../../../common/translations'; import inferenceEndpoint from '../../assets/images/inference_endpoint.svg'; -import { ElserPrompt } from './elser_prompt'; -import { MultilingualE5Prompt } from './multilingual_e5_prompt'; +import { EndpointPrompt } from './endpoint_prompt'; import './add_empty_prompt.scss'; @@ -31,9 +30,12 @@ interface AddEmptyPromptProps { export const AddEmptyPrompt: React.FC = ({ setIsInferenceFlyoutVisible }) => { return ( - } title={

{i18n.INFERENCE_ENDPOINT_LABEL}

} body={ @@ -60,20 +62,41 @@ export const AddEmptyPrompt: React.FC = ({ setIsInferenceFl {i18n.START_WITH_PREPARED_ENDPOINTS_LABEL} - + - + setIsInferenceFlyoutVisible(true)} + > + {i18n.ADD_ENDPOINT_LABEL} + + } + /> - + setIsInferenceFlyoutVisible(true)} + > + {i18n.ADD_ENDPOINT_LABEL} + + } + /> } - color="plain" - hasBorder - icon={} /> ); }; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/elser_prompt.tsx b/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/elser_prompt.tsx deleted file mode 100644 index db38b649fd66e..0000000000000 --- a/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/elser_prompt.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { EuiButton, EuiCard } from '@elastic/eui'; - -import * as i18n from '../../../common/translations'; - -interface ElserPromptProps { - setIsInferenceFlyoutVisible: (value: boolean) => void; -} -export const ElserPrompt: React.FC = ({ setIsInferenceFlyoutVisible }) => ( - setIsInferenceFlyoutVisible(true)}> - {i18n.ADD_ENDPOINT_LABEL} - - } - /> -); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/multilingual_e5_prompt.tsx b/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/endpoint_prompt.tsx similarity index 51% rename from x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/multilingual_e5_prompt.tsx rename to x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/endpoint_prompt.tsx index 69133909efcb2..b10812ef2e6a3 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/multilingual_e5_prompt.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/endpoint_prompt.tsx @@ -6,29 +6,28 @@ */ import React from 'react'; +import { EuiCard } from '@elastic/eui'; -import { EuiButton, EuiCard } from '@elastic/eui'; - -import * as i18n from '../../../common/translations'; - -interface MultilingualE5PromptProps { +interface EndpointPromptProps { setIsInferenceFlyoutVisible: (value: boolean) => void; + title: string; + description: string; + footer: React.ReactElement; } -export const MultilingualE5Prompt: React.FC = ({ +export const EndpointPrompt: React.FC = ({ setIsInferenceFlyoutVisible, + title, + description, + footer, }) => ( setIsInferenceFlyoutVisible(true)}> - {i18n.ADD_ENDPOINT_LABEL} - - } + title={title} + titleSize="xs" + description={description} + footer={footer} /> ); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/inference_endpoints_header.tsx b/x-pack/plugins/search_inference_endpoints/public/components/inference_endpoints_header.tsx index 8b551af28bd4c..6956e470a9b77 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/inference_endpoints_header.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/components/inference_endpoints_header.tsx @@ -5,8 +5,8 @@ * 2.0. */ +import { EuiButton, EuiPageTemplate } from '@elastic/eui'; import React from 'react'; -import { EuiPageTemplate, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; import * as i18n from '../../common/translations'; interface InferenceEndpointsHeaderProps { @@ -21,21 +21,18 @@ export const InferenceEndpointsHeader: React.FC = data-test-subj="allInferenceEndpointsPage" pageTitle={i18n.INFERENCE_ENDPOINT_LABEL} description={i18n.MANAGE_INFERENCE_ENDPOINTS_LABEL} + bottomBorder={true} rightSideItems={[ - - - setIsInferenceFlyoutVisible(true)} - > - {i18n.ADD_ENDPOINT_LABEL} - - - , + setIsInferenceFlyoutVisible(true)} + > + {i18n.ADD_ENDPOINT_LABEL} + , ]} /> ); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/inference_flyout_wrapper_component.tsx b/x-pack/plugins/search_inference_endpoints/public/components/inference_flyout_wrapper_component.tsx index bf1343fe3db8b..43dc7d7d1751c 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/inference_flyout_wrapper_component.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/components/inference_flyout_wrapper_component.tsx @@ -47,9 +47,11 @@ export const InferenceFlyoutWrapperComponent: React.FC { @@ -87,17 +93,26 @@ export const InferenceFlyoutWrapperComponent: React.FC { setIsCreateInferenceApiLoading(true); - try { - await createInferenceEndpointMutation.mutateAsync({ inferenceId, taskType, modelConfig }); - setIsInferenceFlyoutVisible(!isInferenceFlyoutVisible); - } catch (error) { - const errorObj = extractErrorProperties(error); - setInferenceAddError(errorObj.message); - } finally { - setIsCreateInferenceApiLoading(false); - } + + createInferenceEndpointMutation + .mutateAsync({ inferenceId, taskType, modelConfig }) + .catch((error) => { + const errorObj = extractErrorProperties(error); + notifications?.toasts?.addError(errorObj.message ? new Error(error.message) : error, { + title: i18n.ENDPOINT_CREATION_FAILED, + }); + }) + .finally(() => { + setIsCreateInferenceApiLoading(false); + }); + setIsInferenceFlyoutVisible(!isInferenceFlyoutVisible); }, - [createInferenceEndpointMutation, isInferenceFlyoutVisible, setIsInferenceFlyoutVisible] + [ + createInferenceEndpointMutation, + isInferenceFlyoutVisible, + setIsInferenceFlyoutVisible, + notifications, + ] ); const onFlyoutClose = useCallback(() => { diff --git a/x-pack/plugins/search_inference_endpoints/public/hooks/translations.ts b/x-pack/plugins/search_inference_endpoints/public/hooks/translations.ts new file mode 100644 index 0000000000000..ee1dc26a54817 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/hooks/translations.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +export * from '../../common/translations'; + +export const ENDPOINT_DELETION_FAILED = i18n.translate( + 'xpack.searchInferenceEndpoints.deleteEndpoint.endpointDeletionFailed', + { + defaultMessage: 'Endpoint deletion failed', + } +); + +export const DELETE_SUCCESS = i18n.translate( + 'xpack.searchInferenceEndpoints.deleteEndpoint.deleteSuccess', + { + defaultMessage: 'The inference endpoint has been deleted sucessfully.', + } +); diff --git a/x-pack/plugins/search_inference_endpoints/public/hooks/use_all_inference_endpoints_state.tsx b/x-pack/plugins/search_inference_endpoints/public/hooks/use_all_inference_endpoints_state.tsx index 4979cdf7994fc..c2a0486578a9e 100644 --- a/x-pack/plugins/search_inference_endpoints/public/hooks/use_all_inference_endpoints_state.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/hooks/use_all_inference_endpoints_state.tsx @@ -9,7 +9,8 @@ import { useCallback, useState } from 'react'; import type { QueryParams, - AlInferenceEndpointsTableState, + AllInferenceEndpointsTableState, + FilterOptions, } from '../components/all_inference_endpoints/types'; import { DEFAULT_INFERENCE_ENDPOINTS_TABLE_STATE } from '../components/all_inference_endpoints/constants'; @@ -17,13 +18,15 @@ import { DEFAULT_INFERENCE_ENDPOINTS_TABLE_STATE } from '../components/all_infer interface UseAllInferenceEndpointsStateReturn { queryParams: QueryParams; setQueryParams: (queryParam: Partial) => void; + filterOptions: FilterOptions; + setFilterOptions: (filterOptions: Partial) => void; } export function useAllInferenceEndpointsState(): UseAllInferenceEndpointsStateReturn { - const [tableState, setTableState] = useState( + const [tableState, setTableState] = useState( DEFAULT_INFERENCE_ENDPOINTS_TABLE_STATE ); - const setState = useCallback((state: AlInferenceEndpointsTableState) => { + const setState = useCallback((state: AllInferenceEndpointsTableState) => { setTableState(state); }, []); @@ -34,8 +37,19 @@ export function useAllInferenceEndpointsState(): UseAllInferenceEndpointsStateRe }, setQueryParams: (newQueryParams: Partial) => { setState({ + filterOptions: tableState.filterOptions, queryParams: { ...tableState.queryParams, ...newQueryParams }, }); }, + filterOptions: { + ...DEFAULT_INFERENCE_ENDPOINTS_TABLE_STATE.filterOptions, + ...tableState.filterOptions, + }, + setFilterOptions: (newFilterOptions: Partial) => { + setState({ + filterOptions: { ...tableState.filterOptions, ...newFilterOptions }, + queryParams: tableState.queryParams, + }); + }, }; } diff --git a/x-pack/plugins/search_inference_endpoints/public/hooks/use_delete_endpoint.test.tsx b/x-pack/plugins/search_inference_endpoints/public/hooks/use_delete_endpoint.test.tsx new file mode 100644 index 0000000000000..72e8dfe8418e2 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/hooks/use_delete_endpoint.test.tsx @@ -0,0 +1,72 @@ +/* + * 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 { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { useKibana } from './use_kibana'; +import { useDeleteEndpoint } from './use_delete_endpoint'; +import * as i18n from './translations'; +import React from 'react'; + +jest.mock('./use_kibana'); + +const mockUseKibana = useKibana as jest.Mock; +const mockDelete = jest.fn(); +const mockAddSuccess = jest.fn(); +const mockAddError = jest.fn(); + +describe('useDeleteEndpoint', () => { + beforeEach(() => { + mockUseKibana.mockReturnValue({ + services: { + http: { + delete: mockDelete, + }, + notifications: { + toasts: { + addSuccess: mockAddSuccess, + addError: mockAddError, + }, + }, + }, + }); + mockDelete.mockResolvedValue({}); + }); + + const wrapper = ({ children }: { children: React.ReactNode }) => { + const queryClient = new QueryClient(); + return {children}; + }; + + it('should call delete endpoint and show success toast on success', async () => { + const { result, waitFor } = renderHook(() => useDeleteEndpoint(), { wrapper }); + + result.current.mutate({ type: 'text_embedding', id: 'in-1' }); + + await waitFor(() => + expect(mockDelete).toHaveBeenCalledWith( + '/internal/inference_endpoint/endpoints/text_embedding/in-1' + ) + ); + expect(mockAddSuccess).toHaveBeenCalledWith({ + title: i18n.DELETE_SUCCESS, + }); + }); + + it('should show error toast on failure', async () => { + const error = new Error('Deletion failed'); + mockDelete.mockRejectedValue(error); + const { result, waitFor } = renderHook(() => useDeleteEndpoint(), { wrapper }); + + result.current.mutate({ type: 'model', id: '123' }); + + await waitFor(() => expect(mockAddError).toHaveBeenCalled()); + expect(mockAddError).toHaveBeenCalledWith(error, { + title: i18n.ENDPOINT_DELETION_FAILED, + }); + }); +}); diff --git a/x-pack/plugins/search_inference_endpoints/public/hooks/use_delete_endpoint.tsx b/x-pack/plugins/search_inference_endpoints/public/hooks/use_delete_endpoint.tsx new file mode 100644 index 0000000000000..e5464703dcfe2 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/hooks/use_delete_endpoint.tsx @@ -0,0 +1,42 @@ +/* + * 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 { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useKibana } from './use_kibana'; +import * as i18n from './translations'; + +import { INFERENCE_ENDPOINTS_QUERY_KEY } from '../../common/constants'; + +interface MutationArgs { + type: string; + id: string; +} + +export const useDeleteEndpoint = () => { + const queryClient = useQueryClient(); + const { services } = useKibana(); + const toasts = services.notifications?.toasts; + + return useMutation( + async ({ type, id }: MutationArgs) => { + await services.http.delete<{}>(`/internal/inference_endpoint/endpoints/${type}/${id}`); + }, + { + onSuccess: () => { + queryClient.invalidateQueries([INFERENCE_ENDPOINTS_QUERY_KEY]); + toasts?.addSuccess({ + title: i18n.DELETE_SUCCESS, + }); + }, + onError: (error: Error) => { + toasts?.addError(new Error(error?.message), { + title: i18n.ENDPOINT_DELETION_FAILED, + }); + }, + } + ); +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/hooks/use_table_data.test.tsx b/x-pack/plugins/search_inference_endpoints/public/hooks/use_table_data.test.tsx index e0026cbffff98..a8d0326a4c36f 100644 --- a/x-pack/plugins/search_inference_endpoints/public/hooks/use_table_data.test.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/hooks/use_table_data.test.tsx @@ -36,8 +36,8 @@ const inferenceEndpoints = [ }, { model_id: 'my-elser-model-05', - task_type: 'sparse_embedding', - service: 'elser', + task_type: 'text_embedding', + service: 'elasticsearch', service_settings: { num_allocations: 1, num_threads: 1, @@ -54,9 +54,23 @@ const queryParams = { sortOrder: 'desc', } as QueryParams; +const filterOptions = { + provider: ['elser', 'elasticsearch'], + type: ['sparse_embedding', 'text_embedding'], +} as any; + +const deploymentStatus = { + '.elser_model_2': 'deployed', + lang_ident_model_1: 'not_deployed', +} as any; + +const searchKey = 'my'; + describe('useTableData', () => { it('should return correct pagination', () => { - const { result } = renderHook(() => useTableData(inferenceEndpoints, queryParams)); + const { result } = renderHook(() => + useTableData(inferenceEndpoints, queryParams, filterOptions, searchKey, deploymentStatus) + ); expect(result.current.pagination).toEqual({ pageIndex: 0, @@ -67,7 +81,9 @@ describe('useTableData', () => { }); it('should return correct sorting', () => { - const { result } = renderHook(() => useTableData(inferenceEndpoints, queryParams)); + const { result } = renderHook(() => + useTableData(inferenceEndpoints, queryParams, filterOptions, searchKey, deploymentStatus) + ); expect(result.current.sorting).toEqual({ sort: { @@ -78,15 +94,54 @@ describe('useTableData', () => { }); it('should return correctly sorted data', () => { - const { result } = renderHook(() => useTableData(inferenceEndpoints, queryParams)); + const { result } = renderHook(() => + useTableData(inferenceEndpoints, queryParams, filterOptions, searchKey, deploymentStatus) + ); const expectedSortedData = [...inferenceEndpoints].sort((a, b) => b.model_id.localeCompare(a.model_id) ); - const sortedEndpoints = result.current.sortedTableData.map((item) => item.endpoint); + const sortedEndpoints = result.current.sortedTableData.map((item) => item.endpoint.model_id); const expectedModelIds = expectedSortedData.map((item) => item.model_id); expect(sortedEndpoints).toEqual(expectedModelIds); }); + + it('should filter data based on provider and type from filterOptions', () => { + const filterOptions2 = { + provider: ['elser'], + type: ['text_embedding'], + } as any; + const { result } = renderHook(() => + useTableData(inferenceEndpoints, queryParams, filterOptions2, searchKey, deploymentStatus) + ); + + const filteredData = result.current.sortedTableData; + expect( + filteredData.every( + (endpoint) => + filterOptions.provider.includes(endpoint.provider) && + filterOptions.type.includes(endpoint.type) + ) + ).toBeTruthy(); + }); + + it('should filter data based on searchKey', () => { + const searchKey2 = 'model-05'; + const { result } = renderHook(() => + useTableData(inferenceEndpoints, queryParams, filterOptions, searchKey2, deploymentStatus) + ); + const filteredData = result.current.sortedTableData; + expect(filteredData.every((item) => item.endpoint.model_id.includes(searchKey))).toBeTruthy(); + }); + + it('should update deployment status based on deploymentStatus object', () => { + const { result } = renderHook(() => + useTableData(inferenceEndpoints, queryParams, filterOptions, searchKey, deploymentStatus) + ); + + const updatedData = result.current.sortedTableData; + expect(updatedData[0].deployment).toEqual('deployed'); + }); }); diff --git a/x-pack/plugins/search_inference_endpoints/public/hooks/use_table_data.tsx b/x-pack/plugins/search_inference_endpoints/public/hooks/use_table_data.tsx index 304c469e06093..54d865aad5190 100644 --- a/x-pack/plugins/search_inference_endpoints/public/hooks/use_table_data.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/hooks/use_table_data.tsx @@ -11,11 +11,15 @@ import { InferenceAPIConfigResponse } from '@kbn/ml-trained-models-utils'; import { useMemo } from 'react'; import { DEFAULT_TABLE_LIMIT } from '../components/all_inference_endpoints/constants'; import { - InferenceEndpointUI, + FilterOptions, INFERENCE_ENDPOINTS_TABLE_PER_PAGE_VALUES, + InferenceEndpointUI, QueryParams, SortOrder, + ServiceProviderKeys, + TaskTypes, } from '../components/all_inference_endpoints/types'; +import { DeploymentStatusEnum } from '../components/all_inference_endpoints/types'; interface UseTableDataReturn { tableData: InferenceEndpointUI[]; @@ -27,15 +31,50 @@ interface UseTableDataReturn { export const useTableData = ( inferenceEndpoints: InferenceAPIConfigResponse[], - queryParams: QueryParams + queryParams: QueryParams, + filterOptions: FilterOptions, + searchKey: string, + deploymentStatus: Record ): UseTableDataReturn => { const tableData: InferenceEndpointUI[] = useMemo(() => { - return inferenceEndpoints.map((endpoint) => ({ - endpoint: endpoint.model_id, - provider: endpoint.service, - type: endpoint.task_type, - })); - }, [inferenceEndpoints]); + let filteredEndpoints = inferenceEndpoints; + + if (filterOptions.provider.length > 0) { + filteredEndpoints = filteredEndpoints.filter((endpoint) => + filterOptions.provider.includes(ServiceProviderKeys[endpoint.service]) + ); + } + + if (filterOptions.type.length > 0) { + filteredEndpoints = filteredEndpoints.filter((endpoint) => + filterOptions.type.includes(TaskTypes[endpoint.task_type]) + ); + } + + return filteredEndpoints + .filter((endpoint) => endpoint.model_id.includes(searchKey)) + .map((endpoint) => { + const isElasticService = + endpoint.service === ServiceProviderKeys.elasticsearch || + endpoint.service === ServiceProviderKeys.elser; + + let deploymentStatusValue = DeploymentStatusEnum.notApplicable; + if (isElasticService) { + const modelId = endpoint.service_settings?.model_id; + deploymentStatusValue = + modelId && deploymentStatus[modelId] !== undefined + ? deploymentStatus[modelId] + : DeploymentStatusEnum.notDeployable; + } + + return { + deployment: deploymentStatusValue, + endpoint, + provider: endpoint.service, + type: endpoint.task_type, + }; + }); + }, [inferenceEndpoints, searchKey, filterOptions, deploymentStatus]); const sortedTableData: InferenceEndpointUI[] = useMemo(() => { return [...tableData].sort((a, b) => { @@ -43,9 +82,9 @@ export const useTableData = ( const bValue = b[queryParams.sortField]; if (queryParams.sortOrder === SortOrder.asc) { - return aValue.localeCompare(bValue); + return aValue.model_id.localeCompare(bValue.model_id); } else { - return bValue.localeCompare(aValue); + return bValue.model_id.localeCompare(aValue.model_id); } }); }, [tableData, queryParams]); diff --git a/x-pack/plugins/search_inference_endpoints/server/lib/delete_inference_endpoint.test.ts b/x-pack/plugins/search_inference_endpoints/server/lib/delete_inference_endpoint.test.ts new file mode 100644 index 0000000000000..f6c9a5625230d --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/server/lib/delete_inference_endpoint.test.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { deleteInferenceEndpoint } from './delete_inference_endpoint'; + +describe('deleteInferenceEndpoint', () => { + let mockClient: any; + + beforeEach(() => { + mockClient = { + transport: { + request: jest.fn(), + }, + }; + }); + + it('should call the Elasticsearch client with the correct DELETE request', async () => { + const type = 'model'; + const id = 'model-id-123'; + + await deleteInferenceEndpoint(mockClient, type, id); + + expect(mockClient.transport.request).toHaveBeenCalledWith({ + method: 'DELETE', + path: `/_inference/${type}/${id}`, + }); + }); +}); diff --git a/x-pack/plugins/search_inference_endpoints/server/lib/delete_inference_endpoint.ts b/x-pack/plugins/search_inference_endpoints/server/lib/delete_inference_endpoint.ts new file mode 100644 index 0000000000000..c294820e4943e --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/server/lib/delete_inference_endpoint.ts @@ -0,0 +1,19 @@ +/* + * 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 { ElasticsearchClient } from '@kbn/core/server'; + +export const deleteInferenceEndpoint = async ( + client: ElasticsearchClient, + type: string, + id: string +): Promise => { + return await client.transport.request({ + method: 'DELETE', + path: `/_inference/${type}/${id}`, + }); +}; diff --git a/x-pack/plugins/search_inference_endpoints/server/routes.ts b/x-pack/plugins/search_inference_endpoints/server/routes.ts index d5f010b902c52..7456888aabb19 100644 --- a/x-pack/plugins/search_inference_endpoints/server/routes.ts +++ b/x-pack/plugins/search_inference_endpoints/server/routes.ts @@ -6,10 +6,12 @@ */ import { IRouter } from '@kbn/core/server'; +import { schema } from '@kbn/config-schema'; import type { Logger } from '@kbn/logging'; import { fetchInferenceEndpoints } from './lib/fetch_inference_endpoints'; import { APIRoutes } from './types'; import { errorHandler } from './utils/error_handler'; +import { deleteInferenceEndpoint } from './lib/delete_inference_endpoint'; export function defineRoutes({ logger, router }: { logger: Logger; router: IRouter }) { router.get( @@ -32,4 +34,27 @@ export function defineRoutes({ logger, router }: { logger: Logger; router: IRout }); }) ); + + router.delete( + { + path: APIRoutes.DELETE_INFERENCE_ENDPOINT, + validate: { + params: schema.object({ + type: schema.string(), + id: schema.string(), + }), + }, + }, + errorHandler(logger)(async (context, request, response) => { + const { + client: { asCurrentUser }, + } = (await context.core).elasticsearch; + + const { type, id } = request.params; + + await deleteInferenceEndpoint(asCurrentUser, type, id); + + return response.ok(); + }) + ); } diff --git a/x-pack/plugins/security/server/authentication/api_keys/api_keys.test.ts b/x-pack/plugins/security/server/authentication/api_keys/api_keys.test.ts index 7c63ace9fc706..198bc004ecb60 100644 --- a/x-pack/plugins/security/server/authentication/api_keys/api_keys.test.ts +++ b/x-pack/plugins/security/server/authentication/api_keys/api_keys.test.ts @@ -30,6 +30,7 @@ describe('API Keys', () => { >; let mockLicense: jest.Mocked; let logger: Logger; + const roleDescriptors: { [key: string]: any } = { foo: true }; beforeEach(() => { mockValidateKibanaPrivileges.mockReset().mockReturnValue({ validationErrors: [] }); @@ -239,9 +240,10 @@ describe('API Keys', () => { }); const result = await apiKeys.create(httpServerMock.createKibanaRequest(), { name: 'key-name', - role_descriptors: { foo: true }, + role_descriptors: roleDescriptors, expiration: '1d', }); + expect(result).toEqual({ api_key: 'abc123', expiration: '1d', @@ -253,7 +255,7 @@ describe('API Keys', () => { expect(mockScopedClusterClient.asCurrentUser.security.createApiKey).toHaveBeenCalledWith({ body: { name: 'key-name', - role_descriptors: { foo: true }, + role_descriptors: roleDescriptors, expiration: '1d', }, }); @@ -343,7 +345,7 @@ describe('API Keys', () => { const result = await apiKeys.update(httpServerMock.createKibanaRequest(), { id: 'test_id', - role_descriptors: { foo: true }, + role_descriptors: roleDescriptors, metadata: {}, }); @@ -370,7 +372,7 @@ describe('API Keys', () => { const result = await apiKeys.update(httpServerMock.createKibanaRequest(), { id: 'test_id', - role_descriptors: { foo: true }, + role_descriptors: roleDescriptors, metadata: {}, }); @@ -473,7 +475,7 @@ describe('API Keys', () => { }), { name: 'test_api_key', - role_descriptors: { foo: true }, + role_descriptors: roleDescriptors, expiration: '1d', } ); @@ -512,7 +514,7 @@ describe('API Keys', () => { }), { name: 'test_api_key', - role_descriptors: { foo: true }, + role_descriptors: roleDescriptors, expiration: '1d', } ); @@ -527,7 +529,7 @@ describe('API Keys', () => { body: { api_key: { name: 'test_api_key', - role_descriptors: { foo: true }, + role_descriptors: roleDescriptors, expiration: '1d', }, grant_type: 'access_token', @@ -553,7 +555,7 @@ describe('API Keys', () => { }), { name: 'test_api_key', - role_descriptors: { foo: true }, + role_descriptors: roleDescriptors, expiration: '1d', } ); @@ -592,7 +594,7 @@ describe('API Keys', () => { }), { name: 'test_api_key', - role_descriptors: { foo: true }, + role_descriptors: roleDescriptors, expiration: '1d', } ) diff --git a/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts b/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts index 1fbd99b4dd812..054ab59fbd0bb 100644 --- a/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts +++ b/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts @@ -20,6 +20,7 @@ import type { InvalidateAPIKeysParams, ValidateAPIKeyParams, } from '@kbn/security-plugin-types-server'; +import { isCreateRestAPIKeyParams } from '@kbn/security-plugin-types-server'; import { getFakeKibanaRequest } from './fake_kibana_request'; import type { SecurityLicense } from '../../../common'; @@ -96,7 +97,6 @@ export class APIKeys implements APIKeysType { this.logger.debug( `Testing if API Keys are enabled by attempting to invalidate a non-existant key: ${id}` ); - try { await this.clusterClient.asInternalUser.security.invalidateApiKey({ body: { @@ -125,7 +125,6 @@ export class APIKeys implements APIKeysType { this.logger.debug( `Testing if cross-cluster API Keys are enabled by attempting to update a non-existant key: ${id}` ); - try { await this.clusterClient.asInternalUser.transport.request({ method: 'PUT', @@ -155,13 +154,13 @@ export class APIKeys implements APIKeysType { if (!this.license.isEnabled()) { return null; } - const { type, expiration, name, metadata } = createParams; const scopedClusterClient = this.clusterClient.asScoped(request); this.logger.debug('Trying to create an API key'); let result: CreateAPIKeyResult; + try { if (type === 'cross_cluster') { result = await scopedClusterClient.asCurrentUser.transport.request({ @@ -175,13 +174,13 @@ export class APIKeys implements APIKeysType { name, expiration, metadata, - role_descriptors: - 'role_descriptors' in createParams - ? createParams.role_descriptors - : this.parseRoleDescriptorsWithKibanaPrivileges( - createParams.kibana_role_descriptors, - false - ), + role_descriptors: isCreateRestAPIKeyParams(createParams) + ? createParams.role_descriptors + : this.parseRoleDescriptorsWithKibanaPrivileges( + createParams.kibana_role_descriptors, + this.kibanaFeatures, + false + ), }, }); } @@ -234,6 +233,7 @@ export class APIKeys implements APIKeysType { ? updateParams.role_descriptors : this.parseRoleDescriptorsWithKibanaPrivileges( updateParams.kibana_role_descriptors, + this.kibanaFeatures, true ), }); @@ -279,12 +279,12 @@ export class APIKeys implements APIKeysType { ); const { expiration, metadata, name } = createParams; - const roleDescriptors = 'role_descriptors' in createParams ? createParams.role_descriptors : this.parseRoleDescriptorsWithKibanaPrivileges( createParams.kibana_role_descriptors, + this.kibanaFeatures, false ); @@ -293,7 +293,6 @@ export class APIKeys implements APIKeysType { authorizationHeader, clientAuthorizationHeader ); - // User needs `manage_api_key` or `grant_api_key` privilege to use this API let result: GrantAPIKeyResult; try { @@ -318,7 +317,6 @@ export class APIKeys implements APIKeysType { } this.logger.debug(`Trying to invalidate ${params.ids.length} an API key as current user`); - let result: InvalidateAPIKeyResult; try { // User needs `manage_api_key` privilege to use this API @@ -354,6 +352,7 @@ export class APIKeys implements APIKeysType { this.logger.debug(`Trying to invalidate ${params.ids.length} API keys`); let result: InvalidateAPIKeyResult; + try { // Internal user needs `cluster:admin/xpack/security/api_key/invalidate` privilege to use this API result = await this.clusterClient.asInternalUser.security.invalidateApiKey({ @@ -384,7 +383,6 @@ export class APIKeys implements APIKeysType { const fakeRequest = getFakeKibanaRequest(apiKeyPrams); this.logger.debug(`Trying to validate an API key`); - try { await this.clusterClient.asScoped(fakeRequest).asCurrentUser.security.authenticate(); this.logger.debug(`API key was validated successfully`); @@ -445,6 +443,7 @@ export class APIKeys implements APIKeysType { private parseRoleDescriptorsWithKibanaPrivileges( kibanaRoleDescriptors: CreateRestAPIKeyWithKibanaPrivilegesParams['kibana_role_descriptors'], + features: KibanaFeature[], isEdit: boolean ) { const roleDescriptors = Object.create(null); @@ -452,10 +451,7 @@ export class APIKeys implements APIKeysType { const allValidationErrors: string[] = []; if (kibanaRoleDescriptors) { Object.entries(kibanaRoleDescriptors).forEach(([roleKey, roleDescriptor]) => { - const { validationErrors } = validateKibanaPrivileges( - this.kibanaFeatures, - roleDescriptor.kibana - ); + const { validationErrors } = validateKibanaPrivileges(features, roleDescriptor.kibana); allValidationErrors.push(...validationErrors); const applications = transformPrivilegesToElasticsearchPrivileges( diff --git a/x-pack/plugins/security/server/authentication/authentication_service.mock.ts b/x-pack/plugins/security/server/authentication/authentication_service.mock.ts index de87e7161bda8..676b039eefaa4 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.mock.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.mock.ts @@ -5,9 +5,9 @@ * 2.0. */ +import { apiKeysMock } from '@kbn/core-security-server-mocks'; import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; -import { apiKeysMock } from './api_keys/api_keys.mock'; import type { InternalAuthenticationServiceStart } from './authentication_service'; export const authenticationServiceMock = { diff --git a/x-pack/plugins/security/server/authentication/authentication_service.ts b/x-pack/plugins/security/server/authentication/authentication_service.ts index abc400c0b6305..cf99084d4d0bc 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.ts @@ -348,7 +348,6 @@ export class AuthenticationService { applicationName, kibanaFeatures, }); - /** * Retrieves server protocol name/host name/port and merges it with `xpack.security.public` config * to construct a server base URL (deprecated, used by the SAML provider only). diff --git a/x-pack/plugins/security/server/build_delegate_apis.test.ts b/x-pack/plugins/security/server/build_delegate_apis.test.ts index 59963ad2aef8e..297ee6ebf92c4 100644 --- a/x-pack/plugins/security/server/build_delegate_apis.test.ts +++ b/x-pack/plugins/security/server/build_delegate_apis.test.ts @@ -65,6 +65,21 @@ describe('buildSecurityApi', () => { expect(auditService.asScoped(request).log).toHaveBeenCalledWith({ message: 'an event' }); }); }); + + describe('authc.apiKeys', () => { + it('properly delegates to the service', async () => { + await authc.apiKeys.areAPIKeysEnabled(); + expect(authc.apiKeys.areAPIKeysEnabled).toHaveBeenCalledTimes(1); + }); + + it('returns the result from the service', async () => { + authc.apiKeys.areAPIKeysEnabled.mockReturnValue(Promise.resolve(false)); + + const areAPIKeysEnabled = await authc.apiKeys.areAPIKeysEnabled(); + + expect(areAPIKeysEnabled).toBe(false); + }); + }); }); describe('buildUserProfileApi', () => { diff --git a/x-pack/plugins/security/server/build_delegate_apis.ts b/x-pack/plugins/security/server/build_delegate_apis.ts index fb782f3db256f..f6d57cfc8a4a8 100644 --- a/x-pack/plugins/security/server/build_delegate_apis.ts +++ b/x-pack/plugins/security/server/build_delegate_apis.ts @@ -24,6 +24,17 @@ export const buildSecurityApi = ({ getCurrentUser: (request) => { return getAuthc().getCurrentUser(request); }, + apiKeys: { + areAPIKeysEnabled: () => getAuthc().apiKeys.areAPIKeysEnabled(), + areCrossClusterAPIKeysEnabled: () => getAuthc().apiKeys.areAPIKeysEnabled(), + grantAsInternalUser: (request, createParams) => + getAuthc().apiKeys.grantAsInternalUser(request, createParams), + create: (request, createParams) => getAuthc().apiKeys.create(request, createParams), + update: (request, updateParams) => getAuthc().apiKeys.update(request, updateParams), + validate: (apiKeyParams) => getAuthc().apiKeys.validate(apiKeyParams), + invalidate: (request, params) => getAuthc().apiKeys.invalidate(request, params), + invalidateAsInternalUser: (params) => getAuthc().apiKeys.invalidateAsInternalUser(params), + }, }, audit: { asScoped(request) { diff --git a/x-pack/plugins/security/server/mocks.ts b/x-pack/plugins/security/server/mocks.ts index a5473176fc7e7..e47faeba525a0 100644 --- a/x-pack/plugins/security/server/mocks.ts +++ b/x-pack/plugins/security/server/mocks.ts @@ -7,7 +7,7 @@ import type { TransportResult } from '@elastic/elasticsearch'; -import { securityServiceMock } from '@kbn/core-security-server-mocks'; +import { apiKeysMock, securityServiceMock } from '@kbn/core-security-server-mocks'; import { auditServiceMock } from './audit/mocks'; import { authenticationServiceMock } from './authentication/authentication_service.mock'; @@ -19,7 +19,10 @@ function createSetupMock() { const mockAuthz = authorizationMock.create(); return { audit: auditServiceMock.create(), - authc: { getCurrentUser: jest.fn() }, + authc: { + getCurrentUser: jest.fn(), + apiKeys: apiKeysMock.create(), + }, authz: { actions: mockAuthz.actions, checkPrivilegesWithRequest: mockAuthz.checkPrivilegesWithRequest, diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 9d5ffde67b1d7..a500454f98fa9 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -77,7 +77,9 @@ export interface SecurityPluginSetup extends SecurityPluginSetupWithoutDeprecate /** * @deprecated Use `authc` methods from the `SecurityServiceStart` contract instead. */ - authc: { getCurrentUser: (request: KibanaRequest) => AuthenticatedUser | null }; + authc: { + getCurrentUser: (request: KibanaRequest) => AuthenticatedUser | null; + }; /** * @deprecated Use `authz` methods from the `SecurityServiceStart` contract instead. */ @@ -110,6 +112,7 @@ export class SecurityPlugin private readonly logger: Logger; private authorizationSetup?: AuthorizationServiceSetupInternal; private auditSetup?: AuditServiceSetup; + private configSubscription?: Subscription; private config?: ConfigType; @@ -189,6 +192,7 @@ export class SecurityPlugin this.initializerContext.logger.get('authentication') ); this.auditService = new AuditService(this.initializerContext.logger.get('audit')); + this.elasticsearchService = new ElasticsearchService( this.initializerContext.logger.get('elasticsearch') ); @@ -340,7 +344,9 @@ export class SecurityPlugin return Object.freeze({ audit: this.auditSetup, - authc: { getCurrentUser: (request) => this.getAuthentication().getCurrentUser(request) }, + authc: { + getCurrentUser: (request) => this.getAuthentication().getCurrentUser(request), + }, authz: { actions: this.authorizationSetup.actions, checkPrivilegesWithRequest: this.authorizationSetup.checkPrivilegesWithRequest, @@ -421,8 +427,8 @@ export class SecurityPlugin return Object.freeze({ authc: { - apiKeys: this.authenticationStart.apiKeys, getCurrentUser: this.authenticationStart.getCurrentUser, + apiKeys: this.authenticationStart.apiKeys, }, authz: { actions: this.authorizationSetup!.actions, diff --git a/x-pack/plugins/security/tsconfig.json b/x-pack/plugins/security/tsconfig.json index 64d162839cf1e..10c1ada6ede15 100644 --- a/x-pack/plugins/security/tsconfig.json +++ b/x-pack/plugins/security/tsconfig.json @@ -83,7 +83,7 @@ "@kbn/core-user-profile-browser", "@kbn/security-api-key-management", "@kbn/security-form-components", - "@kbn/core-security-server-mocks", + "@kbn/core-security-server-mocks" ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_response_schema.mock.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_response_schema.mock.ts index 1a39d65c1c22f..c605436576995 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_response_schema.mock.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_response_schema.mock.ts @@ -6,16 +6,18 @@ */ import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../../constants'; +import { getListArrayMock } from '../../../../detection_engine/schemas/types/lists.mock'; import type { EqlRule, EsqlRule, MachineLearningRule, + NewTermsRule, QueryRule, SavedQueryRule, SharedResponseProps, ThreatMatchRule, + ThresholdRule, } from './rule_schemas.gen'; -import { getListArrayMock } from '../../../../detection_engine/schemas/types/lists.mock'; export const ANCHOR_DATE = '2020-02-20T03:57:54.037Z'; @@ -238,3 +240,27 @@ export const getRulesEqlSchemaMock = (anchorDate: string = ANCHOR_DATE): EqlRule tiebreaker_field: undefined, }; }; + +export const getRulesNewTermsSchemaMock = (anchorDate: string = ANCHOR_DATE): NewTermsRule => { + return { + ...getResponseBaseParams(anchorDate), + type: 'new_terms', + query: '*', + language: 'kuery', + new_terms_fields: ['user.name'], + history_window_start: 'now-7d', + }; +}; + +export const getRulesThresholdSchemaMock = (anchorDate: string = ANCHOR_DATE): ThresholdRule => { + return { + ...getResponseBaseParams(anchorDate), + type: 'threshold', + language: 'kuery', + query: 'user.name: root or user.name: admin', + threshold: { + field: 'some.field', + value: 4, + }, + }; +}; diff --git a/x-pack/plugins/security_solution/common/api/endpoint/actions/common/base.ts b/x-pack/plugins/security_solution/common/api/endpoint/actions/common/base.ts index a5e5c060e7303..8bb76a19fa015 100644 --- a/x-pack/plugins/security_solution/common/api/endpoint/actions/common/base.ts +++ b/x-pack/plugins/security_solution/common/api/endpoint/actions/common/base.ts @@ -71,13 +71,3 @@ export const NoParametersRequestSchema = { body: schema.object({ ...BaseActionRequestSchema }), }; export type BaseActionRequestBody = TypeOf; - -export const KillOrSuspendProcessRequestSchema = { - body: schema.object({ - ...BaseActionRequestSchema, - parameters: schema.oneOf([ - schema.object({ pid: schema.number({ min: 1 }) }), - schema.object({ entity_id: schema.string({ minLength: 1 }) }), - ]), - }), -}; diff --git a/x-pack/plugins/security_solution/common/api/endpoint/actions/common/response_actions.ts b/x-pack/plugins/security_solution/common/api/endpoint/actions/common/response_actions.ts index 66dc4d5828ce0..ca6d9d5e91982 100644 --- a/x-pack/plugins/security_solution/common/api/endpoint/actions/common/response_actions.ts +++ b/x-pack/plugins/security_solution/common/api/endpoint/actions/common/response_actions.ts @@ -7,15 +7,20 @@ import type { TypeOf } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema'; -import { UploadActionRequestSchema } from '../..'; +import { + KillProcessRouteRequestSchema, + SuspendProcessRouteRequestSchema, + UploadActionRequestSchema, +} from '../..'; import { ExecuteActionRequestSchema } from '../execute_route'; import { EndpointActionGetFileSchema } from '../get_file_route'; import { ScanActionRequestSchema } from '../scan_route'; -import { KillOrSuspendProcessRequestSchema, NoParametersRequestSchema } from './base'; +import { NoParametersRequestSchema } from './base'; export const ResponseActionBodySchema = schema.oneOf([ NoParametersRequestSchema.body, - KillOrSuspendProcessRequestSchema.body, + KillProcessRouteRequestSchema.body, + SuspendProcessRouteRequestSchema.body, EndpointActionGetFileSchema.body, ExecuteActionRequestSchema.body, ScanActionRequestSchema.body, diff --git a/x-pack/plugins/security_solution/common/api/endpoint/actions/kill_process_route.ts b/x-pack/plugins/security_solution/common/api/endpoint/actions/kill_process_route.ts index 8652623f93a57..f3c0d4e8f12be 100644 --- a/x-pack/plugins/security_solution/common/api/endpoint/actions/kill_process_route.ts +++ b/x-pack/plugins/security_solution/common/api/endpoint/actions/kill_process_route.ts @@ -5,6 +5,39 @@ * 2.0. */ -import { KillOrSuspendProcessRequestSchema } from './common/base'; +import { schema } from '@kbn/config-schema'; +import { BaseActionRequestSchema } from './common/base'; -export const KillProcessRouteRequestSchema = KillOrSuspendProcessRequestSchema; +// -------------------------------------------------- +// Tests for this module are at: +// x-pack/plugins/security_solution/common/endpoint/schema/actions.test.ts:604 +// -------------------------------------------------- + +export const KillProcessRouteRequestSchema = { + body: schema.object( + { + ...BaseActionRequestSchema, + parameters: schema.oneOf([ + schema.object({ pid: schema.number({ min: 1 }) }), + schema.object({ entity_id: schema.string({ minLength: 1 }) }), + + // Process Name currently applies only to SentinelOne (validated below) + schema.object({ process_name: schema.string({ minLength: 1 }) }), + ]), + }, + { + validate(bodyContent) { + if ('process_name' in bodyContent.parameters && bodyContent.agent_type !== 'sentinel_one') { + return `[parameters.process_name]: is not valid with agent type of ${bodyContent.agent_type}`; + } + + if ( + bodyContent.agent_type === 'sentinel_one' && + !('process_name' in bodyContent.parameters) + ) { + return `[parameters.process_name]: missing parameter for agent type of ${bodyContent.agent_type}`; + } + }, + } + ), +}; diff --git a/x-pack/plugins/security_solution/common/api/endpoint/actions/suspend_process_route.ts b/x-pack/plugins/security_solution/common/api/endpoint/actions/suspend_process_route.ts index 71801f4b979fc..81edb01197c69 100644 --- a/x-pack/plugins/security_solution/common/api/endpoint/actions/suspend_process_route.ts +++ b/x-pack/plugins/security_solution/common/api/endpoint/actions/suspend_process_route.ts @@ -5,6 +5,15 @@ * 2.0. */ -import { KillOrSuspendProcessRequestSchema } from './common/base'; +import { schema } from '@kbn/config-schema'; +import { BaseActionRequestSchema } from './common/base'; -export const SuspendProcessRouteRequestSchema = KillOrSuspendProcessRequestSchema; +export const SuspendProcessRouteRequestSchema = { + body: schema.object({ + ...BaseActionRequestSchema, + parameters: schema.oneOf([ + schema.object({ pid: schema.number({ min: 1 }) }), + schema.object({ entity_id: schema.string({ minLength: 1 }) }), + ]), + }), +}; diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/actions.test.ts b/x-pack/plugins/security_solution/common/endpoint/schema/actions.test.ts index 563633ed8413d..4ea9f59ea179d 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/actions.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/actions.test.ts @@ -14,11 +14,13 @@ import { } from '../service/response_actions/constants'; import { createHapiReadableStreamMock } from '../../../server/endpoint/services/actions/mocks'; import type { HapiReadableStream } from '../../../server/types'; -import { EndpointActionListRequestSchema, UploadActionRequestSchema } from '../../api/endpoint'; import { - KillOrSuspendProcessRequestSchema, - NoParametersRequestSchema, -} from '../../api/endpoint/actions/common/base'; + EndpointActionListRequestSchema, + KillProcessRouteRequestSchema, + SuspendProcessRouteRequestSchema, + UploadActionRequestSchema, +} from '../../api/endpoint'; +import { NoParametersRequestSchema } from '../../api/endpoint/actions/common/base'; import { ExecuteActionRequestSchema } from '../../api/endpoint/actions/execute_route'; import { ScanActionRequestSchema } from '../../api/endpoint/actions/scan_route'; @@ -507,16 +509,20 @@ describe('actions schemas', () => { }); }); - describe('KillOrSuspendProcessRequestSchema', () => { + describe.each` + name | killOrSuspendSchema + ${'KillProcessRouteRequestSchema'} | ${KillProcessRouteRequestSchema} + ${'SuspendProcessRouteRequestSchema'} | ${SuspendProcessRouteRequestSchema} + `('$name', ({ name, killOrSuspendSchema }) => { it('should not accept when no endpoint_ids', () => { expect(() => { - KillOrSuspendProcessRequestSchema.body.validate({}); + killOrSuspendSchema.body.validate({}); }).toThrow(); }); it('should not accept empty endpoint_ids array', () => { expect(() => { - KillOrSuspendProcessRequestSchema.body.validate({ + killOrSuspendSchema.body.validate({ endpoint_ids: [], }); }).toThrow(); @@ -524,7 +530,7 @@ describe('actions schemas', () => { it('should not accept empty string as endpoint id', () => { expect(() => { - KillOrSuspendProcessRequestSchema.body.validate({ + killOrSuspendSchema.body.validate({ endpoint_ids: [' '], }); }).toThrow(); @@ -532,7 +538,7 @@ describe('actions schemas', () => { it('should not accept any empty string in endpoint_ids array', () => { expect(() => { - KillOrSuspendProcessRequestSchema.body.validate({ + killOrSuspendSchema.body.validate({ endpoint_ids: ['x', ' ', 'y'], }); }).toThrow(); @@ -540,7 +546,7 @@ describe('actions schemas', () => { it('should accept pid', () => { expect(() => { - KillOrSuspendProcessRequestSchema.body.validate({ + killOrSuspendSchema.body.validate({ endpoint_ids: ['ABC-XYZ-000'], parameters: { pid: 1234, @@ -551,7 +557,7 @@ describe('actions schemas', () => { it('should accept entity_id', () => { expect(() => { - KillOrSuspendProcessRequestSchema.body.validate({ + killOrSuspendSchema.body.validate({ endpoint_ids: ['ABC-XYZ-000'], parameters: { entity_id: 'abc123', @@ -562,7 +568,7 @@ describe('actions schemas', () => { it('should reject pid and entity_id together', () => { expect(() => { - KillOrSuspendProcessRequestSchema.body.validate({ + killOrSuspendSchema.body.validate({ endpoint_ids: ['ABC-XYZ-000'], parameters: { pid: 1234, @@ -574,7 +580,7 @@ describe('actions schemas', () => { it('should reject if no pid or entity_id', () => { expect(() => { - KillOrSuspendProcessRequestSchema.body.validate({ + killOrSuspendSchema.body.validate({ endpoint_ids: ['ABC-XYZ-000'], comment: 'a user comment', parameters: {}, @@ -584,7 +590,7 @@ describe('actions schemas', () => { it('should accept a comment', () => { expect(() => { - KillOrSuspendProcessRequestSchema.body.validate({ + killOrSuspendSchema.body.validate({ endpoint_ids: ['ABC-XYZ-000'], comment: 'a user comment', parameters: { @@ -595,6 +601,41 @@ describe('actions schemas', () => { }); }); + describe('KillProcessRequestSchema for SentinelOne', () => { + it('should error if agentType is not sentinel_one and process_name parameter is used', () => { + expect(() => { + KillProcessRouteRequestSchema.body.validate({ + endpoint_ids: ['abc'], + parameters: { + process_name: 'explorer.exe', + }, + }); + }).toThrow(); + }); + + it('should error if agentType is sentinel_one but process_name is not defined', () => { + expect(() => { + KillProcessRouteRequestSchema.body.validate({ + endpoint_ids: ['abc'], + agent_type: 'sentinel_one', + parameters: { pid: 4 }, + }); + }).toThrow(); + }); + + it('should allow use of process_name if agentType is sentinel_one', () => { + expect(() => { + KillProcessRouteRequestSchema.body.validate({ + endpoint_ids: ['abc'], + agent_type: 'sentinel_one', + parameters: { + process_name: 'explorer.exe', + }, + }); + }).not.toThrow(); + }); + }); + describe('ExecuteActionRequestSchema', () => { it('should not accept when no endpoint_ids', () => { expect(() => { diff --git a/x-pack/plugins/security_solution/common/endpoint/service/response_actions/is_response_action_supported.ts b/x-pack/plugins/security_solution/common/endpoint/service/response_actions/is_response_action_supported.ts index 0c0d0db960709..61216afeea1ee 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/response_actions/is_response_action_supported.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/response_actions/is_response_action_supported.ts @@ -74,7 +74,7 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = { }, manual: { endpoint: true, - sentinel_one: false, + sentinel_one: true, crowdstrike: false, }, }, diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts index 28be6f8d3d139..01da8a39ee723 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -8,12 +8,14 @@ import type { TypeOf } from '@kbn/config-schema'; import type { EcsError } from '@elastic/ecs'; import type { BaseFileMetadata, FileCompression, FileJSON } from '@kbn/files-plugin/common'; -import type { ResponseActionBodySchema, UploadActionApiRequestBody } from '../../api/endpoint'; -import type { ActionStatusRequestSchema } from '../../api/endpoint/actions/action_status_route'; import type { - KillOrSuspendProcessRequestSchema, - NoParametersRequestSchema, -} from '../../api/endpoint/actions/common/base'; + ResponseActionBodySchema, + UploadActionApiRequestBody, + KillProcessRouteRequestSchema, + SuspendProcessRouteRequestSchema, +} from '../../api/endpoint'; +import type { ActionStatusRequestSchema } from '../../api/endpoint/actions/action_status_route'; +import type { NoParametersRequestSchema } from '../../api/endpoint/actions/common/base'; import type { ResponseActionAgentType, ResponseActionsApiCommandNames, @@ -178,19 +180,28 @@ export interface LogsEndpointActionResponse< meta?: TMeta; } -interface ResponseActionParametersWithPid { +export interface ResponseActionParametersWithPid { pid: number; entity_id?: never; + process_name?: never; } -interface ResponseActionParametersWithEntityId { +export interface ResponseActionParametersWithEntityId { pid?: never; + process_name?: never; entity_id: string; } -export type ResponseActionParametersWithPidOrEntityId = +export interface ResponseActionParametersWithProcessName { + pid?: never; + entity_id?: never; + process_name: string; +} + +export type ResponseActionParametersWithProcessData = | ResponseActionParametersWithPid - | ResponseActionParametersWithEntityId; + | ResponseActionParametersWithEntityId + | ResponseActionParametersWithProcessName; export interface ResponseActionGetFileParameters { path: string; @@ -207,7 +218,7 @@ export interface ResponseActionScanParameters { export type EndpointActionDataParameterTypes = | undefined - | ResponseActionParametersWithPidOrEntityId + | ResponseActionParametersWithProcessData | ResponseActionsExecuteParameters | ResponseActionGetFileParameters | ResponseActionUploadParameters @@ -350,7 +361,12 @@ export type HostIsolationRequestBody = TypeOf; -export type KillOrSuspendProcessRequestBody = TypeOf; +export type KillProcessRequestBody = TypeOf; + +export type SuspendProcessRequestBody = TypeOf; + +/** Note: this type should almost never be used. Use instead the response action specific types above */ +export type KillOrSuspendProcessRequestBody = KillProcessRequestBody & SuspendProcessRequestBody; export interface HostIsolationResponse { action: string; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/sentinel_one.ts b/x-pack/plugins/security_solution/common/endpoint/types/sentinel_one.ts index 91a06ffc5ffca..a15557617d9f0 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/sentinel_one.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/sentinel_one.ts @@ -115,3 +115,11 @@ export interface SentinelOneGetFileResponseMeta { createdAt: string; filename: string; } + +export interface SentinelOneKillProcessRequestMeta extends SentinelOneIsolationRequestMeta { + /** + * The Parent Task Is that is executing the kill process action in SentinelOne. + * Used to check on the status of that action + */ + parentTaskId: string; +} diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 66b5f4bd948a1..03e1d324494f4 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -81,6 +81,9 @@ export const allowedExperimentalValues = Object.freeze({ /** Enables the `get-file` response action for SentinelOne */ responseActionsSentinelOneGetFileEnabled: true, + /** Enables the `kill-process` response action for SentinelOne */ + responseActionsSentinelOneKillProcessEnabled: false, + /** * Enables the ability to send Response actions to Crowdstrike and persist the results * in ES. @@ -239,11 +242,6 @@ export const allowedExperimentalValues = Object.freeze({ */ unifiedManifestEnabled: true, - /** - * Enables Security AI Assistant's Flyout mode - */ - aiAssistantFlyoutMode: true, - /** * Enables the new modal for the value list items */ diff --git a/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/installation_and_upgrade.md b/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/installation_and_upgrade.md index e2f98296e199c..beda8a9517830 100644 --- a/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/installation_and_upgrade.md +++ b/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/installation_and_upgrade.md @@ -55,14 +55,6 @@ Status: `in progress`. The current test plan matches `Milestone 2` of the [Rule - [**Scenario: User can see correct rule information in preview before upgrading**](#scenario-user-can-see-correct-rule-information-in-preview-before-upgrading) - [**Scenario: Tabs and sections without content should be hidden in preview before upgrading**](#scenario-tabs-and-sections-without-content-should-be-hidden-in-preview-before-upgrading) - [Rule upgrade workflow: filtering, sorting, pagination](#rule-upgrade-workflow-filtering-sorting-pagination) - - [Rule upgrade workflow: simple diff algorithm](#rule-upgrade-workflow-simple-diff-algorithm) - - [**Scenario: Rule field doesn't have an update and has no custom value**](#scenario-rule-field-doesnt-have-an-update-and-has-no-custom-value) - - [**Scenario: Rule field doesn't have an update but has a custom value**](#scenario-rule-field-doesnt-have-an-update-but-has-a-custom-value) - - [**Scenario: Rule field has an update and doesn't have a custom value**](#scenario-rule-field-has-an-update-and-doesnt-have-a-custom-value) - - [**Scenario: Rule field has an update and a custom value that are the same**](#scenario-rule-field-has-an-update-and-a-custom-value-that-are-the-same) - - [**Scenario: Rule field has an update and a custom value that are NOT the same**](#scenario-rule-field-has-an-update-and-a-custom-value-that-are-not-the-same) - - [**Scenario: Rule field has an update and a custom value that are NOT the same and the rule base version doesn't exist**](#scenario-rule-field-has-an-update-and-a-custom-value-that-are-not-the-same-and-the-rule-base-version-doesnt-exist) - - [**Scenario: Rule field has an update and a custom value that are the same and the rule base version doesn't exist**](#scenario-rule-field-has-an-update-and-a-custom-value-that-are-the-same-and-the-rule-base-version-doesnt-exist) - [Rule upgrade workflow: viewing rule changes in JSON diff view](#rule-upgrade-workflow-viewing-rule-changes-in-json-diff-view) - [**Scenario: User can see changes in a side-by-side JSON diff view**](#scenario-user-can-see-changes-in-a-side-by-side-json-diff-view) - [**Scenario: User can see precisely how property values would change after upgrade**](#scenario-user-can-see-precisely-how-property-values-would-change-after-upgrade) @@ -823,144 +815,6 @@ And the Investigation Guide tab should NOT be displayed TODO: add scenarios https://github.com/elastic/kibana/issues/166215 -### Rule upgrade workflow: simple diff algorithm - -#### **Scenario: Rule field doesn't have an update and has no custom value** - -**Automation**: 2 integration tests with mock rules + a set of unit tests for the algorithm - -```Gherkin -Given at least 1 prebuilt rule is installed in Kibana -And for this rule there is a new version available -And field is not customized by the user (current version == base version) -And field is not updated by Elastic in this upgrade (target version == base version) -Then for field the diff algorithm should output the current version as the merged one without a conflict -And field should not be returned from the `upgrade/_review` API endpoint -And field should not be shown in the upgrade preview UI - -Examples: -| field_name | base_version | current_version | target_version | -| name | "A" | "A" | "A" | -| risk_score | 1 | 1 | 1 | -``` - -#### **Scenario: Rule field doesn't have an update but has a custom value** - -**Automation**: 2 integration tests with mock rules + a set of unit tests for the algorithm - -```Gherkin -Given at least 1 prebuilt rule is installed in Kibana -And for this rule there is a new version available -And field is customized by the user (current version != base version) -And field is not updated by Elastic in this upgrade (target version == base version) -Then for field the diff algorithm should output the current version as the merged one without a conflict -And field should be returned from the `upgrade/_review` API endpoint -And field should be shown in the upgrade preview UI - -Examples: -| field_name | base_version | current_version | target_version | -| name | "A" | "B" | "A" | -| risk_score | 1 | 2 | 1 | -``` - -#### **Scenario: Rule field has an update and doesn't have a custom value** - -**Automation**: 2 integration tests with mock rules + a set of unit tests for the algorithm - -```Gherkin -Given at least 1 prebuilt rule is installed in Kibana -And for this rule there is a new version available -And field is not customized by the user (current version == base version) -And field is updated by Elastic in this upgrade (target version != base version) -Then for field the diff algorithm should output the target version as the merged one without a conflict -And field should be returned from the `upgrade/_review` API endpoint -And field should be shown in the upgrade preview UI - -Examples: -| field_name | base_version | current_version | target_version | -| name | "A" | "A" | "B" | -| risk_score | 1 | 1 | 2 | -``` - -#### **Scenario: Rule field has an update and a custom value that are the same** - -**Automation**: 2 integration tests with mock rules + a set of unit tests for the algorithm - -```Gherkin -Given at least 1 prebuilt rule is installed in Kibana -And for this rule there is a new version available -And field is customized by the user (current version != base version) -And field is updated by Elastic in this upgrade (target version != base version) -And customized field is the same as the Elastic update in this upgrade (current version == target version) -Then for field the diff algorithm should output the current version as the merged one without a conflict -And field should be returned from the `upgrade/_review` API endpoint -And field should be shown in the upgrade preview UI - -Examples: -| field_name | base_version | current_version | target_version | -| name | "A" | "B" | "B" | -| risk_score | 1 | 2 | 2 | -``` - -#### **Scenario: Rule field has an update and a custom value that are NOT the same** - -**Automation**: 2 integration tests with mock rules + a set of unit tests for the algorithm - -```Gherkin -Given at least 1 prebuilt rule is installed in Kibana -And for this rule there is a new version available -And field is customized by the user (current version != base version) -And field is updated by Elastic in this upgrade (target version != base version) -And customized field is different than the Elastic update in this upgrade (current version != target version) -Then for field the diff algorithm should output the current version as the merged one with a conflict -And field should be returned from the `upgrade/_review` API endpoint -And field should be shown in the upgrade preview UI - -Examples: -| field_name | base_version | current_version | target_version | -| name | "A" | "B" | "C" | -| risk_score | 1 | 2 | 3 | -``` - -#### **Scenario: Rule field has an update and a custom value that are NOT the same and the rule base version doesn't exist** - -**Automation**: 2 integration tests with mock rules + a set of unit tests for the algorithm - -```Gherkin -Given at least 1 prebuilt rule is installed in Kibana -And for this rule there is a new version available -And the base version of the rule cannot be determined -And customized field is different than the Elastic update in this upgrade (current version != target version) -Then for field the diff algorithm should output the target version as the merged one with a conflict -And field should be returned from the `upgrade/_review` API endpoint -And field should be shown in the upgrade preview UI - -Examples: -| field_name | base_version | current_version | target_version | -| name | N/A | "B" | "C" | -| risk_score | N/A | 2 | 3 | -``` - -#### **Scenario: Rule field has an update and a custom value that are the same and the rule base version doesn't exist** - -**Automation**: 2 integration tests with mock rules + a set of unit tests for the algorithm - -```Gherkin -Given at least 1 prebuilt rule is installed in Kibana -And for this rule there is a new version available -And the base version of the rule cannot be determined -And customized field is the same as the Elastic update in this upgrade (current version == target version) -Then for field the diff algorithm should output the current version as the merged one without a conflict -And field should not be returned from the `upgrade/_review` API endpoint -And field should not be shown in the upgrade preview UI - - -Examples: -| field_name | base_version | current_version | target_version | -| name | N/A | "A" | "A" | -| risk_score | N/A | 1 | 1 | -``` - ### Rule upgrade workflow: viewing rule changes in JSON diff view #### **Scenario: User can see changes in a side-by-side JSON diff view** diff --git a/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/upgrade_review_algorithms.md b/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/upgrade_review_algorithms.md new file mode 100644 index 0000000000000..fad01b9698b9b --- /dev/null +++ b/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/upgrade_review_algorithms.md @@ -0,0 +1,246 @@ +# Diff Algorithms for `upgrade/_review` Endpoint + +This is a test plan for the `upgrade/_review` endpoint diff algorithms that are a part of the larger prebuilt rules customization feature. These algorithms determine what fields get returned when a user makes an API request to review changes as a part of the rule update process and determine what version of those fields should be displayed by the UI. + +Status: `in progress`. + +## Table of Contents + +- [Useful information](#useful-information) + - [Tickets](#tickets) + - [Terminology](#terminology) + - [Assumptions](#assumptions) + - [Non-functional requirements](#non-functional-requirements) + - [Functional requirements](#functional-requirements) +- [Scenarios](#scenarios) + + - [Rule field doesn't have an update and has no custom value - `AAA`](#rule-field-doesnt-have-an-update-and-has-no-custom-value---aaa) + - [**Scenario: `AAA` - Rule field is any type**](#scenario-aaa---rule-field-is-any-type) + - [Rule field doesn't have an update but has a custom value - `ABA`](#rule-field-doesnt-have-an-update-but-has-a-custom-value---aba) + - [**Scenario: `ABA` - Rule field is any type**](#scenario-aba---rule-field-is-any-type) + - [Rule field has an update and doesn't have a custom value - `AAB`](#rule-field-has-an-update-and-doesnt-have-a-custom-value---aab) + - [**Scenario: `AAB` - Rule field is any type**](#scenario-aab---rule-field-is-any-type) + - [Rule field has an update and a custom value that are the same - `ABB`](#rule-field-has-an-update-and-a-custom-value-that-are-the-same---abb) + - [**Scenario: `ABB` - Rule field is any type**](#scenario-abb---rule-field-is-any-type) + - [Rule field has an update and a custom value that are NOT the same - `ABC`](#rule-field-has-an-update-and-a-custom-value-that-are-not-the-same---abc) + - [**Scenario: `ABC` - Rule field is a number or single line string**](#scenario-abc---rule-field-is-a-number-or-single-line-string) + - [**Scenario: `ABC` - Rule field is an array of scalar values**](#scenario-abc---rule-field-is-an-array-of-scalar-values) + - [Rule field has an update and a custom value that are the same and the rule base version doesn't exist - `-AA`](#rule-field-has-an-update-and-a-custom-value-that-are-the-same-and-the-rule-base-version-doesnt-exist----aa) + - [**Scenario: `-AA` - Rule field is any type**](#scenario--aa---rule-field-is-any-type) + - [Rule field has an update and a custom value that are NOT the same and the rule base version doesn't exist - `-BC`](#rule-field-has-an-update-and-a-custom-value-that-are-not-the-same-and-the-rule-base-version-doesnt-exist----bc) + - [**Scenario: `-BC` - Rule field is a number or single line string**](#scenario--bc---rule-field-is-a-number-or-single-line-string) + - [**Scenario: `-BC` - Rule field is an array of scalar values**](#scenario--bc---rule-field-is-an-array-of-scalar-values) + +## Useful information + +### Tickets + +- [Users can customize prebuilt detection rules](https://github.com/elastic/kibana/issues/174168) epic +- [Implement single-line string diff algorithm](https://github.com/elastic/kibana/issues/180158) +- [Implement number diff algorithm](https://github.com/elastic/kibana/issues/180160) +- [Implement array of scalar values diff algorithm](https://github.com/elastic/kibana/issues/180162) + +### Terminology + +- **Base version**: Also labeled as `base_version`. This is the version of a rule authored by Elastic as it is installed from the `security_detection_engine` package, with no customizations to any fields by the user. + +- **Current version**: Also labeled as `current_version`. This is the version of the rule that the user currently has installed. Consists of the `base_version` of the rules plus all customization applies to its fields by the user. + +- **Target version**: Also labeled as `target_version`. This is the version of the rule that contains the update from Elastic. + +- **Merged version**: Also labeled as `merged_version`. This is the version of the rule that we determine via the various algorithms. It could contain a mix of all the rule versions on a per-field basis to create a singluar version of the rule containing all relevant updates and user changes to display to the user. + +### Assumptions + +- All scenarios will contain at least 1 prebuilt rule installed in Kibana. +- A new version will be available for rule(s). + +## Scenarios + +### Rule field doesn't have an update and has no custom value - `AAA` + +#### **Scenario: `AAA` - Rule field is any type** + +**Automation**: 3 integration tests with mock rules + a set of unit tests for each algorithm + +```Gherkin +Given field is not customized by the user (current version == base version) +And field is not updated by Elastic in this upgrade (target version == base version) +Then for field the diff algorithm should output the current version as the merged one without a conflict +And field should not be returned from the `upgrade/_review` API endpoint +And field should not be shown in the upgrade preview UI + +Examples: +| algorithm | field_name | base_version | current_version | target_version | merged_version | +| single line string | name | "A" | "A" | "A" | "A" | +| number | risk_score | 1 | 1 | 1 | 1 | +| array of scalars | tags | ["one", "two", "three"] | ["one", "three", "two"] | ["three", "one", "two"] | ["one", "three", "two"] | +``` + +### Rule field doesn't have an update but has a custom value - `ABA` + +#### **Scenario: `ABA` - Rule field is any type** + +**Automation**: 3 integration tests with mock rules + a set of unit tests for each algorithm + +```Gherkin +Given field is customized by the user (current version != base version) +And field is not updated by Elastic in this upgrade (target version == base version) +Then for field the diff algorithm should output the current version as the merged one without a conflict +And field should be returned from the `upgrade/_review` API endpoint +And field should be shown in the upgrade preview UI + +Examples: +| algorithm | field_name | base_version | current_version | target_version | merged_version | +| single line string | name | "A" | "B" | "A" | "B" | +| number | risk_score | 1 | 2 | 1 | 2 | +| array of scalars | tags | ["one", "two", "three"] | ["one", "two", "four"] | ["one", "two", "three"] | ["one", "two", "four"] | +``` + +### Rule field has an update and doesn't have a custom value - `AAB` + +#### **Scenario: `AAB` - Rule field is any type** + +**Automation**: 3 integration tests with mock rules + a set of unit tests for each algorithm + +```Gherkin +Given field is not customized by the user (current version == base version) +And field is updated by Elastic in this upgrade (target version != base version) +Then for field the diff algorithm should output the target version as the merged one without a conflict +And field should be returned from the `upgrade/_review` API endpoint +And field should be shown in the upgrade preview UI + +Examples: +| algorithm | field_name | base_version | current_version | target_version | merged_version | +| single line string | name | "A" | "A" | "B" | "B" | +| number | risk_score | 1 | 1 | 2 | 2 | +| array of scalars | tags | ["one", "two", "three"] | ["one", "two", "three"] | ["one", "two", "four"] | ["one", "two", "four"] | +``` + +### Rule field has an update and a custom value that are the same - `ABB` + +#### **Scenario: `ABB` - Rule field is any type** + +**Automation**: 3 integration tests with mock rules + a set of unit tests for each algorithm + +```Gherkin +Given field is customized by the user (current version != base version) +And field is updated by Elastic in this upgrade (target version != base version) +And customized field is the same as the Elastic update in this upgrade (current version == target version) +Then for field the diff algorithm should output the current version as the merged one without a conflict +And field should be returned from the `upgrade/_review` API endpoint +And field should be shown in the upgrade preview UI + +Examples: +| algorithm | field_name | base_version | current_version | target_version | merged_version | +| single line string | name | "A" | "B" | "B" | "B" | +| number | risk_score | 1 | 2 | 2 | 2 | +| array of scalars | tags | ["one", "two", "three"] | ["one", "two", "four"] | ["one", "two", "four"] | ["one", "two", "four"] | +``` + +### Rule field has an update and a custom value that are NOT the same - `ABC` + +#### **Scenario: `ABC` - Rule field is a number or single line string** + +**Automation**: 2 integration tests with mock rules + a set of unit tests for the algorithms + +```Gherkin +Given field is customized by the user (current version != base version) +And field is updated by Elastic in this upgrade (target version != base version) +And customized field is different than the Elastic update in this upgrade (current version != target version) +Then for field the diff algorithm should output the current version as the merged one with a non-solvable conflict +And field should be returned from the `upgrade/_review` API endpoint +And field should be shown in the upgrade preview UI + +Examples: +| algorithm | field_name | base_version | current_version | target_version | merged_version | +| single line string | name | "A" | "B" | "C" | "B" | +| number | risk_score | 1 | 2 | 3 | 2 | +``` + +#### **Scenario: `ABC` - Rule field is an array of scalar values** + +**Automation**: 5 integration tests with mock rules + a set of unit tests for the algorithm + +```Gherkin +Given field is customized by the user (current version != base version) +And field is updated by Elastic in this upgrade (target version != base version) +And customized field is different than the Elastic update in this upgrade (current version != target version) +Then for field the diff algorithm should output a custom merged version with a solvable conflict +And arrays should be deduplicated before comparison +And arrays should be compared sensitive of case +And arrays should be compared agnostic of order +And field should be returned from the `upgrade/_review` API endpoint +And field should be shown in the upgrade preview UI + +Examples: +| algorithm | field_name | base_version | current_version | target_version | merged_version | +| array of scalars | tags | ["one", "two", "three"] | ["one", "two", "four"] | ["one", "two", "five"] | ["one", "two", "four", "five"] | +| array of scalars | tags | ["one", "two", "three"] | ["two", "one"] | ["one", "four"] | ["one", "four"] | +| array of scalars | tags | ["one", "two", "three"] | [] | ["one", "two", "five"] | ["five"] | +| array of scalars | tags | ["one", "two", "two"] | ["two", "one", "three"] | ["three", "three", "one"] | ["one", "three"] | +| array of scalars | index | ["logs-*", "endgame-*", "endpoint-*"] | ["Logs-*", "endgame-*"] | ["logs-*", "endgame-*", "new-*"] | ["Logs-*", "endgame-*", "new-*"] | +| array of scalars | index | ["logs-*"] | ["logs-*", "Logs-*"] | ["logs-*", "new-*"] | ["logs-*", "Logs-*", "new-*"] | +``` + +### Rule field has an update and a custom value that are the same and the rule base version doesn't exist - `-AA` + +#### **Scenario: `-AA` - Rule field is any type** + +**Automation**: 3 integration tests with mock rules + a set of unit tests for each algorithm + +```Gherkin +Given at least 1 installed prebuilt rule has a new version available +And the base version of the rule cannot be determined +And customized field is the same as the Elastic update in this upgrade (current version == target version) +Then for field the diff algorithm should output the current version as the merged one without a conflict +And field should not be returned from the `upgrade/_review` API endpoint +And field should not be shown in the upgrade preview UI + +Examples: +| algorithm | field_name | base_version | current_version | target_version | merged_version | +| single line string | name | N/A | "A" | "A" | "A" | +| number | risk_score | N/A | 1 | 1 | 1 | +| array of scalars | tags | N/A | ["one", "three", "two"] | ["three", "one", "two"] | ["one", "three", "two"] | +``` + +### Rule field has an update and a custom value that are NOT the same and the rule base version doesn't exist - `-BC` + +#### **Scenario: `-BC` - Rule field is a number or single line string** + +**Automation**: 2 integration tests with mock rules + a set of unit tests for the algorithms + +```Gherkin +Given at least 1 installed prebuilt rule has a new version available +And the base version of the rule cannot be determined +And customized field is different than the Elastic update in this upgrade (current version != target version) +Then for field the diff algorithm should output the target version as the merged one with a solvable conflict +And field should be returned from the `upgrade/_review` API endpoint +And field should be shown in the upgrade preview UI + +Examples: +| algorithm | field_name | base_version | current_version | target_version | merged_version | +| single line string | name | N/A | "B" | "C" | "C" | +| number | risk_score | N/A | 2 | 3 | 3 | +``` + +#### **Scenario: `-BC` - Rule field is an array of scalar values** + +**Automation**: 1 integration test with mock rules + a set of unit tests for the algorithm + +```Gherkin +Given field is customized by the user (current version != base version) +And field is updated by Elastic in this upgrade (target version != base version) +And customized field is different than the Elastic update in this upgrade (current version != target version) +Then for field the diff algorithm should output a custom merged version with a solvable conflict +And arrays should be deduplicated before comparison +And arrays should be compared sensitive of case +And arrays should be compared agnostic of order +And field should be returned from the `upgrade/_review` API endpoint +And field should be shown in the upgrade preview UI + + +Examples: +| algorithm | field_name | base_version | current_version | target_version | merged_version | +| array of scalars | tags | N/A | ["one", "two", "four"] | ["one", "two", "five"] | ["one", "two", "four", "five"] | +``` diff --git a/x-pack/plugins/security_solution/public/assistant/comment_actions/index.tsx b/x-pack/plugins/security_solution/public/assistant/comment_actions/index.tsx index df961ac223c4d..baf36e278b71f 100644 --- a/x-pack/plugins/security_solution/public/assistant/comment_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/assistant/comment_actions/index.tsx @@ -23,16 +23,15 @@ import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experime interface Props { message: ClientMessage; - isFlyoutMode: boolean; } -const CommentActionsComponent: React.FC = ({ message, isFlyoutMode }) => { +const CommentActionsComponent: React.FC = ({ message }) => { const toasts = useToasts(); const { cases } = useKibana().services; const dispatch = useDispatch(); const isModelEvaluationEnabled = useIsExperimentalFeatureEnabled('assistantModelEvaluation'); - const { showAssistantOverlay, traceOptions } = useAssistantContext(); + const { traceOptions } = useAssistantContext(); const associateNote = useCallback( (noteId: string) => dispatch(timelineActions.addNote({ id: TimelineId.active, noteId })), @@ -65,10 +64,6 @@ const CommentActionsComponent: React.FC = ({ message, isFlyoutMode }) => }); const onAddToExistingCase = useCallback(() => { - if (!isFlyoutMode) { - showAssistantOverlay({ showOverlay: false }); - } - selectCaseModal.open({ getAttachments: () => [ { @@ -78,7 +73,7 @@ const CommentActionsComponent: React.FC = ({ message, isFlyoutMode }) => }, ], }); - }, [content, isFlyoutMode, selectCaseModal, showAssistantOverlay]); + }, [content, selectCaseModal]); // Note: This feature is behind the `isModelEvaluationEnabled` FF. If ever released, this URL should be configurable // as APM data may not go to the same cluster where the Kibana instance is running diff --git a/x-pack/plugins/security_solution/public/assistant/get_comments/index.test.tsx b/x-pack/plugins/security_solution/public/assistant/get_comments/index.test.tsx index b59f14684a2ca..884af527f4be0 100644 --- a/x-pack/plugins/security_solution/public/assistant/get_comments/index.test.tsx +++ b/x-pack/plugins/security_solution/public/assistant/get_comments/index.test.tsx @@ -39,7 +39,6 @@ const testProps = { isFetchingResponse: false, currentConversation, showAnonymizedValues, - isFlyoutMode: false, }; describe('getComments', () => { it('Does not add error state message has no error', () => { diff --git a/x-pack/plugins/security_solution/public/assistant/get_comments/index.tsx b/x-pack/plugins/security_solution/public/assistant/get_comments/index.tsx index 10d5a15c800ae..8976c851c6e65 100644 --- a/x-pack/plugins/security_solution/public/assistant/get_comments/index.tsx +++ b/x-pack/plugins/security_solution/public/assistant/get_comments/index.tsx @@ -60,7 +60,6 @@ export const getComments = ({ refetchCurrentConversation, regenerateMessage, showAnonymizedValues, - isFlyoutMode, currentUserAvatar, setIsStreaming, }: { @@ -71,7 +70,6 @@ export const getComments = ({ refetchCurrentConversation: () => void; regenerateMessage: (conversationId: string) => void; showAnonymizedValues: boolean; - isFlyoutMode: boolean; currentUserAvatar?: UserAvatar; setIsStreaming: (isStreaming: boolean) => void; }): EuiCommentProps[] => { @@ -187,7 +185,7 @@ export const getComments = ({ return { ...messageProps, - actions: , + actions: , children: ( { @@ -31,16 +30,10 @@ export const AssistantOverlay: React.FC = () => { }); const { assistantAvailability } = useAssistantContext(); - const aiAssistantFlyoutMode = useIsExperimentalFeatureEnabled('aiAssistantFlyoutMode'); if (!assistantAvailability.hasAssistantPrivilege) { return null; } - return ( - - ); + return ; }; diff --git a/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.tsx b/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.tsx index 525315ca36eb2..31f3910fdab49 100644 --- a/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.tsx +++ b/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.tsx @@ -16,13 +16,10 @@ import { } from '@kbn/elastic-assistant'; import { useConversation } from '@kbn/elastic-assistant/impl/assistant/use_conversation'; import type { FetchConversationsResponse } from '@kbn/elastic-assistant/impl/assistant/api'; -import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; const defaultSelectedConversationId = WELCOME_CONVERSATION_TITLE; export const ManagementSettings = React.memo(() => { - const isFlyoutMode = useIsExperimentalFeatureEnabled('aiAssistantFlyoutMode'); - const { baseConversations, http, @@ -49,8 +46,8 @@ export const ManagementSettings = React.memo(() => { const currentConversation = useMemo( () => conversations?.[defaultSelectedConversationId] ?? - getDefaultConversation({ cTitle: WELCOME_CONVERSATION_TITLE, isFlyoutMode }), - [conversations, getDefaultConversation, isFlyoutMode] + getDefaultConversation({ cTitle: WELCOME_CONVERSATION_TITLE }), + [conversations, getDefaultConversation] ); if (conversations) { @@ -58,7 +55,6 @@ export const ManagementSettings = React.memo(() => { diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/header/index.test.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/header/index.test.tsx index 938da7f930d51..4f242cedfc92e 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/header/index.test.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/header/index.test.tsx @@ -38,7 +38,7 @@ describe('Header', () => { ); - const connectorSelector = screen.getByTestId('connectorSelectorPlaceholderButton'); + const connectorSelector = screen.getByTestId('addNewConnectorButton'); expect(connectorSelector).toBeInTheDocument(); }); @@ -61,7 +61,7 @@ describe('Header', () => { ); - const connectorSelector = screen.queryByTestId('connectorSelectorPlaceholderButton'); + const connectorSelector = screen.queryByTestId('addNewConnectorButton'); expect(connectorSelector).not.toBeInTheDocument(); }); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/header/index.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/header/index.tsx index 78ad8db6d2f6e..fa4a9caa3dcb1 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/header/index.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/header/index.tsx @@ -38,7 +38,6 @@ const HeaderComponent: React.FC = ({ onCancel, stats, }) => { - const isFlyoutMode = false; // always false for attack discovery const { hasAssistantPrivilege } = useAssistantAvailability(); const { euiTheme } = useEuiTheme(); const disabled = !hasAssistantPrivilege || connectorId == null; @@ -85,7 +84,6 @@ const HeaderComponent: React.FC = ({ {connectorsAreConfigured && ( { expect(bodyText).toHaveTextContent(FIRST_SET_UP); }); - - it('renders connector prompt', () => { - const connectorPrompt = screen.getByTestId('prompt'); - - expect(connectorPrompt).toBeInTheDocument(); - }); }); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/welcome/index.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/welcome/index.tsx index 3f8488f6f64b4..7ab90b524bb93 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/welcome/index.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/welcome/index.tsx @@ -6,21 +6,13 @@ */ import { AssistantAvatar } from '@kbn/elastic-assistant'; -import { useConnectorSetup } from '@kbn/elastic-assistant/impl/connectorland/connector_setup'; +import { ConnectorSetup } from '@kbn/elastic-assistant/impl/connectorland/connector_setup'; import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui'; import React, { useMemo } from 'react'; -import { noop } from 'lodash/fp'; import * as i18n from './translations'; const WelcomeComponent: React.FC = () => { - const { prompt: connectorPrompt } = useConnectorSetup({ - isFlyoutMode: true, // prevents the "Click to skip" button from showing - onConversationUpdate: async () => {}, - onSetupComplete: noop, // this callback cannot be used to select a connector, so it's not used - updateConversationsOnSaveConnector: false, // no conversation to update - }); - const title = useMemo( () => ( { - {connectorPrompt} + + {}} + updateConversationsOnSaveConnector={false} // no conversation to update + /> + ); }; diff --git a/x-pack/plugins/security_solution/public/common/lib/process_actions/index.ts b/x-pack/plugins/security_solution/public/common/lib/process_actions/index.ts index ef38144a5c53c..b8cb7c04f469c 100644 --- a/x-pack/plugins/security_solution/public/common/lib/process_actions/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/process_actions/index.ts @@ -6,16 +6,15 @@ */ import type { - KillOrSuspendProcessRequestBody, ResponseActionApiResponse, + KillProcessRequestBody, + SuspendProcessRequestBody, } from '../../../../common/endpoint/types'; import { KibanaServices } from '../kibana'; import { KILL_PROCESS_ROUTE, SUSPEND_PROCESS_ROUTE } from '../../../../common/endpoint/constants'; /** Kills a process specified by pid or entity id on a host running Endpoint Security */ -export const killProcess = ( - params: KillOrSuspendProcessRequestBody -): Promise => { +export const killProcess = (params: KillProcessRequestBody): Promise => { return KibanaServices.get().http.post(KILL_PROCESS_ROUTE, { body: JSON.stringify(params), version: '2023-10-31', @@ -24,7 +23,7 @@ export const killProcess = ( /** Suspends a process specified by pid or entity id on a host running Endpoint Security */ export const suspendProcess = ( - params: KillOrSuspendProcessRequestBody + params: SuspendProcessRequestBody ): Promise => { return KibanaServices.get().http.post(SUSPEND_PROCESS_ROUTE, { body: JSON.stringify(params), diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/index.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/index.tsx index 4c92512e13344..c863904da6c66 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/index.tsx @@ -14,7 +14,7 @@ import { RiskInputsTab } from './tabs/risk_inputs/risk_inputs_tab'; export const RISK_INPUTS_TAB_TEST_ID = `${PREFIX}RiskInputsTab` as const; -export const getRiskInputTab = ({ entityType, entityName }: RiskInputsTabProps) => ({ +export const getRiskInputTab = ({ entityType, entityName, scopeId }: RiskInputsTabProps) => ({ id: EntityDetailsLeftPanelTab.RISK_INPUTS, 'data-test-subj': RISK_INPUTS_TAB_TEST_ID, name: ( @@ -23,5 +23,5 @@ export const getRiskInputTab = ({ entityType, entityName }: RiskInputsTabProps) defaultMessage="Risk contributions" /> ), - content: , + content: , }); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/risk_inputs.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/risk_inputs.test.tsx index 8524a2a6a26ea..d6a247f6558a7 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/risk_inputs.test.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/risk_inputs.test.tsx @@ -9,7 +9,7 @@ import { render } from '@testing-library/react'; import React from 'react'; import { TestProviders } from '../../../../../common/mock'; import { times } from 'lodash/fp'; -import { RiskInputsTab } from './risk_inputs_tab'; +import { EXPAND_ALERT_TEST_ID, RiskInputsTab } from './risk_inputs_tab'; import { alertInputDataMock } from '../../mocks'; import { RiskSeverity } from '../../../../../../common/search_strategy'; import { RiskScoreEntity } from '../../../../../../common/entity_analytics/risk_engine'; @@ -49,6 +49,12 @@ const riskScore = { }, }; +const mockUseIsExperimentalFeatureEnabled = jest.fn().mockReturnValue(false); + +jest.mock('../../../../../common/hooks/use_experimental_features', () => ({ + useIsExperimentalFeatureEnabled: () => mockUseIsExperimentalFeatureEnabled(), +})); + const riskScoreWithAssetCriticalityContribution = (contribution: number) => { const score = JSON.parse(JSON.stringify(riskScore)); score.user.risk.category_2_score = contribution; @@ -74,7 +80,7 @@ describe('RiskInputsTab', () => { const { getByTestId, queryByTestId } = render( - + ); @@ -87,7 +93,7 @@ describe('RiskInputsTab', () => { const { queryByTestId } = render( - + ); @@ -116,13 +122,57 @@ describe('RiskInputsTab', () => { const { queryByTestId } = render( - + ); expect(queryByTestId('risk-input-contexts-title')).toBeInTheDocument(); }); + it('it renders alert preview button when feature flag is enable', () => { + mockUseIsExperimentalFeatureEnabled.mockReturnValue(true); + mockUseRiskScore.mockReturnValue({ + loading: false, + error: false, + data: [riskScore], + }); + mockUseRiskContributingAlerts.mockReturnValue({ + loading: false, + error: false, + data: [alertInputDataMock], + }); + + const { getByTestId } = render( + + + + ); + + expect(getByTestId(EXPAND_ALERT_TEST_ID)).toBeInTheDocument(); + }); + + it('it does not render alert preview button when feature flag is disable', () => { + mockUseIsExperimentalFeatureEnabled.mockReturnValue(false); + mockUseRiskScore.mockReturnValue({ + loading: false, + error: false, + data: [riskScore], + }); + mockUseRiskContributingAlerts.mockReturnValue({ + loading: false, + error: false, + data: [alertInputDataMock], + }); + + const { queryByTestId } = render( + + + + ); + + expect(queryByTestId(EXPAND_ALERT_TEST_ID)).not.toBeInTheDocument(); + }); + it('Displays 0.00 for the asset criticality contribution if the contribution value is less than -0.01', () => { mockUseUiSetting.mockReturnValue([true]); @@ -134,7 +184,7 @@ describe('RiskInputsTab', () => { const { getByTestId } = render( - + ); const contextsTable = getByTestId('risk-input-contexts-table'); @@ -153,7 +203,7 @@ describe('RiskInputsTab', () => { const { getByTestId } = render( - + ); const contextsTable = getByTestId('risk-input-contexts-table'); @@ -172,7 +222,7 @@ describe('RiskInputsTab', () => { const { getByTestId } = render( - + ); @@ -201,7 +251,7 @@ describe('RiskInputsTab', () => { const { queryByTestId } = render( - + ); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/risk_inputs_tab.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/risk_inputs_tab.tsx index 48f004cbd7069..f4514c6bf8f2b 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/risk_inputs_tab.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/risk_inputs_tab.tsx @@ -14,6 +14,8 @@ import { useUiSetting$ } from '@kbn/kibana-react-plugin/public'; import { ALERT_RULE_NAME } from '@kbn/rule-data-utils'; import { get } from 'lodash/fp'; +import { AlertPreviewButton } from '../../../../../flyout/shared/components/alert_preview_button'; +import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; import { useGlobalTime } from '../../../../../common/containers/use_global_time'; import { useQueryInspector } from '../../../../../common/components/page/manage_query'; import { formatRiskScore } from '../../../../common'; @@ -40,6 +42,7 @@ import { ActionColumn } from '../../components/action_column'; export interface RiskInputsTabProps extends Record { entityType: RiskScoreEntity; entityName: string; + scopeId: string; } const FIRST_RECORD_PAGINATION = { @@ -47,9 +50,10 @@ const FIRST_RECORD_PAGINATION = { querySize: 1, }; +export const EXPAND_ALERT_TEST_ID = 'risk-input-alert-preview-button'; export const RISK_INPUTS_TAB_QUERY_ID = 'RiskInputsTabQuery'; -export const RiskInputsTab = ({ entityType, entityName }: RiskInputsTabProps) => { +export const RiskInputsTab = ({ entityType, entityName, scopeId }: RiskInputsTabProps) => { const { setQuery, deleteQuery } = useGlobalTime(); const [selectedItems, setSelectedItems] = useState([]); @@ -96,9 +100,26 @@ export const RiskInputsTab = ({ entityType, entityName }: RiskInputsTabProps) => }), [] ); + const isPreviewEnabled = useIsExperimentalFeatureEnabled('entityAlertPreviewEnabled'); const inputColumns: Array> = useMemo( () => [ + ...(isPreviewEnabled + ? [ + { + render: (data: InputAlert) => ( + + ), + width: '5%', + }, + ] + : []), + { name: ( render: formatContribution, }, ], - [] + [isPreviewEnabled, scopeId] ); const [isAssetCriticalityEnabled] = useUiSetting$(ENABLE_ASSET_CRITICALITY_SETTING); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/correlations_details_alerts_table.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/correlations_details_alerts_table.test.tsx index 1fce080352a08..fec6a1efaa08f 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/correlations_details_alerts_table.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/correlations_details_alerts_table.test.tsx @@ -16,7 +16,7 @@ import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_ex import { mockFlyoutApi } from '../../shared/mocks/mock_flyout_context'; import { mockContextValue } from '../../shared/mocks/mock_context'; import { DocumentDetailsPreviewPanelKey } from '../../shared/constants/panel_keys'; -import { ALERT_PREVIEW_BANNER } from '../../preview'; +import { ALERT_PREVIEW_BANNER } from '../../preview/constants'; import { DocumentDetailsContext } from '../../shared/context'; jest.mock('../hooks/use_paginated_alerts'); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/correlations_details_alerts_table.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/correlations_details_alerts_table.tsx index bf1a28201fc87..5253aa1cd272b 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/correlations_details_alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/correlations_details_alerts_table.tsx @@ -7,16 +7,14 @@ import type { ReactElement, ReactNode } from 'react'; import React, { type FC, useMemo, useCallback } from 'react'; -import { type Criteria, EuiBasicTable, formatDate, EuiButtonIcon } from '@elastic/eui'; +import { type Criteria, EuiBasicTable, formatDate } from '@elastic/eui'; import { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; import type { Filter } from '@kbn/es-query'; import { isRight } from 'fp-ts/lib/Either'; -import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { ALERT_REASON, ALERT_RULE_NAME } from '@kbn/rule-data-utils'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; -import { useDocumentDetailsContext } from '../../shared/context'; import { CORRELATIONS_DETAILS_ALERT_PREVIEW_BUTTON_TEST_ID } from './test_ids'; import { CellTooltipWrapper } from '../../shared/components/cell_tooltip_wrapper'; import type { DataProvider } from '../../../../../common/types'; @@ -26,51 +24,11 @@ import { ExpandablePanel } from '../../../shared/components/expandable_panel'; import { InvestigateInTimelineButton } from '../../../../common/components/event_details/table/investigate_in_timeline_button'; import { ACTION_INVESTIGATE_IN_TIMELINE } from '../../../../detections/components/alerts_table/translations'; import { getDataProvider } from '../../../../common/components/event_details/table/use_action_cell_data_provider'; -import { DocumentDetailsPreviewPanelKey } from '../../shared/constants/panel_keys'; -import { ALERT_PREVIEW_BANNER } from '../../preview'; +import { AlertPreviewButton } from '../../../shared/components/alert_preview_button'; export const TIMESTAMP_DATE_FORMAT = 'MMM D, YYYY @ HH:mm:ss.SSS'; const dataProviderLimit = 5; -interface AlertPreviewButtonProps { - /** - * Id of the document - */ - id: string; - /** - * Name of the index used in the parent's page - */ - indexName: string; -} - -const AlertPreviewButton: FC = ({ id, indexName }) => { - const { openPreviewPanel } = useExpandableFlyoutApi(); - const { scopeId } = useDocumentDetailsContext(); - - const openAlertPreview = useCallback( - () => - openPreviewPanel({ - id: DocumentDetailsPreviewPanelKey, - params: { - id, - indexName, - scopeId, - isPreviewMode: true, - banner: ALERT_PREVIEW_BANNER, - }, - }), - [openPreviewPanel, id, indexName, scopeId] - ); - - return ( - - ); -}; - export interface CorrelationsDetailsAlertsTableProps { /** * Text to display in the ExpandablePanel title section @@ -172,7 +130,12 @@ export const CorrelationsDetailsAlertsTable: FC) => ( - + ), width: '5%', }, @@ -247,7 +210,7 @@ export const CorrelationsDetailsAlertsTable: FC ({ describe('HostDetailsPanel', () => { it('render risk inputs panel', () => { - const { getByTestId } = render(, { - wrapper: TestProviders, - }); + const { getByTestId } = render( + , + { + wrapper: TestProviders, + } + ); expect(getByTestId(RISK_INPUTS_TAB_TEST_ID)).toBeInTheDocument(); }); it("doesn't render risk inputs panel when no alerts ids are provided", () => { - const { queryByTestId } = render(, { - wrapper: TestProviders, - }); + const { queryByTestId } = render( + , + { + wrapper: TestProviders, + } + ); expect(queryByTestId(RISK_INPUTS_TAB_TEST_ID)).not.toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.tsx index 853a68c4fb95e..ba34ac3d8aa3a 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.tsx @@ -18,6 +18,7 @@ import { RiskScoreEntity } from '../../../../common/entity_analytics/risk_engine export interface HostDetailsPanelProps extends Record { isRiskScoreExist: boolean; name: string; + scopeId: string; } export interface HostDetailsExpandableFlyoutProps extends FlyoutPanelProps { key: 'host_details'; @@ -25,18 +26,18 @@ export interface HostDetailsExpandableFlyoutProps extends FlyoutPanelProps { } export const HostDetailsPanelKey: HostDetailsExpandableFlyoutProps['key'] = 'host_details'; -export const HostDetailsPanel = ({ name, isRiskScoreExist }: HostDetailsPanelProps) => { +export const HostDetailsPanel = ({ name, isRiskScoreExist, scopeId }: HostDetailsPanelProps) => { // Temporary implementation while Host details left panel don't have Asset tabs const [tabs, selectedTabId, setSelectedTabId] = useMemo(() => { const isRiskScoreTabAvailable = isRiskScoreExist && name; return [ isRiskScoreTabAvailable - ? [getRiskInputTab({ entityName: name, entityType: RiskScoreEntity.host })] + ? [getRiskInputTab({ entityName: name, entityType: RiskScoreEntity.host, scopeId })] : [], EntityDetailsLeftPanelTab.RISK_INPUTS, () => {}, ]; - }, [name, isRiskScoreExist]); + }, [name, isRiskScoreExist, scopeId]); return ( <> diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx index 518e423fe0077..798bff18b9c16 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx @@ -112,12 +112,13 @@ export const HostPanel = ({ id: HostDetailsPanelKey, params: { name: hostName, + scopeId, isRiskScoreExist, path: tab ? { tab } : undefined, }, }); }, - [telemetry, openLeftPanel, hostName, isRiskScoreExist] + [telemetry, openLeftPanel, hostName, isRiskScoreExist, scopeId] ); const openDefaultPanel = useCallback(() => openTabPanel(), [openTabPanel]); diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.test.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.test.tsx index 2c88a0ecdfb0b..bdff465e0b982 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.test.tsx @@ -20,6 +20,7 @@ describe('LeftPanel', () => { }} isRiskScoreExist user={{ name: 'test user', email: [] }} + scopeId={'scopeId'} />, { wrapper: TestProviders, @@ -39,6 +40,7 @@ describe('LeftPanel', () => { }} isRiskScoreExist={false} user={{ name: 'test user', email: [] }} + scopeId={'scopeId'} />, { wrapper: TestProviders, diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.tsx index 3a682ba125864..757c6799a6da1 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.tsx @@ -27,6 +27,7 @@ export interface UserDetailsPanelProps extends Record { isRiskScoreExist: boolean; user: UserParam; path?: PanelPath; + scopeId: string; } export interface UserDetailsExpandableFlyoutProps extends FlyoutPanelProps { key: 'user_details'; @@ -34,9 +35,14 @@ export interface UserDetailsExpandableFlyoutProps extends FlyoutPanelProps { } export const UserDetailsPanelKey: UserDetailsExpandableFlyoutProps['key'] = 'user_details'; -export const UserDetailsPanel = ({ isRiskScoreExist, user, path }: UserDetailsPanelProps) => { +export const UserDetailsPanel = ({ + isRiskScoreExist, + user, + path, + scopeId, +}: UserDetailsPanelProps) => { const managedUser = useManagedUser(user.name, user.email); - const tabs = useTabs(managedUser.data, user.name, isRiskScoreExist); + const tabs = useTabs(managedUser.data, user.name, isRiskScoreExist, scopeId); const { selectedTabId, setSelectedTabId } = useSelectedTab(isRiskScoreExist, user, tabs, path); if (managedUser.isLoading) return ; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/tabs.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/tabs.tsx index 358dd5357ae2f..3a6814a28e62c 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/tabs.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/tabs.tsx @@ -25,7 +25,8 @@ import { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_ export const useTabs = ( managedUser: ManagedUserHits, name: string, - isRiskScoreExist: boolean + isRiskScoreExist: boolean, + scopeId: string ): LeftPanelTabsType => useMemo(() => { const tabs: LeftPanelTabsType = []; @@ -37,6 +38,7 @@ export const useTabs = ( getRiskInputTab({ entityName: name, entityType: RiskScoreEntity.user, + scopeId, }) ); } @@ -50,7 +52,7 @@ export const useTabs = ( } return tabs; - }, [isRiskScoreExist, managedUser, name]); + }, [isRiskScoreExist, managedUser, name, scopeId]); const getOktaTab = (oktaManagedUser: ManagedUserHit) => ({ id: EntityDetailsLeftPanelTab.OKTA, diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx index bdfc967fd1508..97f36c3a525c5 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx @@ -115,6 +115,7 @@ export const UserPanel = ({ id: UserDetailsPanelKey, params: { isRiskScoreExist: !!userRiskData?.user?.risk, + scopeId, user: { name: userName, email, @@ -123,7 +124,7 @@ export const UserPanel = ({ path: tab ? { tab } : undefined, }); }, - [telemetry, email, openLeftPanel, userName, userRiskData] + [telemetry, openLeftPanel, userRiskData?.user?.risk, userName, email, scopeId] ); const openPanelFirstTab = useCallback(() => openPanelTab(), [openPanelTab]); diff --git a/x-pack/plugins/security_solution/public/flyout/shared/components/alert_preview_button.test.tsx b/x-pack/plugins/security_solution/public/flyout/shared/components/alert_preview_button.test.tsx new file mode 100644 index 0000000000000..f478812f96f93 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/shared/components/alert_preview_button.test.tsx @@ -0,0 +1,65 @@ +/* + * 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 { fireEvent, render } from '@testing-library/react'; +import { ExpandableFlyoutProvider } from '@kbn/expandable-flyout'; +import React from 'react'; +import { AlertPreviewButton } from './alert_preview_button'; +import { DocumentDetailsPreviewPanelKey } from '../../document_details/shared/constants/panel_keys'; +import { ALERT_PREVIEW_BANNER } from '../../document_details/preview/constants'; + +const mockOpenPreviewPanel = jest.fn(); +jest.mock('@kbn/expandable-flyout', () => { + return { + useExpandableFlyoutApi: () => ({ + openPreviewPanel: mockOpenPreviewPanel, + }), + }; +}); + +describe('AlertPreviewButton', () => { + it('renders the icon', () => { + const { getByTestId } = render( + , + { wrapper: ExpandableFlyoutProvider } + ); + expect(getByTestId('alertPreviewButton')).toBeInTheDocument(); + }); + + it('opens the preview panel when clicked', () => { + const id = '1'; + const indexName = 'index'; + const scopeId = 'scope'; + + const { getByTestId } = render( + , + { wrapper: ExpandableFlyoutProvider } + ); + fireEvent.click(getByTestId('alertPreviewButton')); + + expect(mockOpenPreviewPanel).toHaveBeenCalledWith({ + id: DocumentDetailsPreviewPanelKey, + params: { + id, + indexName, + scopeId, + isPreviewMode: true, + banner: ALERT_PREVIEW_BANNER, + }, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/shared/components/alert_preview_button.tsx b/x-pack/plugins/security_solution/public/flyout/shared/components/alert_preview_button.tsx new file mode 100644 index 0000000000000..62426476e3c1f --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/shared/components/alert_preview_button.tsx @@ -0,0 +1,72 @@ +/* + * 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 { EuiButtonIcon } from '@elastic/eui'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import React, { useCallback } from 'react'; +import type { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { ALERT_PREVIEW_BANNER } from '../../document_details/preview/constants'; +import { DocumentDetailsPreviewPanelKey } from '../../document_details/shared/constants/panel_keys'; + +interface AlertPreviewButtonProps { + /** + * Name of the index used in the parent's page + */ + indexName: string; + /** + * Id of the alert to preview + */ + id: string; + /** + * Data attribute used for testing. + */ + 'data-test-subj'?: string; + /** + * Maintain backwards compatibility // TODO remove when possible + */ + scopeId: string; +} + +/** + * Icon button showed on tables to launch a preview of the alert details panel. + */ +export const AlertPreviewButton: FC = ({ + id, + indexName, + 'data-test-subj': dataTestSubj, + scopeId, +}) => { + const { openPreviewPanel } = useExpandableFlyoutApi(); + + const openAlertPreview = useCallback( + () => + openPreviewPanel({ + id: DocumentDetailsPreviewPanelKey, + params: { + id, + indexName, + scopeId, + isPreviewMode: true, + banner: ALERT_PREVIEW_BANNER, + }, + }), + [openPreviewPanel, id, indexName, scopeId] + ); + + return ( + + ); +}; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/kill_process_action.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/kill_process_action.tsx index 657b6847e7839..a6b2951615381 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/kill_process_action.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/kill_process_action.tsx @@ -6,18 +6,18 @@ */ import { memo, useMemo } from 'react'; -import type { KillOrSuspendProcessRequestBody } from '../../../../../common/endpoint/types'; import { parsedPidOrEntityIdParameter } from '../lib/utils'; import { useSendKillProcessRequest } from '../../../hooks/response_actions/use_send_kill_process_endpoint_request'; import type { ActionRequestComponentProps } from '../types'; import { useConsoleActionSubmitter } from '../hooks/use_console_action_submitter'; +import type { KillProcessRequestBody } from '../../../../../common/endpoint/types'; export const KillProcessActionResult = memo< ActionRequestComponentProps<{ pid?: string[]; entityId?: string[] }> >(({ command, setStore, store, status, setStatus, ResultComponent }) => { const actionCreator = useSendKillProcessRequest(); - const actionRequestBody = useMemo(() => { + const actionRequestBody = useMemo(() => { const endpointId = command.commandDefinition?.meta?.endpointId; const parameters = parsedPidOrEntityIdParameter(command.args.args); @@ -30,7 +30,7 @@ export const KillProcessActionResult = memo< : undefined; }, [command.args.args, command.commandDefinition?.meta?.endpointId]); - return useConsoleActionSubmitter({ + return useConsoleActionSubmitter({ ResultComponent, setStore, store, diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/suspend_process_action.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/suspend_process_action.tsx index fbb18aabd7b00..70893689c0439 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/suspend_process_action.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/suspend_process_action.tsx @@ -8,8 +8,8 @@ import { memo, useMemo } from 'react'; import { parsedPidOrEntityIdParameter } from '../lib/utils'; import type { - KillOrSuspendProcessRequestBody, SuspendProcessActionOutputContent, + SuspendProcessRequestBody, } from '../../../../../common/endpoint/types'; import { useSendSuspendProcessRequest } from '../../../hooks/response_actions/use_send_suspend_process_endpoint_request'; import type { ActionRequestComponentProps } from '../types'; @@ -20,7 +20,7 @@ export const SuspendProcessActionResult = memo< >(({ command, setStore, store, status, setStatus, ResultComponent }) => { const actionCreator = useSendSuspendProcessRequest(); - const actionRequestBody = useMemo(() => { + const actionRequestBody = useMemo(() => { const endpointId = command.commandDefinition?.meta?.endpointId; const parameters = parsedPidOrEntityIdParameter(command.args.args); @@ -33,10 +33,7 @@ export const SuspendProcessActionResult = memo< : undefined; }, [command.args.args, command.commandDefinition?.meta?.endpointId]); - return useConsoleActionSubmitter< - KillOrSuspendProcessRequestBody, - SuspendProcessActionOutputContent - >({ + return useConsoleActionSubmitter({ ResultComponent, setStore, store, diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts index 84624c2ce595b..b57bd57ecd85b 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts @@ -571,9 +571,13 @@ const adjustCommandsForSentinelOne = ({ }: { commandList: CommandDefinition[]; }): CommandDefinition[] => { + const featureFlags = ExperimentalFeaturesService.get(); + const isKillProcessEnabled = featureFlags.responseActionsSentinelOneKillProcessEnabled; + return commandList.map((command) => { if ( command.name === 'status' || + (command.name === 'kill-process' && !isKillProcessEnabled) || !isAgentTypeAndActionSupported( 'sentinel_one', RESPONSE_CONSOLE_COMMAND_TO_API_COMMAND_MAP[command.name as ConsoleResponseActionCommands], diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/utils.ts b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/utils.ts index d5c40347ebf80..41c57feb5c76c 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/utils.ts +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/utils.ts @@ -4,12 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { ResponseActionParametersWithPidOrEntityId } from '../../../../../common/endpoint/types'; +import type { + ResponseActionParametersWithEntityId, + ResponseActionParametersWithPid, +} from '../../../../../common/endpoint/types'; export const parsedPidOrEntityIdParameter = (parameters: { pid?: string[]; entityId?: string[]; -}): ResponseActionParametersWithPidOrEntityId => { +}): ResponseActionParametersWithPid | ResponseActionParametersWithEntityId => { if (parameters.pid) { return { pid: Number(parameters.pid[0]) }; } diff --git a/x-pack/plugins/security_solution/public/management/hooks/response_actions/use_send_kill_process_endpoint_request.ts b/x-pack/plugins/security_solution/public/management/hooks/response_actions/use_send_kill_process_endpoint_request.ts index 2d86f15f81d40..b17d34ab4e463 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/response_actions/use_send_kill_process_endpoint_request.ts +++ b/x-pack/plugins/security_solution/public/management/hooks/response_actions/use_send_kill_process_endpoint_request.ts @@ -9,8 +9,8 @@ import type { UseMutationOptions, UseMutationResult } from '@tanstack/react-quer import { useMutation } from '@tanstack/react-query'; import type { IHttpFetchError } from '@kbn/core-http-browser'; import type { - KillOrSuspendProcessRequestBody, ResponseActionApiResponse, + KillProcessRequestBody, } from '../../../../common/endpoint/types'; import { killProcess } from '../../../common/lib/process_actions'; @@ -22,15 +22,11 @@ export const useSendKillProcessRequest = ( customOptions?: UseMutationOptions< ResponseActionApiResponse, IHttpFetchError, - KillOrSuspendProcessRequestBody + KillProcessRequestBody > -): UseMutationResult< - ResponseActionApiResponse, - IHttpFetchError, - KillOrSuspendProcessRequestBody -> => { - return useMutation( - (processData: KillOrSuspendProcessRequestBody) => { +): UseMutationResult => { + return useMutation( + (processData: KillProcessRequestBody) => { return killProcess(processData); }, customOptions diff --git a/x-pack/plugins/security_solution/public/management/hooks/response_actions/use_send_suspend_process_endpoint_request.ts b/x-pack/plugins/security_solution/public/management/hooks/response_actions/use_send_suspend_process_endpoint_request.ts index d6c2f56cb627f..787344ffd54dc 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/response_actions/use_send_suspend_process_endpoint_request.ts +++ b/x-pack/plugins/security_solution/public/management/hooks/response_actions/use_send_suspend_process_endpoint_request.ts @@ -9,8 +9,8 @@ import { useMutation } from '@tanstack/react-query'; import type { UseMutationOptions, UseMutationResult } from '@tanstack/react-query'; import type { IHttpFetchError } from '@kbn/core-http-browser'; import type { - KillOrSuspendProcessRequestBody, ResponseActionApiResponse, + SuspendProcessRequestBody, } from '../../../../common/endpoint/types'; import { suspendProcess } from '../../../common/lib/process_actions'; @@ -22,15 +22,11 @@ export const useSendSuspendProcessRequest = ( customOptions?: UseMutationOptions< ResponseActionApiResponse, IHttpFetchError, - KillOrSuspendProcessRequestBody + SuspendProcessRequestBody > -): UseMutationResult< - ResponseActionApiResponse, - IHttpFetchError, - KillOrSuspendProcessRequestBody -> => { - return useMutation( - (processData: KillOrSuspendProcessRequestBody) => { +): UseMutationResult => { + return useMutation( + (processData: SuspendProcessRequestBody) => { return suspendProcess(processData); }, customOptions diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/assistant/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/assistant/index.tsx deleted file mode 100644 index a71b7c8231f59..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/assistant/index.tsx +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import styled from 'styled-components'; -import { Assistant } from '@kbn/elastic-assistant'; -import type { Dispatch, SetStateAction } from 'react'; -import React, { memo } from 'react'; -import { TIMELINE_CONVERSATION_TITLE } from '../../../../../assistant/content/conversations/translations'; - -const AssistantTabContainer = styled.div` - overflow-y: auto; - width: 100%; -`; - -const AssistantTab: React.FC<{ - shouldRefocusPrompt: boolean; - setConversationTitle: Dispatch>; -}> = memo(({ shouldRefocusPrompt, setConversationTitle }) => ( - - - -)); - -AssistantTab.displayName = 'AssistantTab'; - -// eslint-disable-next-line import/no-default-export -export { AssistantTab as default }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/index.tsx index 643a5b54be415..7f5ab6f316a62 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/index.tsx @@ -6,17 +6,13 @@ */ import { EuiBadge, EuiSkeletonText, EuiTabs, EuiTab } from '@elastic/eui'; -import { css } from '@emotion/react'; import { isEmpty } from 'lodash/fp'; -import type { Ref, ReactElement, ComponentType, Dispatch, SetStateAction } from 'react'; -import React, { lazy, memo, Suspense, useCallback, useEffect, useMemo, useState } from 'react'; +import type { Ref, ReactElement, ComponentType } from 'react'; +import React, { lazy, memo, Suspense, useCallback, useEffect, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import { useEsqlAvailability } from '../../../../common/hooks/esql/use_esql_availability'; -import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; -import { useAssistantTelemetry } from '../../../../assistant/use_assistant_telemetry'; -import { useAssistantAvailability } from '../../../../assistant/use_assistant_availability'; import type { SessionViewConfig } from '../../../../../common/types'; import type { RowRenderer, TimelineId } from '../../../../../common/types/timeline'; import { TimelineTabs } from '../../../../../common/types/timeline'; @@ -41,7 +37,6 @@ import { } from './selectors'; import * as i18n from './translations'; import { useLicense } from '../../../../common/hooks/use_license'; -import { TIMELINE_CONVERSATION_TITLE } from '../../../../assistant/content/conversations/translations'; import { initializeTimelineSettings } from '../../../store/actions'; import { selectTimelineESQLSavedSearchId } from '../../../store/selectors'; @@ -96,7 +91,6 @@ interface BasicTimelineTab { type ActiveTimelineTabProps = BasicTimelineTab & { activeTimelineTab: TimelineTabs; showTimeline: boolean; - setConversationTitle: Dispatch>; }; const ActiveTimelineTab = memo( @@ -106,10 +100,8 @@ const ActiveTimelineTab = memo( rowRenderers, timelineId, timelineType, - setConversationTitle, showTimeline, }) => { - const { hasAssistantPrivilege } = useAssistantAvailability(); const { isTimelineEsqlEnabledByFeatureFlag, isEsqlAdvancedSettingEnabled } = useEsqlAvailability(); const timelineESQLSavedSearch = useShallowEqualSelector((state) => @@ -124,7 +116,6 @@ const ActiveTimelineTab = memo( } return isEsqlAdvancedSettingEnabled || timelineESQLSavedSearch != null; }, [isEsqlAdvancedSettingEnabled, isTimelineEsqlEnabledByFeatureFlag, timelineESQLSavedSearch]); - const aiAssistantFlyoutMode = useIsExperimentalFeatureEnabled('aiAssistantFlyoutMode'); const getTab = useCallback( (tab: TimelineTabs) => { switch (tab) { @@ -147,33 +138,6 @@ const ActiveTimelineTab = memo( [activeTimelineTab] ); - const getAssistantTab = useCallback(() => { - if (showTimeline) { - const AssistantTab = tabWithSuspense(lazy(() => import('./assistant'))); - return ( - - ); - } else { - return null; - } - }, [activeTimelineTab, setConversationTitle, showTimeline]); - /* Future developer -> why are we doing that * It is really expansive to re-render the QueryTab because the drag/drop * Therefore, we are only hiding its dom when switching to another tab @@ -228,7 +192,6 @@ const ActiveTimelineTab = memo( > {isGraphOrNotesTabs && getTab(activeTimelineTab)} - {hasAssistantPrivilege && !aiAssistantFlyoutMode ? getAssistantTab() : null} ); } @@ -271,8 +234,6 @@ const TabsContentComponent: React.FC = ({ sessionViewConfig, timelineDescription, }) => { - const aiAssistantFlyoutMode = useIsExperimentalFeatureEnabled('aiAssistantFlyoutMode'); - const { hasAssistantPrivilege } = useAssistantAvailability(); const dispatch = useDispatch(); const getActiveTab = useMemo(() => getActiveTabSelector(), []); const getShowTimeline = useMemo(() => getShowTimelineSelector(), []); @@ -312,9 +273,6 @@ const TabsContentComponent: React.FC = ({ const isEnterprisePlus = useLicense().isEnterprise(); - const [conversationTitle, setConversationTitle] = useState(TIMELINE_CONVERSATION_TITLE); - const { reportAssistantInvoked } = useAssistantTelemetry(); - const allTimelineNoteIds = useMemo(() => { const eventNoteIds = Object.values(eventIdToNoteIds).reduce( (acc, v) => [...acc, ...v], @@ -361,16 +319,6 @@ const TabsContentComponent: React.FC = ({ setActiveTab(TimelineTabs.session); }, [setActiveTab]); - const setSecurityAssistantAsActiveTab = useCallback(() => { - setActiveTab(TimelineTabs.securityAssistant); - if (activeTab !== TimelineTabs.securityAssistant) { - reportAssistantInvoked({ - conversationId: conversationTitle, - invokedBy: TIMELINE_CONVERSATION_TITLE, - }); - } - }, [activeTab, conversationTitle, reportAssistantInvoked, setActiveTab]); - const setEsqlAsActiveTab = useCallback(() => { dispatch( initializeTimelineSettings({ @@ -471,17 +419,6 @@ const TabsContentComponent: React.FC = ({ )} - {hasAssistantPrivilege && !aiAssistantFlyoutMode && ( - - {i18n.SECURITY_ASSISTANT} - - )} )} @@ -492,7 +429,6 @@ const TabsContentComponent: React.FC = ({ timelineId={timelineId} timelineType={timelineType} timelineDescription={timelineDescription} - setConversationTitle={setConversationTitle} showTimeline={showTimeline} /> diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index 6f80a018319fa..d7181b3ce49c6 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -11,10 +11,10 @@ import type { Logger, LoggerFactory, SavedObjectsClientContract, + SecurityServiceStart, } from '@kbn/core/server'; import type { ExceptionListClient, ListsServerExtensionRegistrar } from '@kbn/lists-plugin/server'; import type { CasesClient, CasesServerStart } from '@kbn/cases-plugin/server'; -import type { SecurityPluginStart } from '@kbn/security-plugin/server'; import type { FleetFromHostFileClientInterface, FleetStartContract, @@ -68,7 +68,7 @@ export interface EndpointAppContextServiceStartContract { endpointMetadataService: EndpointMetadataService; endpointFleetServicesFactory: EndpointFleetServicesFactoryInterface; manifestManager?: ManifestManager; - security: SecurityPluginStart; + security: SecurityServiceStart; alerting: AlertsPluginStartContract; config: ConfigType; registerIngestCallback?: FleetStartContract['registerExternalCallback']; @@ -93,7 +93,7 @@ export class EndpointAppContextService { private setupDependencies: EndpointAppContextServiceSetupContract | null = null; private startDependencies: EndpointAppContextServiceStartContract | null = null; private fleetServicesFactory: EndpointFleetServicesFactoryInterface | null = null; - public security: SecurityPluginStart | undefined; + public security: SecurityServiceStart | undefined; public setup(dependencies: EndpointAppContextServiceSetupContract) { this.setupDependencies = dependencies; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/response_actions/complete_external_actions_task_runner.ts b/x-pack/plugins/security_solution/server/endpoint/lib/response_actions/complete_external_actions_task_runner.ts index dbc57c2a55a84..0207a3b5d1460 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/response_actions/complete_external_actions_task_runner.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/response_actions/complete_external_actions_task_runner.ts @@ -152,7 +152,7 @@ export class CompleteExternalActionsTaskRunner return null; } - this.errors.push(err.message); + this.errors.push(err.stack); }); } ) @@ -164,7 +164,7 @@ export class CompleteExternalActionsTaskRunner if (this.errors.length) { this.log.error( `${this.errors.length} errors were encountered while running task:\n${this.errors.join( - '\n' + '\n----' )}` ); } diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks/mocks.ts index 3cd77526942ff..9b6f001934910 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks/mocks.ts @@ -15,6 +15,7 @@ import { loggingSystemMock, savedObjectsClientMock, savedObjectsServiceMock, + securityServiceMock, } from '@kbn/core/server/mocks'; import type { IRouter, @@ -154,7 +155,7 @@ export const createMockEndpointAppContextServiceStartContract = const logger = loggingSystemMock.create().get('mock_endpoint_app_context'); const savedObjectsStart = savedObjectsServiceMock.createStartContract(); - const security = securityMock.createStart(); + const security = securityServiceMock.createStart(); const agentService = createMockAgentService(); const agentPolicyService = createMockAgentPolicyService(); const packagePolicyService = createPackagePolicyServiceMock(); @@ -196,10 +197,6 @@ export const createMockEndpointAppContextServiceStartContract = securityMock.createMockAuthenticatedUser({ roles: ['superuser'] }) ); - security.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue( - jest.fn(() => ({ privileges: { kibana: [] } })) - ); - const casesMock = casesPluginMock.createStartContract(); const fleetActionsClientMock = createFleetActionsClientMock(); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_download_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_download_handler.ts index cb4afe4496472..0037d5dded81f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_download_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_download_handler.ts @@ -66,11 +66,12 @@ export const getActionFileDownloadRouteHandler = ( return async (context, req, res) => { const { action_id: actionId, file_id: fileId } = req.params; + const coreContext = await context.core; try { - const esClient = (await context.core).elasticsearch.client.asInternalUser; + const esClient = coreContext.elasticsearch.client.asInternalUser; const { agentType } = await getActionAgentType(esClient, actionId); - const user = endpointContext.service.security?.authc.getCurrentUser(req); + const user = coreContext.security.authc.getCurrentUser(); const casesClient = await endpointContext.service.getCasesClient(req); const connectorActions = (await context.actions).getActionsClient(); const responseActionsClient: ResponseActionsClient = getResponseActionsClient(agentType, { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_info_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_info_handler.ts index 0576cec69b01c..4b1d67ff88718 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_info_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_info_handler.ts @@ -36,11 +36,12 @@ export const getActionFileInfoRouteHandler = ( return async (context, req, res) => { const { action_id: requestActionId, file_id: fileId } = req.params; + const coreContext = await context.core; try { - const esClient = (await context.core).elasticsearch.client.asInternalUser; + const esClient = coreContext.elasticsearch.client.asInternalUser; const { agentType } = await getActionAgentType(esClient, requestActionId); - const user = endpointContext.service.security?.authc.getCurrentUser(req); + const user = coreContext.security.authc.getCurrentUser(); const casesClient = await endpointContext.service.getCasesClient(req); const connectorActions = (await context.actions).getActionsClient(); const responseActionsClient: ResponseActionsClient = getResponseActionsClient(agentType, { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts index 8c1ba19ca626d..47099ae060efa 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts @@ -192,11 +192,6 @@ describe('Response actions', () => { }: CallRouteInterface, indexExists?: { endpointDsExists: boolean } ): Promise> => { - const asUser = mockUser ? mockUser : superUser; - (startContract.security.authc.getCurrentUser as jest.Mock).mockImplementationOnce( - () => asUser - ); - const ctx = createRouteHandlerContext(mockScopedClient, mockSavedObjectClient); ctx.securitySolution.getEndpointAuthz.mockResolvedValue( @@ -218,6 +213,9 @@ describe('Response actions', () => { }; } ); + const asUser = mockUser ? mockUser : superUser; + (ctx.core.security.authc.getCurrentUser as jest.Mock).mockImplementationOnce(() => asUser); + const metadataResponse = docGen.generateHostMetadata(); const withErrorResponse = indexErrorResponse ? indexErrorResponse : { statusCode: 201 }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts index 2bbe35f9747b4..723daf576c38e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts @@ -47,10 +47,11 @@ import { import type { ActionDetails, EndpointActionDataParameterTypes, - KillOrSuspendProcessRequestBody, - ResponseActionParametersWithPidOrEntityId, + ResponseActionParametersWithProcessData, ResponseActionsExecuteParameters, ResponseActionScanParameters, + KillProcessRequestBody, + SuspendProcessRequestBody, } from '../../../../common/endpoint/types'; import type { ResponseActionsApiCommandNames } from '../../../../common/endpoint/service/response_actions/constants'; import type { @@ -165,7 +166,7 @@ export function registerResponseActionRoutes( withEndpointAuthz( { all: ['canKillProcess'] }, logger, - responseActionRequestHandler( + responseActionRequestHandler( endpointContext, 'kill-process' ) @@ -188,7 +189,7 @@ export function registerResponseActionRoutes( withEndpointAuthz( { all: ['canSuspendProcess'] }, logger, - responseActionRequestHandler( + responseActionRequestHandler( endpointContext, 'suspend-process' ) @@ -337,8 +338,9 @@ function responseActionRequestHandler { subActionParams: { actionParameters: { comment: - 'Action triggered from Elastic Security by user foo for action 123-345-456: test comment', + 'Action triggered from Elastic Security by user [foo] for action [isolate (action id: 123-345-456)]: test comment', }, command: 'contain', ids: ['1-2-3'], @@ -232,7 +232,7 @@ describe('CrowdstrikeActionsClient class', () => { command: 'lift_containment', ids: ['1-2-3'], comment: - 'Action triggered from Elastic Security by user foo for action 123-345-456: test comment', + 'Action triggered from Elastic Security by user [foo] for action [unisolate (action id: 123-345-456)]: test comment', }, }, }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.ts index 72994508ebc22..958c51014c6a0 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.ts @@ -12,6 +12,7 @@ import { } from '@kbn/stack-connectors-plugin/common/crowdstrike/constants'; import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import type { CrowdstrikeBaseApiResponse } from '@kbn/stack-connectors-plugin/common/crowdstrike/types'; +import { v4 as uuidv4 } from 'uuid'; import type { CrowdstrikeActionRequestCommonMeta } from '../../../../../../common/endpoint/types/crowdstrike'; import type { CommonResponseActionMethodOptions, @@ -37,7 +38,6 @@ import type { NormalizedExternalConnectorClient, NormalizedExternalConnectorClientExecuteOptions, } from '../lib/normalized_external_connector_client'; -import { ELASTIC_RESPONSE_ACTION_MESSAGE } from '../../utils'; export type CrowdstrikeActionsClientOptions = ResponseActionsClientOptions & { connectorActions: NormalizedExternalConnectorClient; @@ -190,19 +190,15 @@ export class CrowdstrikeActionsClient extends ResponseActionsClientImpl { let actionResponse: ActionTypeExecutorResult | undefined; if (!reqIndexOptions.error) { let error = (await this.validateRequest(reqIndexOptions)).error; - const actionCommentMessage = ELASTIC_RESPONSE_ACTION_MESSAGE( - this.options.username, - reqIndexOptions.actionId - ); if (!error) { + if (!reqIndexOptions.actionId) { + reqIndexOptions.actionId = uuidv4(); + } + try { actionResponse = (await this.sendAction(SUB_ACTION.HOST_ACTIONS, { ids: actionRequest.endpoint_ids, - actionParameters: { - comment: reqIndexOptions.comment - ? `${actionCommentMessage}: ${reqIndexOptions.comment}` - : actionCommentMessage, - }, + actionParameters: { comment: this.buildExternalComment(reqIndexOptions) }, command: 'contain', })) as ActionTypeExecutorResult; } catch (err) { @@ -254,18 +250,13 @@ export class CrowdstrikeActionsClient extends ResponseActionsClientImpl { let actionResponse: ActionTypeExecutorResult | undefined; if (!reqIndexOptions.error) { let error = (await this.validateRequest(reqIndexOptions)).error; - const actionCommentMessage = ELASTIC_RESPONSE_ACTION_MESSAGE( - this.options.username, - reqIndexOptions.actionId - ); + if (!error) { try { actionResponse = (await this.sendAction(SUB_ACTION.HOST_ACTIONS, { ids: actionRequest.endpoint_ids, command: 'lift_containment', - comment: reqIndexOptions.comment - ? `${actionCommentMessage}: ${reqIndexOptions.comment}` - : actionCommentMessage, + comment: this.buildExternalComment(reqIndexOptions), })) as ActionTypeExecutorResult; } catch (err) { error = err; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.test.ts index 1fa046496f231..21a4196ba84e5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.test.ts @@ -263,7 +263,8 @@ describe('EndpointActionsClient', () => { }); }); - it('should create an action with error', async () => { + it('should create an action with error and not trow when in automated mode', async () => { + classConstructorOptions.isAutomated = true; await endpointActionsClient.isolate(getCommonResponseActionOptions(), { error: 'something is wrong', }); @@ -283,7 +284,8 @@ describe('EndpointActionsClient', () => { ); }); - it('should create an action with error when agents are invalid', async () => { + it('should create an action with error when agents are invalid (automated mode)', async () => { + classConstructorOptions.isAutomated = true; // @ts-expect-error mocking this for testing purposes endpointActionsClient.checkAgentIds = jest.fn().mockResolvedValueOnce({ isValid: false, diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.ts index 2c908bd1a3f30..59328beb46c12 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.ts @@ -30,12 +30,11 @@ import type { ActionDetails, HostMetadata, GetProcessesActionOutputContent, - KillOrSuspendProcessRequestBody, KillProcessActionOutputContent, ResponseActionExecuteOutputContent, ResponseActionGetFileOutputContent, ResponseActionGetFileParameters, - ResponseActionParametersWithPidOrEntityId, + ResponseActionParametersWithProcessData, ResponseActionsExecuteParameters, ResponseActionUploadOutputContent, ResponseActionUploadParameters, @@ -45,6 +44,8 @@ import type { UploadedFileInfo, ResponseActionScanParameters, ResponseActionScanOutputContent, + KillProcessRequestBody, + SuspendProcessRequestBody, } from '../../../../../../common/endpoint/types'; import type { CommonResponseActionMethodOptions, @@ -107,6 +108,10 @@ export class EndpointActionsClient extends ResponseActionsClientImpl { const { hosts, ruleName, ruleId, error } = this.getMethodOptions(options); let actionError: string | undefined = validationError?.message || error; + if (actionError && !this.options.isAutomated) { + throw new ResponseActionsClientError(actionError, 400); + } + // Dispatch action to Endpoint using Fleet if (!actionError) { try { @@ -242,26 +247,26 @@ export class EndpointActionsClient extends ResponseActionsClientImpl { } async killProcess( - actionRequest: KillOrSuspendProcessRequestBody, + actionRequest: KillProcessRequestBody, options: CommonResponseActionMethodOptions = {} ): Promise< - ActionDetails + ActionDetails > { return this.handleResponseAction< - KillOrSuspendProcessRequestBody, - ActionDetails + KillProcessRequestBody, + ActionDetails >('kill-process', actionRequest, options); } async suspendProcess( - actionRequest: KillOrSuspendProcessRequestBody, + actionRequest: SuspendProcessRequestBody, options: CommonResponseActionMethodOptions = {} ): Promise< - ActionDetails + ActionDetails > { return this.handleResponseAction< - KillOrSuspendProcessRequestBody, - ActionDetails + SuspendProcessRequestBody, + ActionDetails >('suspend-process', actionRequest, options); } diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts index 677165519796b..927875855eb11 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts @@ -13,6 +13,7 @@ import { AttachmentType, ExternalReferenceStorageType } from '@kbn/cases-plugin/ import type { CaseAttachments } from '@kbn/cases-plugin/public/types'; import { i18n } from '@kbn/i18n'; import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { SimpleMemCache } from './simple_mem_cache'; import { validateActionId } from '../../utils/validate_action_id'; import { fetchActionResponses, @@ -49,14 +50,13 @@ import type { EndpointActionDataParameterTypes, EndpointActionResponseDataOutput, GetProcessesActionOutputContent, - KillOrSuspendProcessRequestBody, KillProcessActionOutputContent, LogsEndpointAction, LogsEndpointActionResponse, ResponseActionExecuteOutputContent, ResponseActionGetFileOutputContent, ResponseActionGetFileParameters, - ResponseActionParametersWithPidOrEntityId, + ResponseActionParametersWithProcessData, ResponseActionScanOutputContent, ResponseActionsExecuteParameters, ResponseActionScanParameters, @@ -65,6 +65,8 @@ import type { SuspendProcessActionOutputContent, UploadedFileInfo, WithAllKeys, + KillProcessRequestBody, + SuspendProcessRequestBody, } from '../../../../../../common/endpoint/types'; import type { ExecuteActionRequestBody, @@ -79,6 +81,17 @@ import { stringify } from '../../../../utils/stringify'; import { CASE_ATTACHMENT_ENDPOINT_TYPE_ID } from '../../../../../../common/constants'; import { EMPTY_COMMENT } from '../../../../utils/translations'; +const ELASTIC_RESPONSE_ACTION_MESSAGE = ( + username: string = 'system', + command: ResponseActionsApiCommandNames, + responseActionId: string +): string => { + return i18n.translate('xpack.securitySolution.responseActions.comment.message', { + values: { username, command, responseActionId }, + defaultMessage: `Action triggered from Elastic Security by user [{username}] for action [{command} (action id: {responseActionId})]`, + }); +}; + const ENTERPRISE_LICENSE_REQUIRED_MSG = i18n.translate( 'xpack.securitySolution.responseActionsList.error.licenseTooLow', { @@ -176,6 +189,8 @@ export interface ResponseActionsClientPendingAction< export abstract class ResponseActionsClientImpl implements ResponseActionsClient { protected readonly log: Logger; + protected readonly cache = new SimpleMemCache(); + protected abstract readonly agentType: ResponseActionAgentType; constructor(protected readonly options: ResponseActionsClientOptions) { @@ -560,6 +575,26 @@ export abstract class ResponseActionsClientImpl implements ResponseActionsClient usageService.notifyUsage(featureKey); } + /** + * Builds a comment for use in response action requests sent to external EDR systems + * @protected + */ + protected buildExternalComment( + actionRequestIndexOptions: ResponseActionsClientWriteActionRequestToEndpointIndexOptions + ): string { + const { actionId = uuidv4(), comment, command } = actionRequestIndexOptions; + + // If the action request index options does not yet have an actionId assigned to it, then do it now. + // Need to ensure we have an action id for cross-reference. + if (!actionRequestIndexOptions.actionId) { + actionRequestIndexOptions.actionId = actionId; + } + + return ( + ELASTIC_RESPONSE_ACTION_MESSAGE(this.options.username, command, actionId) + + (comment ? `: ${comment}` : '') + ); + } protected async ensureValidActionId(actionId: string): Promise { return validateActionId(this.options.esClient, actionId, this.agentType); } @@ -650,19 +685,19 @@ export abstract class ResponseActionsClientImpl implements ResponseActionsClient } public async killProcess( - actionRequest: KillOrSuspendProcessRequestBody, + actionRequest: KillProcessRequestBody, options?: CommonResponseActionMethodOptions ): Promise< - ActionDetails + ActionDetails > { throw new ResponseActionsNotSupportedError('kill-process'); } public async suspendProcess( - actionRequest: KillOrSuspendProcessRequestBody, + actionRequest: SuspendProcessRequestBody, options?: CommonResponseActionMethodOptions ): Promise< - ActionDetails + ActionDetails > { throw new ResponseActionsNotSupportedError('suspend-process'); } diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/simple_mem_cache.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/simple_mem_cache.test.ts new file mode 100644 index 0000000000000..f351e2e40d5be --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/simple_mem_cache.test.ts @@ -0,0 +1,75 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { SimpleMemCache } from './simple_mem_cache'; + +describe('SimpleMemCache class', () => { + let cache: SimpleMemCache; + let key: any; + let value: any; + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(() => { + cache = new SimpleMemCache(); + key = Symbol('foo'); + value = () => {}; + }); + + it('should `set` and `get` a value to cache', () => { + cache.set(key, value); + + expect(cache.get(key)).toEqual(value); + }); + + it('should accept strings as keys', () => { + key = 'mykey'; + cache.set(key, value); + + expect(cache.get(key)).toEqual(value); + }); + + it('should delete a value from cache', () => { + cache.set(key, value); + + expect(cache.get(key)).toEqual(value); + + cache.delete(key); + + expect(cache.get(key)).toEqual(undefined); + }); + + it('should cleanup expired cache entries', () => { + const key2 = 'myKey'; + cache.set(key, value); // Default ttl of 10s + cache.set(key2, value, 60); // ttl 60s + const dateObj = new Date(); + dateObj.setSeconds(dateObj.getSeconds() + 11); + jest.setSystemTime(dateObj); + cache.cleanup(); + + expect(cache.get(key)).toBeUndefined(); + expect(cache.get(key2)).toEqual(value); + }); + + it('should return undefined when a cache entry exists, but it is expired', () => { + cache.set(key, value); + const dateObj = new Date(); + dateObj.setSeconds(dateObj.getSeconds() + 11); + jest.setSystemTime(dateObj); + + expect(cache.get(key)).toBeUndefined(); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/simple_mem_cache.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/simple_mem_cache.ts new file mode 100644 index 0000000000000..fc355bf6c3797 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/simple_mem_cache.ts @@ -0,0 +1,89 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export interface SimpleMemCacheInterface { + /** Store a piece of data in cache */ + set( + key: any, + value: any, + /** Time-to-live for this entry only (in seconds) */ + ttl?: number + ): void; + /** Retrieve a piece of data from cache */ + get(key: any): TValue | undefined; + /** Delete a piece of data from cache */ + delete(key: any): void; + /** Clean up the cache by removing all expired entries */ + cleanup(): void; +} + +export interface SimpleMemCacheOptions { + /** + * Default Time-to-live (in seconds) for each piece of data that is cached. + * Defaults to `10` seconds. Can also be set on each entry explicitly + */ + ttl?: number; +} + +interface CachedEntry { + value: any; + expires: number; +} + +/** + * A simple memory caching mechanism. Entries are given a time-to-live (`ttl`) and deleted only when + * attempted to be retrieved and entry is expired. + * + * > **NOTE**: There is no automated "cache cleanup" to remove expired entries over time due to the + * > fact that it could lead to memory leaks. A `cleanup()` method, however, is provided + * > which can be called periodically to clean up the cache + */ +export class SimpleMemCache implements SimpleMemCacheInterface { + private readonly ttl: number; + private readonly cache = new Map(); + + constructor({ ttl = 10 }: SimpleMemCacheOptions = {}) { + this.ttl = ttl; + } + + private isExpired(entry: CachedEntry): boolean { + return entry.expires < Date.now(); + } + + public set(key: any, value: any, ttl = this.ttl): void { + const expiresDt = new Date(); + expiresDt.setSeconds(expiresDt.getSeconds() + ttl); + this.cache.set(key, { value, expires: expiresDt.getTime() }); + } + + public get(key: any): TValue | undefined { + const cachedValue = this.cache.get(key); + + if (cachedValue) { + if (this.isExpired(cachedValue)) { + this.delete(key); + return; + } + + return cachedValue.value as TValue; + } + } + + public delete(key: any): void { + this.cache.delete(key); + } + + public cleanup(): void { + for (const [cacheKey, cacheData] of this.cache.entries()) { + if (this.isExpired(cacheData)) { + this.delete(cacheKey); + } + } + } +} diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/types.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/types.ts index f95d0b7144a54..4a7b7efd4d4a5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/types.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/types.ts @@ -8,9 +8,8 @@ import type { Readable } from 'stream'; import type { ActionDetails, - KillOrSuspendProcessRequestBody, KillProcessActionOutputContent, - ResponseActionParametersWithPidOrEntityId, + ResponseActionParametersWithProcessData, SuspendProcessActionOutputContent, GetProcessesActionOutputContent, ResponseActionGetFileOutputContent, @@ -24,6 +23,8 @@ import type { UploadedFileInfo, ResponseActionScanOutputContent, ResponseActionScanParameters, + KillProcessRequestBody, + SuspendProcessRequestBody, } from '../../../../../../common/endpoint/types'; import type { IsolationRouteRequestBody, @@ -88,17 +89,17 @@ export interface ResponseActionsClient { ) => Promise; killProcess: ( - actionRequest: OmitUnsupportedAttributes, + actionRequest: OmitUnsupportedAttributes, options?: CommonResponseActionMethodOptions ) => Promise< - ActionDetails + ActionDetails >; suspendProcess: ( - actionRequest: OmitUnsupportedAttributes, + actionRequest: OmitUnsupportedAttributes, options?: CommonResponseActionMethodOptions ) => Promise< - ActionDetails + ActionDetails >; runningProcesses: ( diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/mocks.ts index e8b62fb014306..a721bda2f38ab 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/mocks.ts @@ -172,11 +172,10 @@ const createNoParamsResponseActionOptionsMock = ( const createKillOrSuspendProcessOptionsMock = ( overrides: Partial = {} ): KillOrSuspendProcessRequestBody => { + const parameters = overrides.parameters ?? { pid: 999 }; const options: KillOrSuspendProcessRequestBody = { ...createNoParamsResponseActionOptionsMock(), - parameters: { - pid: 999, - }, + parameters, }; return merge(options, overrides); }; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/mocks.ts index 0971ed7045655..dac3a5c8f1ddb 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/mocks.ts @@ -8,6 +8,7 @@ import type { SentinelOneGetAgentsResponse, SentinelOneGetActivitiesResponse, + SentinelOneGetRemoteScriptsResponse, } from '@kbn/stack-connectors-plugin/common/sentinelone/types'; import { SENTINELONE_CONNECTOR_ID, @@ -130,6 +131,53 @@ const createSentinelOneAgentDetailsMock = ( ); }; +const createSentinelOneGetRemoteScriptsApiResponseMock = + (): SentinelOneGetRemoteScriptsResponse => { + return { + errors: null, + data: [ + { + bucketName: 'us-east-1-prod-remote-scripts', + createdAt: '2022-07-17T14:02:45.309427Z', + createdByUser: 'SentinelOne', + createdByUserId: '-1', + creator: 'SentinelOne', + creatorId: '-1', + fileName: + '-1/-1/75cYNKCLYJ7kEsjtBSrha0dXTSANJeMmBDQpXlRzPQA%3D/multi-operations-script-bash.sh', + fileSize: 13701, + id: '1466645476786791838', + inputExample: '--terminate --processes ping,chrome --force', + inputInstructions: '--terminate --processes [-f|--force]', + inputRequired: true, + isAvailableForArs: false, + isAvailableForLite: false, + mgmtId: -1, + osTypes: ['macos', 'linux'], + outputFilePaths: null, + package: null, + scopeId: '-1', + scopeLevel: 'sentinel', + scopeName: null, + scopePath: 'Global', + scriptDescription: null, + scriptName: 'Terminate Processes (Linux/macOS)', + scriptRuntimeTimeoutSeconds: 3600, + scriptType: 'action', + shortFileName: 'multi-operations-script-bash.sh', + signature: '75cYNKCLYJ7kEsjtBSrha0dXTSANJeMmBDQpXlRzPQA=', + signatureType: 'SHA-256', + supportedDestinations: null, + updatedAt: '2024-06-30T06:37:53.904005Z', + updater: null, + updaterId: null, + version: '1.0.0', + }, + ], + pagination: { nextCursor: null, totalItems: 1 }, + }; + }; + const createSentinelOneGetActivitiesApiResponseMock = (): SentinelOneGetActivitiesResponse => { return { errors: undefined, @@ -225,6 +273,21 @@ const createConnectorActionsClientMock = (): ActionsClientMock => { data: createSentinelOneGetActivitiesApiResponseMock(), }); + case SUB_ACTION.GET_REMOTE_SCRIPTS: + return responseActionsClientMock.createConnectorActionExecuteResponse({ + data: createSentinelOneGetRemoteScriptsApiResponseMock(), + }); + + case SUB_ACTION.EXECUTE_SCRIPT: + return responseActionsClientMock.createConnectorActionExecuteResponse({ + data: { + data: { + affected: 1, + parentTaskId: 'task-789', + }, + }, + }); + default: return responseActionsClientMock.createConnectorActionExecuteResponse(); } @@ -249,4 +312,5 @@ export const sentinelOneMock = { createConnectorActionsClient: createConnectorActionsClientMock, createConstructorOptions: createConstructorOptionsMock, createSentinelOneActivitiesApiResponse: createSentinelOneGetActivitiesApiResponseMock, + createSentinelOneGetRemoteScriptsApiResponse: createSentinelOneGetRemoteScriptsApiResponseMock, }; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.test.ts index df326a269f0a3..6a5304f17b82e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.test.ts @@ -34,6 +34,7 @@ import type { ResponseActionGetFileOutputContent, ResponseActionGetFileParameters, SentinelOneGetFileRequestMeta, + KillOrSuspendProcessRequestBody, } from '../../../../../../common/endpoint/types'; import type { SearchHit, SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import type { ResponseActionGetFileRequestBody } from '../../../../../../common/api/endpoint'; @@ -42,6 +43,7 @@ import { ACTIONS_SEARCH_PAGE_SIZE } from '../../constants'; import type { ElasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks'; import { Readable } from 'stream'; import { RESPONSE_ACTIONS_ZIP_PASSCODE } from '../../../../../../common/endpoint/service/response_actions/constants'; +import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; jest.mock('../../action_details_by_id', () => { const originalMod = jest.requireActual('../../action_details_by_id'); @@ -57,7 +59,7 @@ const getActionDetailsByIdMock = _getActionDetailsById as jest.Mock; describe('SentinelOneActionsClient class', () => { let classConstructorOptions: SentinelOneActionsClientOptionsMock; let s1ActionsClient: ResponseActionsClient; - let connectorActionsMock: NormalizedExternalConnectorClient; + let connectorActionsMock: DeeplyMockedKeys; const createS1IsolationOptions = ( overrides: Omit< @@ -68,11 +70,12 @@ describe('SentinelOneActionsClient class', () => { beforeEach(() => { classConstructorOptions = sentinelOneMock.createConstructorOptions(); - connectorActionsMock = classConstructorOptions.connectorActions; + connectorActionsMock = + classConstructorOptions.connectorActions as DeeplyMockedKeys; s1ActionsClient = new SentinelOneActionsClient(classConstructorOptions); }); - it.each(['killProcess', 'suspendProcess', 'runningProcesses', 'execute', 'upload'] as Array< + it.each(['suspendProcess', 'runningProcesses', 'execute', 'upload', 'scan'] as Array< keyof ResponseActionsClient >)('should throw an un-supported error for %s', async (methodName) => { // @ts-expect-error Purposely passing in empty object for options @@ -794,7 +797,8 @@ describe('SentinelOneActionsClient class', () => { classConstructorOptions.isAutomated = true; classConstructorOptions.connectorActions = responseActionsClientMock.createNormalizedExternalConnectorClient(subActionsClient); - connectorActionsMock = classConstructorOptions.connectorActions; + connectorActionsMock = + classConstructorOptions.connectorActions as DeeplyMockedKeys; // @ts-expect-error readonly prop assignment classConstructorOptions.endpointService.experimentalFeatures.responseActionsSentinelOneGetFileEnabled = true; @@ -1125,4 +1129,169 @@ describe('SentinelOneActionsClient class', () => { }); }); }); + + describe('#killProcess()', () => { + let killProcessActionRequest: KillOrSuspendProcessRequestBody; + + beforeEach(() => { + // @ts-expect-error readonly prop assignment + classConstructorOptions.endpointService.experimentalFeatures.responseActionsSentinelOneKillProcessEnabled = + true; + + killProcessActionRequest = responseActionsClientMock.createKillProcessOptions({ + // @ts-expect-error TS2322 due to type being overloaded to handle kill/suspend process and specific option for S1 + parameters: { process_name: 'foo' }, + }); + }); + + it('should throw an error if feature flag is disabled', async () => { + // @ts-expect-error readonly prop assignment + classConstructorOptions.endpointService.experimentalFeatures.responseActionsSentinelOneKillProcessEnabled = + false; + + await expect(s1ActionsClient.killProcess(killProcessActionRequest)).rejects.toThrow( + `kill-process not supported for sentinel_one agent type. Feature disabled` + ); + }); + + it('should throw an error if `process_name` is not defined (manual mode)', async () => { + // @ts-expect-error + killProcessActionRequest.parameters.process_name = ''; + + await expect(s1ActionsClient.killProcess(killProcessActionRequest)).rejects.toThrow( + '[body.parameters.process_name]: missing parameter or value is empty' + ); + }); + + it('should still create action at error if something goes wrong in automated mode', async () => { + // @ts-expect-error + killProcessActionRequest.parameters.process_name = ''; + classConstructorOptions.isAutomated = true; + classConstructorOptions.connectorActions = + responseActionsClientMock.createNormalizedExternalConnectorClient( + sentinelOneMock.createConnectorActionsClient() + ); + s1ActionsClient = new SentinelOneActionsClient(classConstructorOptions); + await s1ActionsClient.killProcess(killProcessActionRequest); + + expect(classConstructorOptions.esClient.index).toHaveBeenCalledWith( + expect.objectContaining({ + document: expect.objectContaining({ + error: { + message: '[body.parameters.process_name]: missing parameter or value is empty', + }, + }), + }), + { meta: true } + ); + }); + + it('should retrieve script execution information from S1 using host OS', async () => { + await s1ActionsClient.killProcess(killProcessActionRequest); + + expect(connectorActionsMock.execute as jest.Mock).toHaveBeenCalledWith({ + params: { + subAction: SUB_ACTION.GET_REMOTE_SCRIPTS, + subActionParams: { + osTypes: 'linux', + query: 'terminate', + scriptType: 'action', + }, + }, + }); + }); + + it('should throw error if unable to retrieve S1 script information', async () => { + const executeMockImplementation = connectorActionsMock.execute.getMockImplementation()!; + connectorActionsMock.execute.mockImplementation(async (options) => { + if (options.params.subAction === SUB_ACTION.GET_REMOTE_SCRIPTS) { + return responseActionsClientMock.createConnectorActionExecuteResponse({ + data: { data: [] }, + }); + } + return executeMockImplementation.call(connectorActionsMock, options); + }); + + await expect(s1ActionsClient.killProcess(killProcessActionRequest)).rejects.toThrow( + 'Unable to find a script from SentinelOne to handle [kill-process] response action for host running [linux])' + ); + }); + + it('should send execute script request to S1 for kill-process', async () => { + await s1ActionsClient.killProcess(killProcessActionRequest); + + expect(connectorActionsMock.execute).toHaveBeenCalledWith({ + params: { + subAction: 'executeScript', + subActionParams: { + filter: { uuids: '1-2-3' }, + script: { + inputParams: '--terminate --processes "foo" --force', + outputDestination: 'SentinelCloud', + requiresApproval: false, + scriptId: '1466645476786791838', + taskDescription: expect.stringContaining( + 'Action triggered from Elastic Security by user [foo] for action [kill-process' + ), + }, + }, + }, + }); + }); + + it('should return action details on success', async () => { + await s1ActionsClient.killProcess(killProcessActionRequest); + + expect(getActionDetailsByIdMock).toHaveBeenCalled(); + }); + + it('should create action request doc with expected meta info', async () => { + await s1ActionsClient.killProcess(killProcessActionRequest); + + expect(classConstructorOptions.esClient.index).toHaveBeenCalledWith( + { + document: { + '@timestamp': expect.any(String), + EndpointActions: { + action_id: expect.any(String), + data: { + command: 'kill-process', + comment: 'test comment', + parameters: { process_name: 'foo' }, + hosts: { + '1-2-3': { + name: 'sentinelone-1460', + }, + }, + }, + expiration: expect.any(String), + input_type: 'sentinel_one', + type: 'INPUT_ACTION', + }, + agent: { id: ['1-2-3'] }, + user: { id: 'foo' }, + meta: { + agentId: '1845174760470303882', + agentUUID: '1-2-3', + hostName: 'sentinelone-1460', + parentTaskId: 'task-789', + }, + }, + index: ENDPOINT_ACTIONS_INDEX, + refresh: 'wait_for', + }, + { meta: true } + ); + }); + + it('should update cases', async () => { + killProcessActionRequest = { + ...killProcessActionRequest, + case_ids: ['case-1'], + }; + await s1ActionsClient.killProcess(killProcessActionRequest); + + expect(classConstructorOptions.casesClient?.attachments.bulkCreate).toHaveBeenCalled(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.ts index f1f36e993e8a0..54d2147cbe2e2 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.ts @@ -12,11 +12,13 @@ import { import { groupBy } from 'lodash'; import type { ActionTypeExecutorResult } from '@kbn/actions-plugin/common'; import type { + SentinelOneDownloadAgentFileParams, SentinelOneGetActivitiesParams, SentinelOneGetActivitiesResponse, - SentinelOneGetAgentsParams, SentinelOneGetAgentsResponse, - SentinelOneDownloadAgentFileParams, + SentinelOneGetRemoteScriptsParams, + SentinelOneGetRemoteScriptsResponse, + SentinelOneExecuteScriptResponse, } from '@kbn/stack-connectors-plugin/common/sentinelone/types'; import type { QueryDslQueryContainer, @@ -24,11 +26,10 @@ import type { SearchRequest, } from '@elastic/elasticsearch/lib/api/types'; import type { Readable } from 'stream'; +import type { Mutable } from 'utility-types'; +import type { SentinelOneKillProcessScriptArgs, SentinelOneScriptArgs } from './types'; import { ACTIONS_SEARCH_PAGE_SIZE } from '../../constants'; -import type { - NormalizedExternalConnectorClient, - NormalizedExternalConnectorClientExecuteOptions, -} from '../lib/normalized_external_connector_client'; +import type { NormalizedExternalConnectorClient } from '../lib/normalized_external_connector_client'; import { SENTINEL_ONE_ACTIVITY_INDEX_PATTERN } from '../../../../../../common'; import { catchAndWrapError } from '../../../../utils'; import type { @@ -40,16 +41,20 @@ import type { ResponseActionAgentType, ResponseActionsApiCommandNames, } from '../../../../../../common/endpoint/service/response_actions/constants'; +import { RESPONSE_ACTIONS_ZIP_PASSCODE } from '../../../../../../common/endpoint/service/response_actions/constants'; import { stringify } from '../../../../utils/stringify'; import { ResponseActionAgentResponseEsDocNotFound, ResponseActionsClientError } from '../errors'; import type { ActionDetails, EndpointActionDataParameterTypes, EndpointActionResponseDataOutput, + KillProcessActionOutputContent, + KillProcessRequestBody, LogsEndpointAction, LogsEndpointActionResponse, ResponseActionGetFileOutputContent, ResponseActionGetFileParameters, + ResponseActionParametersWithProcessData, SentinelOneActionRequestCommonMeta, SentinelOneActivityDataForType80, SentinelOneActivityEsDoc, @@ -57,7 +62,9 @@ import type { SentinelOneGetFileResponseMeta, SentinelOneIsolationRequestMeta, SentinelOneIsolationResponseMeta, + SentinelOneKillProcessRequestMeta, UploadedFileInfo, + ResponseActionParametersWithProcessName, } from '../../../../../../common/endpoint/types'; import type { IsolationRouteRequestBody, @@ -65,17 +72,29 @@ import type { } from '../../../../../../common/api/endpoint'; import type { ResponseActionsClientOptions, + ResponseActionsClientPendingAction, ResponseActionsClientValidateRequestResponse, ResponseActionsClientWriteActionRequestToEndpointIndexOptions, - ResponseActionsClientPendingAction, } from '../lib/base_response_actions_client'; import { ResponseActionsClientImpl } from '../lib/base_response_actions_client'; -import { RESPONSE_ACTIONS_ZIP_PASSCODE } from '../../../../../../common/endpoint/service/response_actions/constants'; + +const NOOP_THROW = () => { + throw new ResponseActionsClientError('not implemented!'); +}; export type SentinelOneActionsClientOptions = ResponseActionsClientOptions & { connectorActions: NormalizedExternalConnectorClient; }; +interface FetchScriptInfoResponse< + TScriptOptions extends SentinelOneScriptArgs = SentinelOneScriptArgs +> { + scriptId: string; + scriptInfo: SentinelOneGetRemoteScriptsResponse['data'][number]; + /** A helper method that will build the arguments for the given script */ + buildScriptArgs: (options: TScriptOptions) => string; +} + export class SentinelOneActionsClient extends ResponseActionsClientImpl { protected readonly agentType: ResponseActionAgentType = 'sentinel_one'; private readonly connectorActionsClient: NormalizedExternalConnectorClient; @@ -208,28 +227,21 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl { private async getAgentDetails( agentUUID: string ): Promise { - const executeOptions: NormalizedExternalConnectorClientExecuteOptions< - SentinelOneGetAgentsParams, - SUB_ACTION - > = { - params: { - subAction: SUB_ACTION.GET_AGENTS, - subActionParams: { - uuid: agentUUID, - }, - }, - }; + const cachedEntry = this.cache.get(agentUUID); + + if (cachedEntry) { + this.log.debug( + `Found cached agent details for UUID [${agentUUID}]:\n${stringify(cachedEntry)}` + ); + return cachedEntry; + } let s1ApiResponse: SentinelOneGetAgentsResponse | undefined; try { - const response = (await this.connectorActionsClient.execute( - executeOptions - )) as ActionTypeExecutorResult; - - this.log.debug( - () => `Response for SentinelOne agent id [${agentUUID}] returned:\n${stringify(response)}` - ); + const response = await this.sendAction(SUB_ACTION.GET_AGENTS, { + uuid: agentUUID, + }); s1ApiResponse = response.data; } catch (err) { @@ -244,6 +256,8 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl { throw new ResponseActionsClientError(`SentinelOne agent id [${agentUUID}] not found`, 404); } + this.cache.set(agentUUID, s1ApiResponse.data[0]); + return s1ApiResponse.data[0]; } @@ -261,6 +275,23 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl { }; } + // validate that we have a `process_name`. We need this here because the schema for this command + // specifically because `KillProcessRequestBody` allows 3 types of parameters. + if (payload.command === 'kill-process') { + if ( + !payload.parameters || + !('process_name' in payload.parameters) || + !payload.parameters.process_name + ) { + return { + isValid: false, + error: new ResponseActionsClientError( + '[body.parameters.process_name]: missing parameter or value is empty' + ), + }; + } + } + return super.validateRequest(payload); } @@ -439,13 +470,6 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl { SentinelOneGetActivitiesResponse<{ commandBatchUuid: string }> >(SUB_ACTION.GET_ACTIVITIES, activitySearchCriteria); - this.log.debug( - () => - `Search of activity log with:\n${stringify( - activitySearchCriteria - )}\n returned:\n${stringify(activityLogSearchResponse.data)}` - ); - if (activityLogSearchResponse.data?.data.length) { const activityLogItem = activityLogSearchResponse.data?.data[0]; @@ -559,6 +583,86 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl { }; } + async killProcess( + actionRequest: KillProcessRequestBody, + options?: CommonResponseActionMethodOptions + ): Promise< + ActionDetails + > { + if ( + !this.options.endpointService.experimentalFeatures + .responseActionsSentinelOneKillProcessEnabled + ) { + throw new ResponseActionsClientError( + `kill-process not supported for ${this.agentType} agent type. Feature disabled`, + 400 + ); + } + + const reqIndexOptions: ResponseActionsClientWriteActionRequestToEndpointIndexOptions< + ResponseActionParametersWithProcessName, + KillProcessActionOutputContent, + Partial + > = { + ...actionRequest, + ...this.getMethodOptions(options), + command: 'kill-process', + meta: { parentTaskId: '' }, + }; + + if (!reqIndexOptions.error) { + let error = (await this.validateRequest(reqIndexOptions)).error; + + if (!error) { + const s1AgentDetails = await this.getAgentDetails(reqIndexOptions.endpoint_ids[0]); + const terminateScriptInfo = await this.fetchScriptInfo( + 'kill-process', + s1AgentDetails.osType + ); + + try { + const s1Response = await this.sendAction( + SUB_ACTION.EXECUTE_SCRIPT, + { + filter: { + uuids: actionRequest.endpoint_ids[0], + }, + script: { + scriptId: terminateScriptInfo.scriptId, + taskDescription: this.buildExternalComment(reqIndexOptions), + requiresApproval: false, + outputDestination: 'SentinelCloud', + inputParams: terminateScriptInfo.buildScriptArgs({ + // @ts-expect-error TS2339: Property 'process_name' does not exist (`.validateRequest()` has already validated that `process_name` exists) + processName: reqIndexOptions.parameters.process_name, + }), + }, + } + ); + + reqIndexOptions.meta = { + parentTaskId: s1Response.data?.data?.parentTaskId ?? '', + }; + } catch (err) { + error = err; + } + } + + reqIndexOptions.error = error?.message; + + if (!this.options.isAutomated && error) { + throw error; + } + } + + const { actionDetails } = await this.handleResponseActionCreation< + ResponseActionParametersWithProcessData, + KillProcessActionOutputContent + >(reqIndexOptions); + + return actionDetails; + } + async processPendingActions({ abortSignal, addToQueue, @@ -616,6 +720,93 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl { } } + /** + * retrieve script info. for scripts that are used to handle Elastic response actions + * @param scriptType + * @param osType + * @private + */ + private async fetchScriptInfo< + TScriptOptions extends SentinelOneScriptArgs = SentinelOneScriptArgs + >( + scriptType: Extract, + osType: string | 'linux' | 'macos' | 'windows' + ): Promise> { + const searchQueryParams: Mutable> = { + query: '', + osTypes: osType, + }; + let buildScriptArgs = NOOP_THROW as FetchScriptInfoResponse['buildScriptArgs']; + let isDesiredScript: ( + scriptInfo: SentinelOneGetRemoteScriptsResponse['data'][number] + ) => boolean = () => false; + + // Set the query value for filtering the list of scripts in S1 + switch (scriptType) { + case 'kill-process': + searchQueryParams.query = 'terminate'; + searchQueryParams.scriptType = 'action'; + + isDesiredScript = (scriptInfo) => { + return ( + scriptInfo.creator === 'SentinelOne' && + scriptInfo.creatorId === '-1' && + // Using single `-` (instead of double `--`) in match below to ensure both windows and macos/linux are matched + /-terminate/i.test(scriptInfo.inputInstructions ?? '') && + /-processes/i.test(scriptInfo.inputInstructions ?? '') + ); + }; + break; + + default: + throw new ResponseActionsClientError( + `Unable to fetch SentinelOne script for OS [${osType}]. Unknown script type [${scriptType}]` + ); + } + + const { data: scriptSearchResults } = + await this.sendAction( + SUB_ACTION.GET_REMOTE_SCRIPTS, + searchQueryParams + ); + + const s1Script: SentinelOneGetRemoteScriptsResponse['data'][number] | undefined = ( + scriptSearchResults?.data ?? [] + ).find(isDesiredScript); + + if (!s1Script) { + throw new ResponseActionsClientError( + `Unable to find a script from SentinelOne to handle [${scriptType}] response action for host running [${osType}])` + ); + } + + switch (scriptType) { + case 'kill-process': + buildScriptArgs = (args: SentinelOneKillProcessScriptArgs) => { + if (!args.processName) { + throw new ResponseActionsClientError( + `'processName' missing while building script args for [${s1Script.scriptName} (id: ${s1Script.id})] script` + ); + } + + if (osType === 'windows') { + return `-Terminate -Processes "${args.processName}" -Force`; + } + + // Linux + Macos + return `--terminate --processes "${args.processName}" --force`; + }; + + break; + } + + return { + scriptId: s1Script.id, + scriptInfo: s1Script, + buildScriptArgs, + } as FetchScriptInfoResponse; + } + private async fetchGetFileResponseEsDocForAgentId( actionId: string, agentId: string diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/types.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/types.ts index fbb28df5e4449..a51a9ec981765 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/types.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/types.ts @@ -5,22 +5,11 @@ * 2.0. */ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import type { ActionsClient } from '@kbn/actions-plugin/server'; -import type { SUB_ACTION } from '@kbn/stack-connectors-plugin/common/sentinelone/constants'; - -type ConnectorActionsExecuteOptions = Parameters[0]; - -interface SentinelOneConnectorExecuteParams< - P extends Record = Record -> { - subAction: SUB_ACTION; - subActionParams: P; +export interface SentinelOneKillProcessScriptArgs { + processName: string; } -export type SentinelOneConnectorExecuteOptions< - P extends Record = Record -> = Omit & { - params: SentinelOneConnectorExecuteParams

& Record; -}; +/** + * All the possible set of arguments running SentinelOne scripts that we support for response actions + */ +export type SentinelOneScriptArgs = SentinelOneKillProcessScriptArgs; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/utils/utils.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/utils/utils.ts index f5087b18e03d6..9c9f8e5b62cac 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/utils/utils.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/utils/utils.ts @@ -9,7 +9,6 @@ import type { ElasticsearchClient } from '@kbn/core/server'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { EcsError } from '@elastic/ecs'; import moment from 'moment/moment'; -import { i18n } from '@kbn/i18n'; import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import type { FetchActionResponsesResult } from '../..'; import type { @@ -611,12 +610,3 @@ export const createActionDetailsRecord = { return moment().add(2, 'weeks').toISOString(); }; - -export const ELASTIC_RESPONSE_ACTION_MESSAGE = ( - username: string = 'system', - responseActionId: string = 'response-action-id' // I believe actionId exists always and there is a mismatch in types, but this default is just a safety net -): string => - i18n.translate('xpack.securitySolution.responseActions.comment.message', { - values: { username, responseActionId }, - defaultMessage: `Action triggered from Elastic Security by user {username} for action {responseActionId}`, - }); diff --git a/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_dashboards_by_tags.test.ts b/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_dashboards_by_tags.test.ts index 4c9fe188fdffb..dfd38ab8e6eea 100644 --- a/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_dashboards_by_tags.test.ts +++ b/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_dashboards_by_tags.test.ts @@ -5,12 +5,10 @@ * 2.0. */ import type { Logger, SavedObjectsFindResponse } from '@kbn/core/server'; -import type { SecurityPluginSetup } from '@kbn/security-plugin/server'; import { INTERNAL_DASHBOARDS_URL } from '../../../../common/constants'; import { serverMock, requestContextMock, - mockGetCurrentUser, requestMock, } from '../../detection_engine/routes/__mocks__'; import { mockGetDashboardsResult } from '../__mocks__'; @@ -18,7 +16,6 @@ import { getDashboardsByTagsRoute } from './get_dashboards_by_tags'; describe('getDashboardsByTagsRoute', () => { let server: ReturnType; - let securitySetup: SecurityPluginSetup; const { context } = requestContextMock.createTools(); const logger = { error: jest.fn() } as unknown as Logger; @@ -36,14 +33,7 @@ describe('getDashboardsByTagsRoute', () => { jest.clearAllMocks(); server = serverMock.create(); - securitySetup = { - authc: { - getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), - }, - authz: {}, - } as unknown as SecurityPluginSetup; - - getDashboardsByTagsRoute(server.router, logger, securitySetup); + getDashboardsByTagsRoute(server.router, logger); }); it('should return dashboards with Security Solution tags', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_dashboards_by_tags.ts b/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_dashboards_by_tags.ts index ef4695aea6ea5..e7a3ebf9a7f10 100644 --- a/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_dashboards_by_tags.ts +++ b/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_dashboards_by_tags.ts @@ -10,18 +10,13 @@ import { i18n } from '@kbn/i18n'; import type { DashboardAttributes } from '@kbn/dashboard-plugin/common'; import { transformError } from '@kbn/securitysolution-es-utils'; import { INTERNAL_DASHBOARDS_URL } from '../../../../common/constants'; -import type { SetupPlugins } from '../../../plugin'; import type { SecuritySolutionPluginRouter } from '../../../types'; import { buildSiemResponse } from '../../detection_engine/routes/utils'; import { buildFrameworkRequest } from '../../timeline/utils/common'; import { getDashboardsRequest } from '../../../../common/api/tags'; import { buildRouteValidationWithExcess } from '../../../utils/build_validation/route_validation'; -export const getDashboardsByTagsRoute = ( - router: SecuritySolutionPluginRouter, - logger: Logger, - security: SetupPlugins['security'] -) => { +export const getDashboardsByTagsRoute = (router: SecuritySolutionPluginRouter, logger: Logger) => { router.versioned .post({ path: INTERNAL_DASHBOARDS_URL, @@ -36,7 +31,7 @@ export const getDashboardsByTagsRoute = ( validate: { request: { body: buildRouteValidationWithExcess(getDashboardsRequest) } }, }, async (context, request, response) => { - const frameworkRequest = await buildFrameworkRequest(context, security, request); + const frameworkRequest = await buildFrameworkRequest(context, request); const savedObjectsClient = (await frameworkRequest.context.core).savedObjects.client; const { tagIds } = request.body; diff --git a/x-pack/plugins/security_solution/server/lib/dashboards/routes/index.ts b/x-pack/plugins/security_solution/server/lib/dashboards/routes/index.ts index 17b6cec643ec4..dfbb6fbb8771d 100644 --- a/x-pack/plugins/security_solution/server/lib/dashboards/routes/index.ts +++ b/x-pack/plugins/security_solution/server/lib/dashboards/routes/index.ts @@ -5,14 +5,9 @@ * 2.0. */ import type { Logger } from '@kbn/core/server'; -import type { SetupPlugins } from '../../../plugin_contract'; import type { SecuritySolutionPluginRouter } from '../../../types'; import { getDashboardsByTagsRoute } from './get_dashboards_by_tags'; -export const registerDashboardsRoutes = ( - router: SecuritySolutionPluginRouter, - logger: Logger, - security: SetupPlugins['security'] -) => { - getDashboardsByTagsRoute(router, logger, security); +export const registerDashboardsRoutes = (router: SecuritySolutionPluginRouter, logger: Logger) => { + getDashboardsByTagsRoute(router, logger); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route.test.ts index 9e9bd77e81a1c..8e029f939d111 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { securityServiceMock } from '@kbn/core/server/mocks'; import { getPrebuiltRulesAndTimelinesStatusRoute } from './get_prebuilt_rules_and_timelines_status_route'; import { @@ -13,7 +14,6 @@ import { getPrepackagedRulesStatusRequest, } from '../../../routes/__mocks__/request_responses'; import { requestContextMock, serverMock } from '../../../routes/__mocks__'; -import type { SecurityPluginSetup } from '@kbn/security-plugin/server'; import { checkTimelinesStatus } from '../../../../timeline/utils/check_timelines_status'; import { mockCheckTimelinesStatusBeforeInstallResult, @@ -56,6 +56,7 @@ jest.mock('../../../../timeline/utils/check_timelines_status', () => { }); describe('get_prepackaged_rule_status_route', () => { + const securityCore = securityServiceMock.createStart(); const mockGetCurrentUser = { user: { username: 'mockUser', @@ -64,19 +65,13 @@ describe('get_prepackaged_rule_status_route', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); - let securitySetup: SecurityPluginSetup; beforeEach(() => { jest.clearAllMocks(); server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); - securitySetup = { - authc: { - getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), - }, - authz: {}, - } as unknown as SecurityPluginSetup; + jest.spyOn(securityCore.authc, 'getCurrentUser').mockReturnValue(mockGetCurrentUser); clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); @@ -86,7 +81,7 @@ describe('get_prepackaged_rule_status_route', () => { prepackagedTimelines: [], }); - getPrebuiltRulesAndTimelinesStatusRoute(server.router, securitySetup); + getPrebuiltRulesAndTimelinesStatusRoute(server.router); }); describe('status codes', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route.ts index 4848cb5e52c4e..84cb18032f50e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route.ts @@ -9,7 +9,6 @@ import { transformError } from '@kbn/securitysolution-es-utils'; import { validate } from '@kbn/securitysolution-io-ts-utils'; import { checkTimelineStatusRt } from '../../../../../../common/api/timeline'; import { buildSiemResponse } from '../../../routes/utils'; -import type { SetupPlugins } from '../../../../../plugin'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; import { @@ -27,10 +26,7 @@ import { rulesToMap } from '../../logic/utils'; import { buildFrameworkRequest } from '../../../../timeline/utils/common'; import { checkTimelinesStatus } from '../../../../timeline/utils/check_timelines_status'; -export const getPrebuiltRulesAndTimelinesStatusRoute = ( - router: SecuritySolutionPluginRouter, - security: SetupPlugins['security'] -) => { +export const getPrebuiltRulesAndTimelinesStatusRoute = (router: SecuritySolutionPluginRouter) => { router.versioned .get({ access: 'public', @@ -71,7 +67,7 @@ export const getPrebuiltRulesAndTimelinesStatusRoute = ( const rulesToInstall = getRulesToInstall(latestPrebuiltRules, installedPrebuiltRules); const rulesToUpdate = getRulesToUpdate(latestPrebuiltRules, installedPrebuiltRules); - const frameworkRequest = await buildFrameworkRequest(context, security, request); + const frameworkRequest = await buildFrameworkRequest(context, request); const prebuiltTimelineStatus = await checkTimelinesStatus(frameworkRequest); const [validatedPrebuiltTimelineStatus] = validate( prebuiltTimelineStatus, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/register_routes.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/register_routes.ts index 1ac2ef2a3954d..71740086e3fa5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/register_routes.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/register_routes.ts @@ -5,7 +5,6 @@ * 2.0. */ -import type { SetupPlugins } from '../../../../plugin_contract'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { getPrebuiltRulesAndTimelinesStatusRoute } from './get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route'; @@ -16,12 +15,9 @@ import { reviewRuleUpgradeRoute } from './review_rule_upgrade/review_rule_upgrad import { performRuleInstallationRoute } from './perform_rule_installation/perform_rule_installation_route'; import { performRuleUpgradeRoute } from './perform_rule_upgrade/perform_rule_upgrade_route'; -export const registerPrebuiltRulesRoutes = ( - router: SecuritySolutionPluginRouter, - security: SetupPlugins['security'] -) => { +export const registerPrebuiltRulesRoutes = (router: SecuritySolutionPluginRouter) => { // Legacy endpoints that we're going to deprecate - getPrebuiltRulesAndTimelinesStatusRoute(router, security); + getPrebuiltRulesAndTimelinesStatusRoute(router); installPrebuiltRulesAndTimelinesRoute(router); // New endpoints for the rule upgrade and installation workflows diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_route.ts index 0b30c9bab4782..ec3ca342bf8c9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_route.ts @@ -18,7 +18,7 @@ import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebui import { fetchRuleVersionsTriad } from '../../logic/rule_versions/fetch_rule_versions_triad'; import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset'; import { getVersionBuckets } from '../../model/rule_versions/get_version_buckets'; -import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management/normalization/rule_converters'; +import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response'; import { PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS } from '../../constants'; export const reviewRuleInstallationRoute = (router: SecuritySolutionPluginRouter) => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts index de7db929790de..f38fcc7953641 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts @@ -27,7 +27,7 @@ import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; import { fetchRuleVersionsTriad } from '../../logic/rule_versions/fetch_rule_versions_triad'; import { getVersionBuckets } from '../../model/rule_versions/get_version_buckets'; -import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management/normalization/rule_converters'; +import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response'; import { PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS } from '../../constants'; export const reviewRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_assets/__mocks__/prebuilt_rule_assets_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_assets/__mocks__/prebuilt_rule_assets_client.ts new file mode 100644 index 0000000000000..0776fefb98656 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_assets/__mocks__/prebuilt_rule_assets_client.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const createPrebuiltRuleAssetsClient = () => { + return { + fetchLatestAssets: jest.fn(), + fetchLatestVersions: jest.fn(), + fetchAssetsByVersion: jest.fn(), + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts index 7ad4df3cddabd..1138a48cc39d4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts @@ -13,7 +13,7 @@ import type { import { withSecuritySpan } from '../../../../../utils/with_security_span'; import { findRules } from '../../../rule_management/logic/search/find_rules'; import { getExistingPrepackagedRules } from '../../../rule_management/logic/search/get_existing_prepackaged_rules'; -import { internalRuleToAPIResponse } from '../../../rule_management/normalization/rule_converters'; +import { internalRuleToAPIResponse } from '../../../rule_management/logic/detection_rules_client/converters/internal_rule_to_api_response'; export interface IPrebuiltRuleObjectsClient { fetchAllInstalledRules(): Promise; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/create_signals_migration_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/create_signals_migration_route.test.ts index 5d81134325c50..094f3e560ec3e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/create_signals_migration_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/create_signals_migration_route.test.ts @@ -6,7 +6,6 @@ */ import { requestMock, serverMock } from '../__mocks__'; -import type { SetupPlugins } from '../../../../plugin'; import { DETECTION_ENGINE_SIGNALS_MIGRATION_URL } from '../../../../../common/constants'; import { getCreateSignalsMigrationSchemaMock } from '../../../../../common/api/detection_engine/signals_migration/create_signals_migration/create_signals_migration_route.mock'; import { getIndexVersionsByIndex } from '../../migrations/get_index_versions_by_index'; @@ -43,13 +42,7 @@ describe('creating signals migrations route', () => { (getIndexVersionsByIndex as jest.Mock).mockResolvedValue({ 'my-signals-index': -1 }); (getSignalVersionsByIndex as jest.Mock).mockResolvedValue({ 'my-signals-index': [] }); - const securityMock = { - authc: { - getCurrentUser: jest.fn().mockReturnValue({ user: { username: 'my-username' } }), - }, - } as unknown as SetupPlugins['security']; - - createSignalsMigrationRoute(server.router, securityMock); + createSignalsMigrationRoute(server.router); }); it('passes options to the createMigration', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/create_signals_migration_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/create_signals_migration_route.ts index b6e7ae696b755..8d8d80a700478 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/create_signals_migration_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/create_signals_migration_route.ts @@ -9,7 +9,6 @@ import { transformError, BadRequestError, getIndexAliases } from '@kbn/securitys import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import { CreateAlertsMigrationRequestBody } from '../../../../../common/api/detection_engine/signals_migration'; import type { SecuritySolutionPluginRouter } from '../../../../types'; -import type { SetupPlugins } from '../../../../plugin'; import { DETECTION_ENGINE_SIGNALS_MIGRATION_URL } from '../../../../../common/constants'; import { buildSiemResponse } from '../utils'; @@ -20,10 +19,7 @@ import { isOutdated, signalsAreOutdated } from '../../migrations/helpers'; import { getIndexVersionsByIndex } from '../../migrations/get_index_versions_by_index'; import { getSignalVersionsByIndex } from '../../migrations/get_signal_versions_by_index'; -export const createSignalsMigrationRoute = ( - router: SecuritySolutionPluginRouter, - security: SetupPlugins['security'] -) => { +export const createSignalsMigrationRoute = (router: SecuritySolutionPluginRouter) => { router.versioned .post({ path: DETECTION_ENGINE_SIGNALS_MIGRATION_URL, @@ -53,7 +49,7 @@ export const createSignalsMigrationRoute = ( if (!appClient) { return siemResponse.error({ statusCode: 404 }); } - const user = await security?.authc.getCurrentUser(request); + const user = core.security.authc.getCurrentUser(); const migrationService = signalsMigrationService({ esClient, soClient, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/delete_signals_migration_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/delete_signals_migration_route.ts index f4452c73eaf78..c4838280ac6a4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/delete_signals_migration_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/delete_signals_migration_route.ts @@ -9,17 +9,13 @@ import { transformError } from '@kbn/securitysolution-es-utils'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import { AlertsMigrationCleanupRequestBody } from '../../../../../common/api/detection_engine/signals_migration'; import type { SecuritySolutionPluginRouter } from '../../../../types'; -import type { SetupPlugins } from '../../../../plugin'; import { DETECTION_ENGINE_SIGNALS_MIGRATION_URL } from '../../../../../common/constants'; import { buildSiemResponse } from '../utils'; import { signalsMigrationService } from '../../migrations/migration_service'; import { getMigrationSavedObjectsById } from '../../migrations/get_migration_saved_objects_by_id'; -export const deleteSignalsMigrationRoute = ( - router: SecuritySolutionPluginRouter, - security: SetupPlugins['security'] -) => { +export const deleteSignalsMigrationRoute = (router: SecuritySolutionPluginRouter) => { router.versioned .delete({ path: DETECTION_ENGINE_SIGNALS_MIGRATION_URL, @@ -50,7 +46,7 @@ export const deleteSignalsMigrationRoute = ( return siemResponse.error({ statusCode: 404 }); } - const user = await security?.authc.getCurrentUser(request); + const user = core.security.authc.getCurrentUser(); const migrationService = signalsMigrationService({ esClient, soClient, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/finalize_signals_migration_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/finalize_signals_migration_route.test.ts index 341cff93ccc63..8183c0bbac7bd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/finalize_signals_migration_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/finalize_signals_migration_route.test.ts @@ -6,7 +6,6 @@ */ import { serverMock } from '../__mocks__'; -import type { SetupPlugins } from '../../../../plugin'; import { getFinalizeSignalsMigrationRequest } from '../__mocks__/request_responses'; import { getMigrationSavedObjectsById } from '../../migrations/get_migration_saved_objects_by_id'; import { getSignalsMigrationSavedObjectMock } from '../../migrations/saved_objects_schema.mock'; @@ -22,14 +21,9 @@ describe('finalizing signals migrations', () => { beforeEach(() => { server = serverMock.create(); - const securityMock = { - authc: { - getCurrentUser: jest.fn().mockReturnValue({ user: { username: 'my-username' } }), - }, - } as unknown as SetupPlugins['security']; const ruleDataPluginServiceMock = ruleDataServiceMock.create() as unknown as RuleDataPluginService; - finalizeSignalsMigrationRoute(server.router, ruleDataPluginServiceMock, securityMock); + finalizeSignalsMigrationRoute(server.router, ruleDataPluginServiceMock); }); it('returns an empty array error if no migrations exists', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/finalize_signals_migration_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/finalize_signals_migration_route.ts index 468baae7fdcad..9e09ffe0cf895 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/finalize_signals_migration_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/finalize_signals_migration_route.ts @@ -10,7 +10,6 @@ import type { RuleDataPluginService } from '@kbn/rule-registry-plugin/server'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import { FinalizeAlertsMigrationRequestBody } from '../../../../../common/api/detection_engine/signals_migration'; import type { SecuritySolutionPluginRouter } from '../../../../types'; -import type { SetupPlugins } from '../../../../plugin'; import { DETECTION_ENGINE_SIGNALS_FINALIZE_MIGRATION_URL } from '../../../../../common/constants'; import { isMigrationFailed, isMigrationPending } from '../../migrations/helpers'; import { signalsMigrationService } from '../../migrations/migration_service'; @@ -20,8 +19,7 @@ import { getMigrationSavedObjectsById } from '../../migrations/get_migration_sav export const finalizeSignalsMigrationRoute = ( router: SecuritySolutionPluginRouter, - ruleDataService: RuleDataPluginService, - security: SetupPlugins['security'] + ruleDataService: RuleDataPluginService ) => { router.versioned .post({ @@ -53,7 +51,7 @@ export const finalizeSignalsMigrationRoute = ( if (!appClient) { return siemResponse.error({ statusCode: 404 }); } - const user = await security?.authc.getCurrentUser(request); + const user = core.security.authc.getCurrentUser(); const migrationService = signalsMigrationService({ esClient, soClient, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals.test.ts index 1b2bdb6ca1ef4..a1569e8bcab07 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { coreMock, loggingSystemMock } from '@kbn/core/server/mocks'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '../../../../../common/constants'; import { @@ -24,18 +24,8 @@ describe('set signal status', () => { let server: ReturnType; let { context } = requestContextMock.createTools(); let logger: ReturnType; - let mockCore: ReturnType; beforeEach(() => { - mockCore = coreMock.createSetup({ - pluginStartDeps: { - security: { - authc: { - getCurrentUser: jest.fn().mockReturnValue({ user: { username: 'my-username' } }), - }, - }, - }, - }); server = serverMock.create(); logger = loggingSystemMock.createLogger(); ({ context } = requestContextMock.createTools()); @@ -44,7 +34,7 @@ describe('set signal status', () => { getSuccessfulSignalUpdateResponse() ); const telemetrySenderMock = createMockTelemetryEventsSender(); - setSignalsStatusRoute(server.router, logger, telemetrySenderMock, mockCore.getStartServices); + setSignalsStatusRoute(server.router, logger, telemetrySenderMock); }); describe('status on signal', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts index a92f3a54ebac7..dde24af7007c4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts @@ -12,8 +12,7 @@ import { ALERT_WORKFLOW_STATUS_UPDATED_AT, ALERT_WORKFLOW_USER, } from '@kbn/rule-data-utils'; -import type { ElasticsearchClient, Logger, StartServicesAccessor } from '@kbn/core/server'; -import type { AuthenticatedUser } from '@kbn/security-plugin/common'; +import type { AuthenticatedUser, ElasticsearchClient, Logger } from '@kbn/core/server'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import { SetAlertsStatusRequestBody } from '../../../../../common/api/detection_engine/signals'; import type { SecuritySolutionPluginRouter } from '../../../../types'; @@ -24,7 +23,6 @@ import { import { buildSiemResponse } from '../utils'; import type { ITelemetryEventsSender } from '../../../telemetry/sender'; import { INSIGHTS_CHANNEL } from '../../../telemetry/constants'; -import type { StartPlugins } from '../../../../plugin'; import { getSessionIDfromKibanaRequest, createAlertStatusPayloads, @@ -33,8 +31,7 @@ import { export const setSignalsStatusRoute = ( router: SecuritySolutionPluginRouter, logger: Logger, - sender: ITelemetryEventsSender, - startServices: StartServicesAccessor + sender: ITelemetryEventsSender ) => { router.versioned .post({ @@ -65,14 +62,10 @@ export const setSignalsStatusRoute = ( if (!siemClient) { return siemResponse.error({ statusCode: 404 }); } - const [_, { security }] = await startServices(); - const user = security.authc.getCurrentUser(request); + const user = core.security.authc.getCurrentUser(); const clusterId = sender.getClusterID(); - const [isTelemetryOptedIn, username] = await Promise.all([ - sender.isTelemetryOptedIn(), - security?.authc.getCurrentUser(request)?.username, - ]); + const isTelemetryOptedIn = await sender.isTelemetryOptedIn(); if (isTelemetryOptedIn && clusterId) { // Sometimes the ids are in the query not passed in the request? @@ -82,12 +75,12 @@ export const setSignalsStatusRoute = ( : (get(request.body.query, 'bool.filter.terms._id') as string[]); // Get Context for Insights Payloads const sessionId = getSessionIDfromKibanaRequest(clusterId, request); - if (username && toSendAlertIds && sessionId && status) { + if (user?.username && toSendAlertIds && sessionId && status) { const insightsPayloads = createAlertStatusPayloads( clusterId, toSendAlertIds, sessionId, - username, + user.username, DETECTION_ENGINE_SIGNALS_STATUS_URL, status ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_exceptions/api/create_rule_exceptions/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_exceptions/api/create_rule_exceptions/route.ts index c8c257770e26a..060043c14819f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_exceptions/api/create_rule_exceptions/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_exceptions/api/create_rule_exceptions/route.ts @@ -295,7 +295,7 @@ export const createAndAssociateDefaultExceptionList = async ({ : existingRuleExceptionLists; await detectionRulesClient.patchRule({ - nextParams: { + rulePatch: { rule_id: rule.params.ruleId, ...rule.params, exceptions_list: [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/bulk_actions_response.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/bulk_actions_response.ts index 2bf14ccbf085e..1b78da705e346 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/bulk_actions_response.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/bulk_actions_response.ts @@ -25,7 +25,7 @@ import type { BulkActionsDryRunErrCode } from '../../../../../../../common/const import type { PromisePoolError } from '../../../../../../utils/promise_pool'; import type { RuleAlertType } from '../../../../rule_schema'; import type { DryRunError } from '../../../logic/bulk_actions/dry_run'; -import { internalRuleToAPIResponse } from '../../../normalization/rule_converters'; +import { internalRuleToAPIResponse } from '../../../logic/detection_rules_client/converters/internal_rule_to_api_response'; const MAX_ERROR_MESSAGE_LENGTH = 1000; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_patch_rules/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_patch_rules/route.ts index 3b16ba5fa4742..da75e4e33362a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_patch_rules/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_patch_rules/route.ts @@ -86,7 +86,7 @@ export const bulkPatchRulesRoute = (router: SecuritySolutionPluginRouter, logger }); const patchedRule = await detectionRulesClient.patchRule({ - nextParams: payloadRule, + rulePatch: payloadRule, }); return patchedRule; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/patch_rule/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/patch_rule/route.ts index 0e508f43103d6..3886f63c482b0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/patch_rule/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/patch_rule/route.ts @@ -76,7 +76,7 @@ export const patchRuleRoute = (router: SecuritySolutionPluginRouter) => { }); const patchedRule = await detectionRulesClient.patchRule({ - nextParams: params, + rulePatch: params, }); return response.ok({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/index.ts index 7e379651b2faf..f2e147ef3154f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/index.ts @@ -7,12 +7,7 @@ export * from './api/register_routes'; -// TODO: https://github.com/elastic/kibana/pull/142950 -// TODO: Revisit and consider moving to the rule_schema subdomain -export { - commonParamsCamelToSnake, - typeSpecificCamelToSnake, - convertCreateAPIToInternalSchema, -} from './normalization/rule_converters'; +export { commonParamsCamelToSnake } from './logic/detection_rules_client/converters/common_params_camel_to_snake'; +export { typeSpecificCamelToSnake } from './logic/detection_rules_client/converters/type_specific_camel_to_snake'; export { transformFromAlertThrottle, transformToNotifyWhen } from './normalization/rule_actions'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.ts index dd22dac3adc77..1dfa3a9b0e9ad 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.ts @@ -12,7 +12,6 @@ import type { SanitizedRule } from '@kbn/alerting-plugin/common'; import { SERVER_APP_ID } from '../../../../../../common/constants'; import type { InternalRuleCreate, RuleParams } from '../../../rule_schema'; import { transformToActionFrequency } from '../../normalization/rule_actions'; -import { convertImmutableToRuleSource } from '../../normalization/rule_converters'; const DUPLICATE_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.cloneRule.duplicateTitle', @@ -47,7 +46,9 @@ export const duplicateRule = async ({ rule }: DuplicateRuleParams): Promise { + test('should convert rule_source params to snake case', () => { + const transformedParams = commonParamsCamelToSnake({ + ...getBaseRuleParams(), + ruleSource: { + type: 'external', + isCustomized: false, + }, + }); + expect(transformedParams).toEqual( + expect.objectContaining({ + rule_source: { + type: 'external', + is_customized: false, + }, + }) + ); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/common_params_camel_to_snake.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/common_params_camel_to_snake.ts new file mode 100644 index 0000000000000..6f98230043e74 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/common_params_camel_to_snake.ts @@ -0,0 +1,47 @@ +/* + * 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 { convertObjectKeysToSnakeCase } from '../../../../../../utils/object_case_converters'; +import type { BaseRuleParams } from '../../../../rule_schema'; +import { migrateLegacyInvestigationFields } from '../../../utils/utils'; + +export const commonParamsCamelToSnake = (params: BaseRuleParams) => { + return { + description: params.description, + risk_score: params.riskScore, + severity: params.severity, + building_block_type: params.buildingBlockType, + namespace: params.namespace, + note: params.note, + license: params.license, + output_index: params.outputIndex, + timeline_id: params.timelineId, + timeline_title: params.timelineTitle, + meta: params.meta, + rule_name_override: params.ruleNameOverride, + timestamp_override: params.timestampOverride, + timestamp_override_fallback_disabled: params.timestampOverrideFallbackDisabled, + investigation_fields: migrateLegacyInvestigationFields(params.investigationFields), + author: params.author, + false_positives: params.falsePositives, + from: params.from, + rule_id: params.ruleId, + max_signals: params.maxSignals, + risk_score_mapping: params.riskScoreMapping, + severity_mapping: params.severityMapping, + threat: params.threat, + to: params.to, + references: params.references, + version: params.version, + exceptions_list: params.exceptionsList, + immutable: params.immutable, + rule_source: convertObjectKeysToSnakeCase(params.ruleSource), + related_integrations: params.relatedIntegrations ?? [], + required_fields: params.requiredFields ?? [], + setup: params.setup ?? '', + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_alerting_rule_to_rule_response.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_alerting_rule_to_rule_response.ts new file mode 100644 index 0000000000000..ab7fb237e64f1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_alerting_rule_to_rule_response.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SanitizedRule } from '@kbn/alerting-plugin/common'; +import { stringifyZodError } from '@kbn/zod-helpers'; +import { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import type { RuleParams } from '../../../../rule_schema'; +import { internalRuleToAPIResponse } from './internal_rule_to_api_response'; +import { RuleResponseValidationError } from '../utils'; + +export function convertAlertingRuleToRuleResponse(rule: SanitizedRule): RuleResponse { + const parseResult = RuleResponse.safeParse(internalRuleToAPIResponse(rule)); + + if (!parseResult.success) { + throw new RuleResponseValidationError({ + message: stringifyZodError(parseResult.error), + ruleId: rule.params.ruleId, + }); + } + + return parseResult.data; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response.ts new file mode 100644 index 0000000000000..0cb42100d4512 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response.ts @@ -0,0 +1,39 @@ +/* + * 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 { v4 as uuidv4 } from 'uuid'; +import { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import { addEcsToRequiredFields } from '../../../utils/utils'; +import type { PrebuiltRuleAsset } from '../../../../prebuilt_rules'; +import { RULE_DEFAULTS } from '../mergers/apply_rule_defaults'; + +export const convertPrebuiltRuleAssetToRuleResponse = ( + prebuiltRuleAsset: PrebuiltRuleAsset +): RuleResponse => { + const immutable = true; + + const ruleResponseSpecificFields = { + id: uuidv4(), + updated_at: new Date().toISOString(), + updated_by: '', + created_at: new Date().toISOString(), + created_by: '', + immutable, + rule_source: { + type: 'external', + is_customized: false, + }, + revision: 1, + }; + + return RuleResponse.parse({ + ...RULE_DEFAULTS, + ...prebuiltRuleAsset, + required_fields: addEcsToRequiredFields(prebuiltRuleAsset.required_fields), + ...ruleResponseSpecificFields, + }); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_rule_response_to_alerting_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_rule_response_to_alerting_rule.ts new file mode 100644 index 0000000000000..60a41211a66c5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_rule_response_to_alerting_rule.ts @@ -0,0 +1,210 @@ +/* + * 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 { UpdateRuleData } from '@kbn/alerting-plugin/server/application/rule/methods/update'; +import type { + RuleResponse, + TypeSpecificCreateProps, +} from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import { + transformRuleToAlertAction, + transformRuleToAlertResponseAction, +} from '../../../../../../../common/detection_engine/transform_actions'; +import { + normalizeMachineLearningJobIds, + normalizeThresholdObject, +} from '../../../../../../../common/detection_engine/utils'; +import { assertUnreachable } from '../../../../../../../common/utility_types'; +import { convertObjectKeysToCamelCase } from '../../../../../../utils/object_case_converters'; +import type { RuleParams, TypeSpecificRuleParams } from '../../../../rule_schema'; +import { transformToActionFrequency } from '../../../normalization/rule_actions'; +import { addEcsToRequiredFields } from '../../../utils/utils'; + +/** + * These are the fields that are added to the rule response that are not part of the rule params + */ +type RuntimeFields = + | 'id' + | 'created_at' + | 'updated_at' + | 'created_by' + | 'updated_by' + | 'revision' + | 'execution_summary'; + +export const convertRuleResponseToAlertingRule = ( + rule: Omit +): UpdateRuleData => { + const alertActions = rule.actions.map((action) => transformRuleToAlertAction(action)); + const actions = transformToActionFrequency(alertActions, rule.throttle); + + // Because of Omit Typescript doesn't recognize + // that rule is assignable to TypeSpecificCreateProps despite omitted fields + // are not part of type specific props. So we need to cast here. + const typeSpecificParams = typeSpecificSnakeToCamel(rule as TypeSpecificCreateProps); + + return { + name: rule.name, + tags: rule.tags, + params: { + author: rule.author, + buildingBlockType: rule.building_block_type, + description: rule.description, + ruleId: rule.rule_id, + falsePositives: rule.false_positives, + from: rule.from, + investigationFields: rule.investigation_fields, + immutable: rule.immutable, + ruleSource: convertObjectKeysToCamelCase(rule.rule_source), + license: rule.license, + outputIndex: rule.output_index ?? '', + timelineId: rule.timeline_id, + timelineTitle: rule.timeline_title, + meta: rule.meta, + maxSignals: rule.max_signals, + relatedIntegrations: rule.related_integrations, + requiredFields: addEcsToRequiredFields(rule.required_fields), + riskScore: rule.risk_score, + riskScoreMapping: rule.risk_score_mapping, + ruleNameOverride: rule.rule_name_override, + setup: rule.setup, + severity: rule.severity, + severityMapping: rule.severity_mapping, + threat: rule.threat, + timestampOverride: rule.timestamp_override, + timestampOverrideFallbackDisabled: rule.timestamp_override_fallback_disabled, + to: rule.to, + references: rule.references, + namespace: rule.namespace, + note: rule.note, + version: rule.version, + exceptionsList: rule.exceptions_list, + ...typeSpecificParams, + }, + schedule: { interval: rule.interval }, + actions, + }; +}; + +// Converts params from the snake case API format to the internal camel case format AND applies default values where needed. +// Notice that params.language is possibly undefined for most rule types in the API but we default it to kuery to match +// the legacy API behavior +const typeSpecificSnakeToCamel = (params: TypeSpecificCreateProps): TypeSpecificRuleParams => { + switch (params.type) { + case 'eql': { + return { + type: params.type, + language: params.language, + index: params.index, + dataViewId: params.data_view_id, + query: params.query, + filters: params.filters, + timestampField: params.timestamp_field, + eventCategoryOverride: params.event_category_override, + tiebreakerField: params.tiebreaker_field, + alertSuppression: convertObjectKeysToCamelCase(params.alert_suppression), + }; + } + case 'esql': { + return { + type: params.type, + language: params.language, + query: params.query, + alertSuppression: convertObjectKeysToCamelCase(params.alert_suppression), + }; + } + case 'threat_match': { + return { + type: params.type, + language: params.language ?? 'kuery', + index: params.index, + dataViewId: params.data_view_id, + query: params.query, + filters: params.filters, + savedId: params.saved_id, + threatFilters: params.threat_filters, + threatQuery: params.threat_query, + threatMapping: params.threat_mapping, + threatLanguage: params.threat_language, + threatIndex: params.threat_index, + threatIndicatorPath: params.threat_indicator_path, + concurrentSearches: params.concurrent_searches, + itemsPerSearch: params.items_per_search, + alertSuppression: convertObjectKeysToCamelCase(params.alert_suppression), + }; + } + case 'query': { + return { + type: params.type, + language: params.language ?? 'kuery', + index: params.index, + dataViewId: params.data_view_id, + query: params.query ?? '', + filters: params.filters, + savedId: params.saved_id, + responseActions: params.response_actions?.map((rule) => + transformRuleToAlertResponseAction(rule) + ), + alertSuppression: convertObjectKeysToCamelCase(params.alert_suppression), + }; + } + case 'saved_query': { + return { + type: params.type, + language: params.language ?? 'kuery', + index: params.index, + query: params.query, + filters: params.filters, + savedId: params.saved_id, + dataViewId: params.data_view_id, + responseActions: params.response_actions?.map((rule) => + transformRuleToAlertResponseAction(rule) + ), + alertSuppression: convertObjectKeysToCamelCase(params.alert_suppression), + }; + } + case 'threshold': { + return { + type: params.type, + language: params.language ?? 'kuery', + index: params.index, + dataViewId: params.data_view_id, + query: params.query, + filters: params.filters, + savedId: params.saved_id, + threshold: normalizeThresholdObject(params.threshold), + alertSuppression: params.alert_suppression?.duration + ? { duration: params.alert_suppression.duration } + : undefined, + }; + } + case 'machine_learning': { + return { + type: params.type, + anomalyThreshold: params.anomaly_threshold, + machineLearningJobId: normalizeMachineLearningJobIds(params.machine_learning_job_id), + alertSuppression: convertObjectKeysToCamelCase(params.alert_suppression), + }; + } + case 'new_terms': { + return { + type: params.type, + query: params.query, + newTermsFields: params.new_terms_fields, + historyWindowStart: params.history_window_start, + index: params.index, + filters: params.filters, + language: params.language ?? 'kuery', + dataViewId: params.data_view_id, + alertSuppression: convertObjectKeysToCamelCase(params.alert_suppression), + }; + } + default: { + return assertUnreachable(params); + } + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/internal_rule_to_api_response.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/internal_rule_to_api_response.ts new file mode 100644 index 0000000000000..452f59df8dcf9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/internal_rule_to_api_response.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 type { ResolvedSanitizedRule, SanitizedRule } from '@kbn/alerting-plugin/common'; +import type { RequiredOptional } from '@kbn/zod-helpers'; +import type { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import { transformAlertToRuleAction } from '../../../../../../../common/detection_engine/transform_actions'; +import { createRuleExecutionSummary } from '../../../../rule_monitoring'; +import type { RuleParams } from '../../../../rule_schema'; +import { + transformFromAlertThrottle, + transformToActionFrequency, +} from '../../../normalization/rule_actions'; +import { typeSpecificCamelToSnake } from './type_specific_camel_to_snake'; +import { commonParamsCamelToSnake } from './common_params_camel_to_snake'; + +export const internalRuleToAPIResponse = ( + rule: SanitizedRule | ResolvedSanitizedRule +): RequiredOptional => { + const executionSummary = createRuleExecutionSummary(rule); + + const isResolvedRule = (obj: unknown): obj is ResolvedSanitizedRule => { + const outcome = (obj as ResolvedSanitizedRule).outcome; + return outcome != null && outcome !== 'exactMatch'; + }; + + const alertActions = rule.actions.map(transformAlertToRuleAction); + const throttle = transformFromAlertThrottle(rule); + const actions = transformToActionFrequency(alertActions, throttle); + + return { + // saved object properties + outcome: isResolvedRule(rule) ? rule.outcome : undefined, + alias_target_id: isResolvedRule(rule) ? rule.alias_target_id : undefined, + alias_purpose: isResolvedRule(rule) ? rule.alias_purpose : undefined, + // Alerting framework params + id: rule.id, + updated_at: rule.updatedAt.toISOString(), + updated_by: rule.updatedBy ?? 'elastic', + created_at: rule.createdAt.toISOString(), + created_by: rule.createdBy ?? 'elastic', + name: rule.name, + tags: rule.tags, + interval: rule.schedule.interval, + enabled: rule.enabled, + revision: rule.revision, + // Security solution shared rule params + ...commonParamsCamelToSnake(rule.params), + // Type specific security solution rule params + ...typeSpecificCamelToSnake(rule.params), + // Actions + throttle: undefined, + actions, + // Execution summary + execution_summary: executionSummary ?? undefined, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/type_specific_camel_to_snake.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/type_specific_camel_to_snake.test.ts new file mode 100644 index 0000000000000..08e6d3cc64b0a --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/type_specific_camel_to_snake.test.ts @@ -0,0 +1,67 @@ +/* + * 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 { + AlertSuppressionDuration, + AlertSuppressionMissingFieldsStrategy, +} from '../../../../../../../common/api/detection_engine'; +import { getEqlRuleParams } from '../../../../rule_schema/mocks'; +import { typeSpecificCamelToSnake } from './type_specific_camel_to_snake'; + +describe('typeSpecificCamelToSnake', () => { + describe('EQL', () => { + test('should accept EQL params when existing rule type is EQL', () => { + const params = { + timestampField: 'event.created', + eventCategoryOverride: 'event.not_category', + tiebreakerField: 'event.created', + }; + const eqlRule = { ...getEqlRuleParams(), ...params }; + const transformedParams = typeSpecificCamelToSnake(eqlRule); + expect(transformedParams).toEqual( + expect.objectContaining({ + timestamp_field: 'event.created', + event_category_override: 'event.not_category', + tiebreaker_field: 'event.created', + }) + ); + }); + + test('should accept EQL params with suppression in camel case and convert to snake case when rule type is EQL', () => { + const params = { + timestampField: 'event.created', + eventCategoryOverride: 'event.not_category', + tiebreakerField: 'event.created', + alertSuppression: { + groupBy: ['event.type'], + duration: { + value: 10, + unit: 'm', + } as AlertSuppressionDuration, + missingFieldsStrategy: 'suppress' as AlertSuppressionMissingFieldsStrategy, + }, + }; + const eqlRule = { ...getEqlRuleParams(), ...params }; + const transformedParams = typeSpecificCamelToSnake(eqlRule); + expect(transformedParams).toEqual( + expect.objectContaining({ + timestamp_field: 'event.created', + event_category_override: 'event.not_category', + tiebreaker_field: 'event.created', + alert_suppression: { + group_by: ['event.type'], + duration: { + value: 10, + unit: 'm', + } as AlertSuppressionDuration, + missing_fields_strategy: 'suppress', + }, + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/type_specific_camel_to_snake.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/type_specific_camel_to_snake.ts new file mode 100644 index 0000000000000..0808d1921e9bf --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/type_specific_camel_to_snake.ts @@ -0,0 +1,127 @@ +/* + * 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 { RequiredOptional } from '@kbn/zod-helpers'; +import type { TypeSpecificResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import { transformAlertToRuleResponseAction } from '../../../../../../../common/detection_engine/transform_actions'; +import { assertUnreachable } from '../../../../../../../common/utility_types'; +import { convertObjectKeysToSnakeCase } from '../../../../../../utils/object_case_converters'; +import type { TypeSpecificRuleParams } from '../../../../rule_schema'; + +export const typeSpecificCamelToSnake = ( + params: TypeSpecificRuleParams +): RequiredOptional => { + switch (params.type) { + case 'eql': { + return { + type: params.type, + language: params.language, + index: params.index, + data_view_id: params.dataViewId, + query: params.query, + filters: params.filters, + timestamp_field: params.timestampField, + event_category_override: params.eventCategoryOverride, + tiebreaker_field: params.tiebreakerField, + alert_suppression: convertObjectKeysToSnakeCase(params.alertSuppression), + }; + } + case 'esql': { + return { + type: params.type, + language: params.language, + query: params.query, + alert_suppression: convertObjectKeysToSnakeCase(params.alertSuppression), + }; + } + case 'threat_match': { + return { + type: params.type, + language: params.language, + index: params.index, + data_view_id: params.dataViewId, + query: params.query, + filters: params.filters, + saved_id: params.savedId, + threat_filters: params.threatFilters, + threat_query: params.threatQuery, + threat_mapping: params.threatMapping, + threat_language: params.threatLanguage, + threat_index: params.threatIndex, + threat_indicator_path: params.threatIndicatorPath, + concurrent_searches: params.concurrentSearches, + items_per_search: params.itemsPerSearch, + alert_suppression: convertObjectKeysToSnakeCase(params.alertSuppression), + }; + } + case 'query': { + return { + type: params.type, + language: params.language, + index: params.index, + data_view_id: params.dataViewId, + query: params.query, + filters: params.filters, + saved_id: params.savedId, + response_actions: params.responseActions?.map(transformAlertToRuleResponseAction), + alert_suppression: convertObjectKeysToSnakeCase(params.alertSuppression), + }; + } + case 'saved_query': { + return { + type: params.type, + language: params.language, + index: params.index, + query: params.query, + filters: params.filters, + saved_id: params.savedId, + data_view_id: params.dataViewId, + response_actions: params.responseActions?.map(transformAlertToRuleResponseAction), + alert_suppression: convertObjectKeysToSnakeCase(params.alertSuppression), + }; + } + case 'threshold': { + return { + type: params.type, + language: params.language, + index: params.index, + data_view_id: params.dataViewId, + query: params.query, + filters: params.filters, + saved_id: params.savedId, + threshold: params.threshold, + alert_suppression: params.alertSuppression?.duration + ? { duration: params.alertSuppression?.duration } + : undefined, + }; + } + case 'machine_learning': { + return { + type: params.type, + anomaly_threshold: params.anomalyThreshold, + machine_learning_job_id: params.machineLearningJobId, + alert_suppression: convertObjectKeysToSnakeCase(params.alertSuppression), + }; + } + case 'new_terms': { + return { + type: params.type, + query: params.query, + new_terms_fields: params.newTermsFields, + history_window_start: params.historyWindowStart, + index: params.index, + filters: params.filters, + language: params.language, + data_view_id: params.dataViewId, + alert_suppression: convertObjectKeysToSnakeCase(params.alertSuppression), + }; + } + default: { + return assertUnreachable(params); + } + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.create_custom_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.create_custom_rule.test.ts index 7aab6640a1b52..5578854ed95b2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.create_custom_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.create_custom_rule.test.ts @@ -6,6 +6,7 @@ */ import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks'; +import { savedObjectsClientMock } from '@kbn/core/server/mocks'; import { getCreateRulesSchemaMock, @@ -35,7 +36,8 @@ describe('DetectionRulesClient.createCustomRule', () => { rulesClient = rulesClientMock.create(); rulesClient.create.mockResolvedValue(getRuleMock(getQueryRuleParams())); - detectionRulesClient = createDetectionRulesClient(rulesClient, mlAuthz); + const savedObjectsClient = savedObjectsClientMock.create(); + detectionRulesClient = createDetectionRulesClient({ rulesClient, mlAuthz, savedObjectsClient }); }); it('should create a rule with the correct parameters and options', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.create_prebuilt_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.create_prebuilt_rule.test.ts index fd3ac991a968f..f91c577f3b2a0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.create_prebuilt_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.create_prebuilt_rule.test.ts @@ -6,6 +6,7 @@ */ import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks'; +import { savedObjectsClientMock } from '@kbn/core/server/mocks'; import { getCreateRulesSchemaMock, @@ -35,7 +36,8 @@ describe('DetectionRulesClient.createPrebuiltRule', () => { rulesClient = rulesClientMock.create(); rulesClient.create.mockResolvedValue(getRuleMock(getQueryRuleParams())); - detectionRulesClient = createDetectionRulesClient(rulesClient, mlAuthz); + const savedObjectsClient = savedObjectsClientMock.create(); + detectionRulesClient = createDetectionRulesClient({ rulesClient, mlAuthz, savedObjectsClient }); }); it('creates a rule with the correct parameters and options', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.delete_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.delete_rule.test.ts index 37cb8e0aa709e..166656701f304 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.delete_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.delete_rule.test.ts @@ -6,6 +6,7 @@ */ import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks'; +import { savedObjectsClientMock } from '@kbn/core/server/mocks'; import { buildMlAuthz } from '../../../../machine_learning/authz'; import { createDetectionRulesClient } from './detection_rules_client'; import type { IDetectionRulesClient } from './detection_rules_client_interface'; @@ -20,7 +21,8 @@ describe('DetectionRulesClient.deleteRule', () => { beforeEach(() => { rulesClient = rulesClientMock.create(); - detectionRulesClient = createDetectionRulesClient(rulesClient, mlAuthz); + const savedObjectsClient = savedObjectsClientMock.create(); + detectionRulesClient = createDetectionRulesClient({ rulesClient, mlAuthz, savedObjectsClient }); }); it('should call rulesClient.delete passing the expected ruleId', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.import_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.import_rule.test.ts index 474fecc186519..fb9b4f7995c90 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.import_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.import_rule.test.ts @@ -6,19 +6,23 @@ */ import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks'; -import { readRules } from './read_rules'; -import { getCreateRulesSchemaMock } from '../../../../../../common/api/detection_engine/model/rule_schema/mocks'; -import { getRuleMock } from '../../../routes/__mocks__/request_responses'; -import { getQueryRuleParams } from '../../../rule_schema/mocks'; +import { savedObjectsClientMock } from '@kbn/core/server/mocks'; +import { + getCreateRulesSchemaMock, + getRulesSchemaMock, +} from '../../../../../../common/api/detection_engine/model/rule_schema/mocks'; import { buildMlAuthz } from '../../../../machine_learning/authz'; import { throwAuthzError } from '../../../../machine_learning/validation'; +import { getRuleMock } from '../../../routes/__mocks__/request_responses'; +import { getQueryRuleParams } from '../../../rule_schema/mocks'; import { createDetectionRulesClient } from './detection_rules_client'; import type { IDetectionRulesClient } from './detection_rules_client_interface'; +import { getRuleByRuleId } from './methods/get_rule_by_rule_id'; jest.mock('../../../../machine_learning/authz'); jest.mock('../../../../machine_learning/validation'); -jest.mock('./read_rules'); +jest.mock('./methods/get_rule_by_rule_id'); describe('DetectionRulesClient.importRule', () => { let rulesClient: ReturnType; @@ -34,21 +38,19 @@ describe('DetectionRulesClient.importRule', () => { version: 1, immutable, }; - const existingRule = getRuleMock({ - ...getQueryRuleParams({ - ruleId: ruleToImport.rule_id, - }), - }); + const existingRule = getRulesSchemaMock(); + existingRule.rule_id = ruleToImport.rule_id; beforeEach(() => { rulesClient = rulesClientMock.create(); rulesClient.create.mockResolvedValue(getRuleMock(getQueryRuleParams())); rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); - detectionRulesClient = createDetectionRulesClient(rulesClient, mlAuthz); + const savedObjectsClient = savedObjectsClientMock.create(); + detectionRulesClient = createDetectionRulesClient({ rulesClient, mlAuthz, savedObjectsClient }); }); it('calls rulesClient.create with the correct parameters when rule_id does not match an installed rule', async () => { - (readRules as jest.Mock).mockResolvedValue(null); + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(null); await detectionRulesClient.importRule({ ruleToImport, overwriteRules: true, @@ -90,7 +92,8 @@ describe('DetectionRulesClient.importRule', () => { describe('when rule_id matches an installed rule', () => { it('calls rulesClient.update with the correct parameters when overwriteRules is true', async () => { - (readRules as jest.Mock).mockResolvedValue(existingRule); + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); + await detectionRulesClient.importRule({ ruleToImport, overwriteRules: true, @@ -122,12 +125,9 @@ describe('DetectionRulesClient.importRule', () => { it('ensures overwritten rule DOES NOT preserve fields missed in the imported rule when "overwriteRules" is "true" and matching rule found', async () => { const existingRuleWithTimestampOverride = { ...existingRule, - params: { - ...existingRule.params, - timestamp_override: '2020-01-01T00:00:00Z', - }, + timestamp_override: '2020-01-01T00:00:00Z', }; - (readRules as jest.Mock).mockResolvedValue(existingRuleWithTimestampOverride); + (getRuleByRuleId as jest.Mock).mockResolvedValue(existingRuleWithTimestampOverride); await detectionRulesClient.importRule({ ruleToImport: { @@ -151,7 +151,7 @@ describe('DetectionRulesClient.importRule', () => { }); it('rejects when overwriteRules is false', async () => { - (readRules as jest.Mock).mockResolvedValue(existingRule); + (getRuleByRuleId as jest.Mock).mockResolvedValue(existingRule); await expect( detectionRulesClient.importRule({ ruleToImport, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.patch_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.patch_rule.test.ts index 7f1c219888636..d17b12415642a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.patch_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.patch_rule.test.ts @@ -12,17 +12,20 @@ import { getMlRuleParams, getQueryRuleParams } from '../../../rule_schema/mocks' import { getCreateMachineLearningRulesSchemaMock, getCreateRulesSchemaMock, + getRulesMlSchemaMock, + getRulesSchemaMock, } from '../../../../../../common/api/detection_engine/model/rule_schema/mocks'; -import { readRules } from './read_rules'; +import { getRuleByRuleId } from './methods/get_rule_by_rule_id'; import { buildMlAuthz } from '../../../../machine_learning/authz'; import { throwAuthzError } from '../../../../machine_learning/validation'; import { createDetectionRulesClient } from './detection_rules_client'; import type { IDetectionRulesClient } from './detection_rules_client_interface'; +import { savedObjectsClientMock } from '@kbn/core/server/mocks'; jest.mock('../../../../machine_learning/authz'); jest.mock('../../../../machine_learning/validation'); -jest.mock('./read_rules'); +jest.mock('./methods/get_rule_by_rule_id'); describe('DetectionRulesClient.patchRule', () => { let rulesClient: ReturnType; @@ -32,97 +35,78 @@ describe('DetectionRulesClient.patchRule', () => { beforeEach(() => { rulesClient = rulesClientMock.create(); - detectionRulesClient = createDetectionRulesClient(rulesClient, mlAuthz); + const savedObjectsClient = savedObjectsClientMock.create(); + detectionRulesClient = createDetectionRulesClient({ rulesClient, mlAuthz, savedObjectsClient }); }); it('calls the rulesClient with expected params', async () => { - const nextParams = getCreateRulesSchemaMock(); - const existingRule = getRuleMock(getQueryRuleParams()); - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); + // Mock the existing rule + const existingRule = getRulesSchemaMock(); + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); + + // Mock the rule update + const rulePatch = getCreateRulesSchemaMock('query-rule-id'); + rulePatch.name = 'new name'; + rulePatch.description = 'new description'; + + // Mock the rule returned after update; not used for this test directly but + // needed so that the patchRule method does not throw rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); - await detectionRulesClient.patchRule({ nextParams }); + await detectionRulesClient.patchRule({ rulePatch }); expect(rulesClient.update).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ - name: nextParams.name, + name: rulePatch.name, params: expect.objectContaining({ - ruleId: nextParams.rule_id, - description: nextParams.description, + ruleId: rulePatch.rule_id, + description: rulePatch.description, }), }), }) ); }); - it('returns rule enabled: true if the nexParams have enabled: true', async () => { - const nextParams = { ...getCreateRulesSchemaMock(), enabled: true }; - const existingRule = getRuleMock(getQueryRuleParams()); - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); - rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); + it('enables the rule if the nexParams have enabled: true', async () => { + // Mock the existing rule + const existingRule = getRulesSchemaMock(); + existingRule.enabled = false; + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); - const rule = await detectionRulesClient.patchRule({ nextParams }); + // Mock the rule update + const rulePatch = { ...getCreateRulesSchemaMock(), enabled: true }; - expect(rule.enabled).toBe(true); - }); + // Mock the rule returned after update; not used for this test directly but + // needed so that the patchRule method does not throw + rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); - it('calls the rulesClient with legacy ML params', async () => { - const nextParams = getCreateMachineLearningRulesSchemaMock(); - const existingRule = getRuleMock(getMlRuleParams()); - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); - rulesClient.update.mockResolvedValue(getRuleMock(getMlRuleParams())); + const rule = await detectionRulesClient.patchRule({ rulePatch }); - await detectionRulesClient.patchRule({ nextParams }); - expect(rulesClient.update).toHaveBeenCalledWith( + expect(rule.enabled).toBe(true); + expect(rulesClient.enable).toHaveBeenCalledWith( expect.objectContaining({ - data: expect.objectContaining({ - params: expect.objectContaining({ - anomalyThreshold: 58, - machineLearningJobId: ['typical-ml-job-id'], - }), - }), + id: existingRule.id, }) ); }); - it('calls the rulesClient with new ML params', async () => { - const nextParams = { - ...getCreateMachineLearningRulesSchemaMock(), - machine_learning_job_id: ['new_job_1', 'new_job_2'], - }; - const existingRule = getRuleMock(getMlRuleParams()); - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); - rulesClient.update.mockResolvedValue(getRuleMock(getMlRuleParams())); - - await detectionRulesClient.patchRule({ nextParams }); + it('disables the rule if the nexParams have enabled: false', async () => { + // Mock the existing rule + const existingRule = getRulesSchemaMock(); + existingRule.enabled = true; + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); - expect(rulesClient.update).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ - params: expect.objectContaining({ - anomalyThreshold: 58, - machineLearningJobId: ['new_job_1', 'new_job_2'], - }), - }), - }) - ); - }); + // Mock the rule update + const rulePatch = { ...getCreateRulesSchemaMock(), enabled: false }; - it('should call rulesClient.disable if the rule was enabled and enabled is false', async () => { - const nextParams = { - ...getCreateRulesSchemaMock(), - enabled: false, - }; - const existingRule = { - ...getRuleMock(getQueryRuleParams()), - enabled: true, - }; - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); + // Mock the rule returned after update; not used for this test directly but + // needed so that the patchRule method does not throw rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); - await detectionRulesClient.patchRule({ nextParams }); + const rule = await detectionRulesClient.patchRule({ rulePatch }); + expect(rule.enabled).toBe(false); expect(rulesClient.disable).toHaveBeenCalledWith( expect.objectContaining({ id: existingRule.id, @@ -130,23 +114,29 @@ describe('DetectionRulesClient.patchRule', () => { ); }); - it('should call rulesClient.enable if the rule was disabled and enabled is true', async () => { - const nextParams = { - ...getCreateRulesSchemaMock(), - enabled: true, - }; - const existingRule = { - ...getRuleMock(getQueryRuleParams()), - enabled: false, - }; - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); - rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); + it('calls the rulesClient with new ML params', async () => { + // Mock the existing rule + const existingRule = getRulesMlSchemaMock(); + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); - await detectionRulesClient.patchRule({ nextParams }); + // Mock the rule update + const rulePatch = getCreateMachineLearningRulesSchemaMock(); + rulePatch.anomaly_threshold = 42; + rulePatch.machine_learning_job_id = ['new-job-id']; - expect(rulesClient.enable).toHaveBeenCalledWith( + // Mock the rule returned after update; not used for this test directly but + // needed so that the patchRule method does not throw + rulesClient.update.mockResolvedValue(getRuleMock(getMlRuleParams())); + + await detectionRulesClient.patchRule({ rulePatch }); + expect(rulesClient.update).toHaveBeenCalledWith( expect.objectContaining({ - id: existingRule.id, + data: expect.objectContaining({ + params: expect.objectContaining({ + anomalyThreshold: rulePatch.anomaly_threshold, + machineLearningJobId: rulePatch.machine_learning_job_id, + }), + }), }) ); }); @@ -156,21 +146,23 @@ describe('DetectionRulesClient.patchRule', () => { throw new Error('mocked MLAuth error'); }); - const nextParams = { - ...getCreateRulesSchemaMock(), - enabled: true, - }; + const rulePatch = getCreateRulesSchemaMock(); - await expect(detectionRulesClient.patchRule({ nextParams })).rejects.toThrow( + await expect(detectionRulesClient.patchRule({ rulePatch })).rejects.toThrow( 'mocked MLAuth error' ); expect(rulesClient.create).not.toHaveBeenCalled(); }); - describe('regression tests', () => { + describe('actions', () => { it("updates the rule's actions if provided", async () => { - const nextParams = { + // Mock the existing rule + const existingRule = getRulesSchemaMock(); + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); + + // Mock the rule update + const rulePatch = { ...getCreateRulesSchemaMock(), actions: [ { @@ -183,11 +175,12 @@ describe('DetectionRulesClient.patchRule', () => { }, ], }; - const existingRule = getRuleMock(getQueryRuleParams()); - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); + + // Mock the rule returned after update; not used for this test directly but + // needed so that the patchRule method does not throw rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); - await detectionRulesClient.patchRule({ nextParams }); + await detectionRulesClient.patchRule({ rulePatch }); expect(rulesClient.update).toHaveBeenCalledWith( expect.objectContaining({ @@ -209,12 +202,12 @@ describe('DetectionRulesClient.patchRule', () => { }); it('does not update actions if none are specified', async () => { - const nextParams = getCreateRulesSchemaMock(); - delete nextParams.actions; - const existingRule = getRuleMock(getQueryRuleParams()); + // Mock the existing rule + const existingRule = getRulesSchemaMock(); + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); existingRule.actions = [ { - actionTypeId: '.slack', + action_type_id: '.slack', id: '2933e581-d81c-4fe3-88fe-c57c6b8a5bfd', params: { message: 'Rule {{context.rule.name}} generated {{state.signals_count}} signals', @@ -222,10 +215,16 @@ describe('DetectionRulesClient.patchRule', () => { group: 'default', }, ]; - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); + + // Mock the rule update + const rulePatch = getCreateRulesSchemaMock(); + delete rulePatch.actions; + + // Mock the rule returned after update; not used for this test directly but + // needed so that the patchRule method does not throw rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); - await detectionRulesClient.patchRule({ nextParams }); + await detectionRulesClient.patchRule({ rulePatch }); expect(rulesClient.update).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.ts index ce6043a420907..dfcfc8f7fa393 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.ts @@ -6,73 +6,110 @@ */ import type { RulesClient } from '@kbn/alerting-plugin/server'; -import type { MlAuthz } from '../../../../machine_learning/authz'; - +import type { SavedObjectsClientContract } from '@kbn/core/server'; import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema'; +import { withSecuritySpan } from '../../../../../utils/with_security_span'; +import type { MlAuthz } from '../../../../machine_learning/authz'; +import { createPrebuiltRuleAssetsClient } from '../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client'; import type { - IDetectionRulesClient, CreateCustomRuleArgs, CreatePrebuiltRuleArgs, - UpdateRuleArgs, - PatchRuleArgs, DeleteRuleArgs, - UpgradePrebuiltRuleArgs, + IDetectionRulesClient, ImportRuleArgs, + PatchRuleArgs, + UpdateRuleArgs, + UpgradePrebuiltRuleArgs, } from './detection_rules_client_interface'; - -import { createCustomRule } from './methods/create_custom_rule'; -import { createPrebuiltRule } from './methods/create_prebuilt_rule'; -import { updateRule } from './methods/update_rule'; -import { patchRule } from './methods/patch_rule'; +import { createRule } from './methods/create_rule'; import { deleteRule } from './methods/delete_rule'; -import { upgradePrebuiltRule } from './methods/upgrade_prebuilt_rule'; import { importRule } from './methods/import_rule'; +import { patchRule } from './methods/patch_rule'; +import { updateRule } from './methods/update_rule'; +import { upgradePrebuiltRule } from './methods/upgrade_prebuilt_rule'; -import { withSecuritySpan } from '../../../../../utils/with_security_span'; +interface DetectionRulesClientParams { + rulesClient: RulesClient; + savedObjectsClient: SavedObjectsClientContract; + mlAuthz: MlAuthz; +} + +export const createDetectionRulesClient = ({ + rulesClient, + mlAuthz, + savedObjectsClient, +}: DetectionRulesClientParams): IDetectionRulesClient => { + const prebuiltRuleAssetClient = createPrebuiltRuleAssetsClient(savedObjectsClient); -export const createDetectionRulesClient = ( - rulesClient: RulesClient, - mlAuthz: MlAuthz -): IDetectionRulesClient => ({ - async createCustomRule(args: CreateCustomRuleArgs): Promise { - return withSecuritySpan('DetectionRulesClient.createCustomRule', async () => { - return createCustomRule(rulesClient, args, mlAuthz); - }); - }, + return { + async createCustomRule(args: CreateCustomRuleArgs): Promise { + return withSecuritySpan('DetectionRulesClient.createCustomRule', async () => { + return createRule({ + rulesClient, + rule: { + ...args.params, + // For backwards compatibility, we default to true if not provided. + // The default enabled value is false for prebuilt rules, and true + // for custom rules. + enabled: args.params.enabled ?? true, + immutable: false, + }, + mlAuthz, + }); + }); + }, - async createPrebuiltRule(args: CreatePrebuiltRuleArgs): Promise { - return withSecuritySpan('DetectionRulesClient.createPrebuiltRule', async () => { - return createPrebuiltRule(rulesClient, args, mlAuthz); - }); - }, + async createPrebuiltRule(args: CreatePrebuiltRuleArgs): Promise { + return withSecuritySpan('DetectionRulesClient.createPrebuiltRule', async () => { + return createRule({ + rulesClient, + rule: { + ...args.params, + immutable: true, + }, + mlAuthz, + }); + }); + }, - async updateRule(args: UpdateRuleArgs): Promise { - return withSecuritySpan('DetectionRulesClient.updateRule', async () => { - return updateRule(rulesClient, args, mlAuthz); - }); - }, + async updateRule({ ruleUpdate }: UpdateRuleArgs): Promise { + return withSecuritySpan('DetectionRulesClient.updateRule', async () => { + return updateRule({ rulesClient, prebuiltRuleAssetClient, mlAuthz, ruleUpdate }); + }); + }, - async patchRule(args: PatchRuleArgs): Promise { - return withSecuritySpan('DetectionRulesClient.patchRule', async () => { - return patchRule(rulesClient, args, mlAuthz); - }); - }, + async patchRule({ rulePatch }: PatchRuleArgs): Promise { + return withSecuritySpan('DetectionRulesClient.patchRule', async () => { + return patchRule({ rulesClient, prebuiltRuleAssetClient, mlAuthz, rulePatch }); + }); + }, - async deleteRule(args: DeleteRuleArgs): Promise { - return withSecuritySpan('DetectionRulesClient.deleteRule', async () => { - return deleteRule(rulesClient, args); - }); - }, + async deleteRule({ ruleId }: DeleteRuleArgs): Promise { + return withSecuritySpan('DetectionRulesClient.deleteRule', async () => { + return deleteRule({ rulesClient, ruleId }); + }); + }, - async upgradePrebuiltRule(args: UpgradePrebuiltRuleArgs): Promise { - return withSecuritySpan('DetectionRulesClient.upgradePrebuiltRule', async () => { - return upgradePrebuiltRule(rulesClient, args, mlAuthz); - }); - }, + async upgradePrebuiltRule({ ruleAsset }: UpgradePrebuiltRuleArgs): Promise { + return withSecuritySpan('DetectionRulesClient.upgradePrebuiltRule', async () => { + return upgradePrebuiltRule({ + rulesClient, + ruleAsset, + mlAuthz, + prebuiltRuleAssetClient, + }); + }); + }, - async importRule(args: ImportRuleArgs): Promise { - return withSecuritySpan('DetectionRulesClient.importRule', async () => { - return importRule(rulesClient, args, mlAuthz); - }); - }, -}); + async importRule(args: ImportRuleArgs): Promise { + return withSecuritySpan('DetectionRulesClient.importRule', async () => { + return importRule({ + rulesClient, + importRulePayload: args, + mlAuthz, + prebuiltRuleAssetClient, + }); + }); + }, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.update_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.update_rule.test.ts index 671460b046fea..db9d122e7d912 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.update_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.update_rule.test.ts @@ -12,17 +12,20 @@ import { getMlRuleParams, getQueryRuleParams } from '../../../rule_schema/mocks' import { getCreateMachineLearningRulesSchemaMock, getCreateRulesSchemaMock, + getRulesMlSchemaMock, + getRulesSchemaMock, } from '../../../../../../common/api/detection_engine/model/rule_schema/mocks'; -import { readRules } from './read_rules'; +import { getRuleByRuleId } from './methods/get_rule_by_rule_id'; import { buildMlAuthz } from '../../../../machine_learning/authz'; import { throwAuthzError } from '../../../../machine_learning/validation'; import { createDetectionRulesClient } from './detection_rules_client'; import type { IDetectionRulesClient } from './detection_rules_client_interface'; +import { savedObjectsClientMock } from '@kbn/core/server/mocks'; jest.mock('../../../../machine_learning/authz'); jest.mock('../../../../machine_learning/validation'); -jest.mock('./read_rules'); +jest.mock('./methods/get_rule_by_rule_id'); describe('DetectionRulesClient.updateRule', () => { let rulesClient: ReturnType; @@ -32,13 +35,22 @@ describe('DetectionRulesClient.updateRule', () => { beforeEach(() => { rulesClient = rulesClientMock.create(); - detectionRulesClient = createDetectionRulesClient(rulesClient, mlAuthz); + const savedObjectsClient = savedObjectsClientMock.create(); + detectionRulesClient = createDetectionRulesClient({ rulesClient, mlAuthz, savedObjectsClient }); }); it('calls the rulesClient with expected params', async () => { - const ruleUpdate = getCreateRulesSchemaMock(); - const existingRule = getRuleMock(getQueryRuleParams()); - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); + // Mock the existing rule + const existingRule = getRulesSchemaMock(); + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); + + // Mock the rule update + const ruleUpdate = getCreateRulesSchemaMock('query-rule-id'); + ruleUpdate.name = 'new name'; + ruleUpdate.description = 'new description'; + + // Mock the rule returned after update; not used for this test directly but + // needed so that the patchRule method does not throw rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); await detectionRulesClient.updateRule({ ruleUpdate }); @@ -56,21 +68,18 @@ describe('DetectionRulesClient.updateRule', () => { ); }); - it('returns rule enabled: true if the nexParams have enabled: true', async () => { - const ruleUpdate = { ...getCreateRulesSchemaMock(), enabled: true }; - const existingRule = getRuleMock(getQueryRuleParams()); - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); - rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); - - const rule = await detectionRulesClient.updateRule({ ruleUpdate }); - - expect(rule.enabled).toBe(true); - }); + it('calls the rulesClient with new ML params', async () => { + // Mock the existing rule + const existingRule = getRulesMlSchemaMock(); + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); - it('calls the rulesClient with legacy ML params', async () => { + // Mock the rule update const ruleUpdate = getCreateMachineLearningRulesSchemaMock(); - const existingRule = getRuleMock(getMlRuleParams()); - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); + ruleUpdate.anomaly_threshold = 42; + ruleUpdate.machine_learning_job_id = ['new-job-id']; + + // Mock the rule returned after update; not used for this test directly but + // needed so that the patchRule method does not throw rulesClient.update.mockResolvedValue(getRuleMock(getMlRuleParams())); await detectionRulesClient.updateRule({ ruleUpdate }); @@ -79,48 +88,26 @@ describe('DetectionRulesClient.updateRule', () => { expect.objectContaining({ data: expect.objectContaining({ params: expect.objectContaining({ - anomalyThreshold: 58, - machineLearningJobId: ['typical-ml-job-id'], + anomalyThreshold: ruleUpdate.anomaly_threshold, + machineLearningJobId: ruleUpdate.machine_learning_job_id, }), }), }) ); }); - it('calls the rulesClient with new ML params', async () => { - const ruleUpdate = { - ...getCreateMachineLearningRulesSchemaMock(), - machine_learning_job_id: ['new_job_1', 'new_job_2'], - }; - const existingRule = getRuleMock(getMlRuleParams()); - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); - rulesClient.update.mockResolvedValue(getRuleMock(getMlRuleParams())); + it('disables rule if the rule was enabled and enabled is false', async () => { + // Mock the existing rule + const existingRule = getRulesSchemaMock(); + existingRule.enabled = true; + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); - await detectionRulesClient.updateRule({ ruleUpdate }); + // Mock the rule update + const ruleUpdate = { ...getCreateRulesSchemaMock(), enabled: false }; - expect(rulesClient.update).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ - params: expect.objectContaining({ - anomalyThreshold: 58, - machineLearningJobId: ['new_job_1', 'new_job_2'], - }), - }), - }) - ); - }); - - it('should call rulesClient.disable if the rule was enabled and enabled is false', async () => { - const ruleUpdate = { - ...getCreateRulesSchemaMock(), - enabled: false, - }; - const existingRule = { - ...getRuleMock(getQueryRuleParams()), - enabled: true, - }; + // Mock the rule returned after update; not used for this test directly but + // needed so that the patchRule method does not throw rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); await detectionRulesClient.updateRule({ ruleUpdate }); @@ -131,17 +118,18 @@ describe('DetectionRulesClient.updateRule', () => { ); }); - it('should call rulesClient.enable if the rule was disabled and enabled is true', async () => { - const ruleUpdate = { - ...getCreateRulesSchemaMock(), - enabled: true, - }; - const existingRule = { - ...getRuleMock(getQueryRuleParams()), - enabled: false, - }; + it('enables rule if the rule was disabled and enabled is true', async () => { + // Mock the existing rule + const existingRule = getRulesSchemaMock(); + existingRule.enabled = false; + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); + + // Mock the rule update + const ruleUpdate = { ...getCreateRulesSchemaMock(), enabled: true }; + + // Mock the rule returned after update; not used for this test directly but + // needed so that the patchRule method does not throw rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); await detectionRulesClient.updateRule({ ruleUpdate }); @@ -169,8 +157,13 @@ describe('DetectionRulesClient.updateRule', () => { expect(rulesClient.create).not.toHaveBeenCalled(); }); - describe('regression tests', () => { + describe('actions', () => { it("updates the rule's actions if provided", async () => { + // Mock the existing rule + const existingRule = getRulesSchemaMock(); + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); + + // Mock the rule update const ruleUpdate = { ...getCreateRulesSchemaMock(), actions: [ @@ -184,8 +177,9 @@ describe('DetectionRulesClient.updateRule', () => { }, ], }; - const existingRule = getRuleMock(getQueryRuleParams()); - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); + + // Mock the rule returned after update; not used for this test directly but + // needed so that the patchRule method does not throw rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); await detectionRulesClient.updateRule({ ruleUpdate }); @@ -210,12 +204,12 @@ describe('DetectionRulesClient.updateRule', () => { }); it('updates actions to empty if none are specified', async () => { - const ruleUpdate = getCreateRulesSchemaMock(); - delete ruleUpdate.actions; - const existingRule = getRuleMock(getQueryRuleParams()); + // Mock the existing rule + const existingRule = getRulesSchemaMock(); + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); existingRule.actions = [ { - actionTypeId: '.slack', + action_type_id: '.slack', id: '2933e581-d81c-4fe3-88fe-c57c6b8a5bfd', params: { message: 'Rule {{context.rule.name}} generated {{state.signals_count}} signals', @@ -223,8 +217,14 @@ describe('DetectionRulesClient.updateRule', () => { group: 'default', }, ]; + + // Mock the rule update + const ruleUpdate = getCreateRulesSchemaMock(); + delete ruleUpdate.actions; + + // Mock the rule returned after update; not used for this test directly but + // needed so that the patchRule method does not throw rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); await detectionRulesClient.updateRule({ ruleUpdate }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.upgrade_prebuilt_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.upgrade_prebuilt_rule.test.ts index 38f3507d2f7ae..7a5ae76371532 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.upgrade_prebuilt_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.upgrade_prebuilt_rule.test.ts @@ -10,21 +10,22 @@ import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks'; import { getCreateEqlRuleSchemaMock, getCreateRulesSchemaMock, + getRulesEqlSchemaMock, + getRulesSchemaMock, } from '../../../../../../common/api/detection_engine/model/rule_schema/mocks'; import type { PrebuiltRuleAsset } from '../../../prebuilt_rules'; - -import { readRules } from './read_rules'; +import { getRuleByRuleId } from './methods/get_rule_by_rule_id'; import { getRuleMock } from '../../../routes/__mocks__/request_responses'; import { getEqlRuleParams, getQueryRuleParams } from '../../../rule_schema/mocks'; - import { buildMlAuthz } from '../../../../machine_learning/authz'; import { throwAuthzError } from '../../../../machine_learning/validation'; import { createDetectionRulesClient } from './detection_rules_client'; import type { IDetectionRulesClient } from './detection_rules_client_interface'; +import { savedObjectsClientMock } from '@kbn/core/server/mocks'; jest.mock('../../../../machine_learning/authz'); jest.mock('../../../../machine_learning/validation'); -jest.mock('./read_rules'); +jest.mock('./methods/get_rule_by_rule_id'); describe('DetectionRulesClient.upgradePrebuiltRule', () => { let rulesClient: ReturnType; @@ -34,7 +35,8 @@ describe('DetectionRulesClient.upgradePrebuiltRule', () => { beforeEach(() => { rulesClient = rulesClientMock.create(); - detectionRulesClient = createDetectionRulesClient(rulesClient, mlAuthz); + const savedObjectsClient = savedObjectsClientMock.create(); + detectionRulesClient = createDetectionRulesClient({ rulesClient, mlAuthz, savedObjectsClient }); }); it('throws if no matching rule_id is found', async () => { @@ -44,7 +46,7 @@ describe('DetectionRulesClient.upgradePrebuiltRule', () => { rule_id: 'rule-id', }; - (readRules as jest.Mock).mockResolvedValue(null); + (getRuleByRuleId as jest.Mock).mockResolvedValue(null); await expect(detectionRulesClient.upgradePrebuiltRule({ ruleAsset })).rejects.toThrow( `Failed to find rule ${ruleAsset.rule_id}` ); @@ -80,28 +82,24 @@ describe('DetectionRulesClient.upgradePrebuiltRule', () => { rule_id: 'rule-id', }; // Installed version is "query" - const installedRule = getRuleMock({ - ...getQueryRuleParams({ - exceptionsList: [ - { id: 'test_id', list_id: 'hi', type: 'detection', namespace_type: 'agnostic' }, - ], - }), - actions: [ - { - group: 'default', - id: 'test_id', - action_type_id: '.index', - config: { - index: ['index-1', 'index-2'], - }, - }, - ], - ruleId: 'rule-id', - }); + const installedRule = getRulesSchemaMock(); + installedRule.exceptions_list = [ + { id: 'test_id', list_id: 'hi', type: 'detection', namespace_type: 'agnostic' }, + ]; + installedRule.actions = [ + { + group: 'default', + id: 'test_id', + action_type_id: '.index', + params: {}, + }, + ]; + installedRule.rule_id = 'rule-id'; + beforeEach(() => { jest.resetAllMocks(); rulesClient.create.mockResolvedValue(getRuleMock(getQueryRuleParams())); - (readRules as jest.Mock).mockResolvedValue(installedRule); + (getRuleByRuleId as jest.Mock).mockResolvedValue(installedRule); }); it('deletes the old rule', async () => { @@ -117,16 +115,23 @@ describe('DetectionRulesClient.upgradePrebuiltRule', () => { name: ruleAsset.name, tags: ruleAsset.tags, // enabled and actions are kept from original rule - actions: installedRule.actions, + actions: [ + expect.objectContaining({ + actionTypeId: '.index', + group: 'default', + id: 'test_id', + params: {}, + }), + ], enabled: installedRule.enabled, params: expect.objectContaining({ index: ruleAsset.index, description: ruleAsset.description, immutable: true, // exceptions_lists, actions, timeline_id and timeline_title are maintained - timelineTitle: installedRule.params.timelineTitle, - timelineId: installedRule.params.timelineId, - exceptionsList: installedRule.params.exceptionsList, + timelineTitle: installedRule.timeline_title, + timelineId: installedRule.timeline_id, + exceptionsList: installedRule.exceptions_list, }), }), options: { @@ -147,11 +152,9 @@ describe('DetectionRulesClient.upgradePrebuiltRule', () => { rule_id: 'rule-id', }; // Installed version is "eql" - const installedRule = getRuleMock({ - ...getEqlRuleParams(), - }); + const installedRule = getRulesEqlSchemaMock(); beforeEach(() => { - (readRules as jest.Mock).mockResolvedValue(installedRule); + (getRuleByRuleId as jest.Mock).mockResolvedValue(installedRule); }); it('patches the existing rule with the new params from the rule asset', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client_interface.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client_interface.ts index 34c39153206b1..d7b45f83e8bf8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client_interface.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client_interface.ts @@ -38,7 +38,7 @@ export interface UpdateRuleArgs { } export interface PatchRuleArgs { - nextParams: RulePatchProps; + rulePatch: RulePatchProps; } export interface DeleteRuleArgs { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_defaults.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_defaults.ts new file mode 100644 index 0000000000000..837df0b3b2f1f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_defaults.ts @@ -0,0 +1,185 @@ +/* + * 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 { v4 as uuidv4 } from 'uuid'; +import type { + RuleCreateProps, + RuleSource, + TypeSpecificCreateProps, +} from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import { + DEFAULT_INDICATOR_SOURCE_PATH, + DEFAULT_MAX_SIGNALS, +} from '../../../../../../../common/constants'; +import { + normalizeMachineLearningJobIds, + normalizeThresholdObject, +} from '../../../../../../../common/detection_engine/utils'; +import { assertUnreachable } from '../../../../../../../common/utility_types'; +import { addEcsToRequiredFields } from '../../../utils/utils'; + +export const RULE_DEFAULTS = { + enabled: false, + risk_score_mapping: [], + severity_mapping: [], + interval: '5m' as const, + to: 'now' as const, + from: 'now-6m' as const, + exceptions_list: [], + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + actions: [], + related_integrations: [], + required_fields: [], + setup: '', + references: [], + threat: [], + tags: [], + author: [], + output_index: '', + version: 1, +}; + +export function applyRuleDefaults(rule: RuleCreateProps & { immutable?: boolean }) { + const typeSpecificParams = setTypeSpecificDefaults(rule); + const immutable = rule.immutable ?? false; + + return { + ...RULE_DEFAULTS, + ...rule, + ...typeSpecificParams, + rule_id: rule.rule_id ?? uuidv4(), + immutable, + rule_source: convertImmutableToRuleSource(immutable), + required_fields: addEcsToRequiredFields(rule.required_fields), + }; +} + +const convertImmutableToRuleSource = (immutable: boolean): RuleSource => { + if (immutable) { + return { + type: 'external', + is_customized: false, + }; + } + + return { + type: 'internal', + }; +}; + +export const setTypeSpecificDefaults = (props: TypeSpecificCreateProps) => { + switch (props.type) { + case 'eql': { + return { + type: props.type, + language: props.language, + index: props.index, + data_view_id: props.data_view_id, + query: props.query, + filters: props.filters, + timestamp_field: props.timestamp_field, + event_category_override: props.event_category_override, + tiebreaker_field: props.tiebreaker_field, + alert_suppression: props.alert_suppression, + }; + } + case 'esql': { + return { + type: props.type, + language: props.language, + query: props.query, + alert_suppression: props.alert_suppression, + }; + } + case 'threat_match': { + return { + type: props.type, + language: props.language ?? 'kuery', + index: props.index, + data_view_id: props.data_view_id, + query: props.query, + filters: props.filters, + saved_id: props.saved_id, + threat_filters: props.threat_filters, + threat_query: props.threat_query, + threat_mapping: props.threat_mapping, + threat_language: props.threat_language, + threat_index: props.threat_index, + threat_indicator_path: props.threat_indicator_path ?? DEFAULT_INDICATOR_SOURCE_PATH, + concurrent_searches: props.concurrent_searches, + items_per_search: props.items_per_search, + alert_suppression: props.alert_suppression, + }; + } + case 'query': { + return { + type: props.type, + language: props.language ?? 'kuery', + index: props.index, + data_view_id: props.data_view_id, + query: props.query ?? '', + filters: props.filters, + saved_id: props.saved_id, + response_actions: props.response_actions, + alert_suppression: props.alert_suppression, + }; + } + case 'saved_query': { + return { + type: props.type, + language: props.language ?? 'kuery', + index: props.index, + query: props.query, + filters: props.filters, + saved_id: props.saved_id, + data_view_id: props.data_view_id, + response_actions: props.response_actions, + alert_suppression: props.alert_suppression, + }; + } + case 'threshold': { + return { + type: props.type, + language: props.language ?? 'kuery', + index: props.index, + data_view_id: props.data_view_id, + query: props.query, + filters: props.filters, + saved_id: props.saved_id, + threshold: normalizeThresholdObject(props.threshold), + alert_suppression: props.alert_suppression?.duration + ? { duration: props.alert_suppression.duration } + : undefined, + }; + } + case 'machine_learning': { + return { + type: props.type, + anomaly_threshold: props.anomaly_threshold, + machine_learning_job_id: normalizeMachineLearningJobIds(props.machine_learning_job_id), + alert_suppression: props.alert_suppression, + }; + } + case 'new_terms': { + return { + type: props.type, + query: props.query, + new_terms_fields: props.new_terms_fields, + history_window_start: props.history_window_start, + index: props.index, + filters: props.filters, + language: props.language ?? 'kuery', + data_view_id: props.data_view_id, + alert_suppression: props.alert_suppression, + }; + } + default: { + return assertUnreachable(props); + } + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.test.ts new file mode 100644 index 0000000000000..49592aff28f95 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.test.ts @@ -0,0 +1,456 @@ +/* + * 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 { + AlertSuppressionDuration, + PatchRuleRequestBody, +} from '../../../../../../../common/api/detection_engine'; +import { + getEsqlRuleSchemaMock, + getRulesEqlSchemaMock, + getRulesMlSchemaMock, + getRulesNewTermsSchemaMock, + getRulesSchemaMock, + getRulesThresholdSchemaMock, + getSavedQuerySchemaMock, + getThreatMatchingSchemaMock, +} from '../../../../../../../common/api/detection_engine/model/rule_schema/mocks'; +import { createPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic/rule_assets/__mocks__/prebuilt_rule_assets_client'; +import { applyRulePatch } from './apply_rule_patch'; + +const prebuiltRuleAssetClient = createPrebuiltRuleAssetsClient(); + +describe('applyRulePatch', () => { + describe('EQL', () => { + test('should accept EQL params when existing rule type is EQL', async () => { + const rulePatch = { + timestamp_field: 'event.created', + event_category_override: 'event.not_category', + tiebreaker_field: 'event.created', + }; + const existingRule = getRulesEqlSchemaMock(); + const patchedRule = await applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }); + expect(patchedRule).toEqual( + expect.objectContaining({ + timestamp_field: 'event.created', + event_category_override: 'event.not_category', + tiebreaker_field: 'event.created', + }) + ); + }); + test('should accept EQL params with suppression in snake case and convert to camel case when rule type is EQL', async () => { + const rulePatch = { + timestamp_field: 'event.created', + event_category_override: 'event.not_category', + tiebreaker_field: 'event.created', + alert_suppression: { + group_by: ['event.type'], + duration: { + value: 10, + unit: 'm', + } as AlertSuppressionDuration, + missing_fields_strategy: 'suppress', + }, + }; + const existingRule = getRulesEqlSchemaMock(); + const patchedRule = await applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }); + expect(patchedRule).toEqual( + expect.objectContaining({ + timestamp_field: 'event.created', + event_category_override: 'event.not_category', + tiebreaker_field: 'event.created', + alert_suppression: { + group_by: ['event.type'], + duration: { + value: 10, + unit: 'm', + }, + missing_fields_strategy: 'suppress', + }, + }) + ); + }); + test('should reject invalid EQL params when existing rule type is EQL', async () => { + const rulePatch = { + timestamp_field: 1, + event_category_override: 1, + tiebreaker_field: 1, + } as PatchRuleRequestBody; + const existingRule = getRulesEqlSchemaMock(); + await expect( + applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }) + ).rejects.toThrowError( + 'event_category_override: Expected string, received number, tiebreaker_field: Expected string, received number, timestamp_field: Expected string, received number' + ); + }); + test('should reject EQL params with invalid suppression group_by field', async () => { + const rulePatch = { + timestamp_field: 'event.created', + event_category_override: 'event.not_category', + tiebreaker_field: 'event.created', + alert_suppression: { + group_by: 'event.type', + duration: { + value: 10, + unit: 'm', + } as AlertSuppressionDuration, + missing_fields_strategy: 'suppress', + }, + }; + const existingRule = getRulesEqlSchemaMock(); + await expect( + applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }) + ).rejects.toThrowError('alert_suppression.group_by: Expected array, received string'); + }); + }); + + test('should accept threat match params when existing rule type is threat match', async () => { + const rulePatch = { + threat_indicator_path: 'my.indicator', + threat_query: 'test-query', + }; + const existingRule = getThreatMatchingSchemaMock(); + const patchedRule = await applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }); + expect(patchedRule).toEqual( + expect.objectContaining({ + threat_indicator_path: 'my.indicator', + threat_query: 'test-query', + }) + ); + }); + + test('should reject invalid threat match params when existing rule type is threat match', async () => { + const rulePatch = { + threat_indicator_path: 1, + threat_query: 1, + } as PatchRuleRequestBody; + const existingRule = getThreatMatchingSchemaMock(); + await expect( + applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }) + ).rejects.toThrowError( + 'threat_query: Expected string, received number, threat_indicator_path: Expected string, received number' + ); + }); + + test('should accept query params when existing rule type is query', async () => { + const rulePatch = { + index: ['new-test-index'], + language: 'lucene', + } as PatchRuleRequestBody; + const existingRule = getRulesSchemaMock(); + const patchedRule = await applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }); + expect(patchedRule).toEqual( + expect.objectContaining({ + index: ['new-test-index'], + language: 'lucene', + }) + ); + }); + + test('should reject invalid query params when existing rule type is query', async () => { + const rulePatch = { + index: [1], + language: 'non-language', + } as PatchRuleRequestBody; + const existingRule = getRulesSchemaMock(); + await expect( + applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }) + ).rejects.toThrowError( + "index.0: Expected string, received number, language: Invalid enum value. Expected 'kuery' | 'lucene', received 'non-language'" + ); + }); + + test('should accept saved query params when existing rule type is saved query', async () => { + const rulePatch = { + index: ['new-test-index'], + language: 'lucene', + } as PatchRuleRequestBody; + const existingRule = getSavedQuerySchemaMock(); + const patchedRule = await applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }); + expect(patchedRule).toEqual( + expect.objectContaining({ + index: ['new-test-index'], + language: 'lucene', + }) + ); + }); + + test('should reject invalid saved query params when existing rule type is saved query', async () => { + const rulePatch = { + index: [1], + language: 'non-language', + } as PatchRuleRequestBody; + const existingRule = getSavedQuerySchemaMock(); + await expect( + applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }) + ).rejects.toThrowError( + "index.0: Expected string, received number, language: Invalid enum value. Expected 'kuery' | 'lucene', received 'non-language'" + ); + }); + + test('should accept threshold params when existing rule type is threshold', async () => { + const rulePatch = { + threshold: { + field: ['host.name'], + value: 107, + }, + }; + const existingRule = getRulesThresholdSchemaMock(); + const patchedRule = await applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }); + expect(patchedRule).toEqual( + expect.objectContaining({ + threshold: { + field: ['host.name'], + value: 107, + }, + }) + ); + }); + + test('should reject invalid threshold params when existing rule type is threshold', async () => { + const rulePatch = { + threshold: { + field: ['host.name'], + value: 'invalid', + }, + } as PatchRuleRequestBody; + const existingRule = getRulesThresholdSchemaMock(); + await expect( + applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }) + ).rejects.toThrowError('threshold.value: Expected number, received string'); + }); + + test('should accept ES|QL alerts suppression params', async () => { + const rulePatch = { + alert_suppression: { + group_by: ['agent.name'], + duration: { value: 4, unit: 'h' as const }, + missing_fields_strategy: 'doNotSuppress' as const, + }, + }; + const existingRule = getEsqlRuleSchemaMock(); + const patchedRule = await applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }); + expect(patchedRule).toEqual( + expect.objectContaining({ + alert_suppression: { + group_by: ['agent.name'], + missing_fields_strategy: 'doNotSuppress', + duration: { value: 4, unit: 'h' }, + }, + }) + ); + }); + + test('should accept threshold alerts suppression params', async () => { + const rulePatch = { + alert_suppression: { + duration: { value: 4, unit: 'h' as const }, + }, + }; + const existingRule = getRulesThresholdSchemaMock(); + const patchedRule = await applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }); + expect(patchedRule).toEqual( + expect.objectContaining({ + alert_suppression: { + duration: { value: 4, unit: 'h' }, + }, + }) + ); + }); + + test('should accept threat_match alerts suppression params', async () => { + const rulePatch = { + alert_suppression: { + group_by: ['agent.name'], + missing_fields_strategy: 'suppress' as const, + }, + }; + const existingRule = getThreatMatchingSchemaMock(); + const patchedRule = await applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }); + expect(patchedRule).toEqual( + expect.objectContaining({ + alert_suppression: { + group_by: ['agent.name'], + missing_fields_strategy: 'suppress', + }, + }) + ); + }); + + test('should accept new_terms alerts suppression params', async () => { + const rulePatch = { + alert_suppression: { + group_by: ['agent.name'], + duration: { value: 4, unit: 'h' as const }, + missing_fields_strategy: 'suppress' as const, + }, + }; + const existingRule = getRulesNewTermsSchemaMock(); + const patchedRule = await applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }); + expect(patchedRule).toEqual( + expect.objectContaining({ + alert_suppression: { + group_by: ['agent.name'], + missing_fields_strategy: 'suppress', + duration: { value: 4, unit: 'h' }, + }, + }) + ); + }); + + describe('machine learning rules', () => { + test('should accept machine learning params when existing rule type is machine learning', async () => { + const rulePatch = { + anomaly_threshold: 5, + }; + const existingRule = getRulesMlSchemaMock(); + const patchedRule = await applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }); + expect(patchedRule).toEqual( + expect.objectContaining({ + anomaly_threshold: 5, + }) + ); + }); + + test('should reject invalid machine learning params when existing rule type is machine learning', async () => { + const rulePatch = { + anomaly_threshold: 'invalid', + } as PatchRuleRequestBody; + const existingRule = getRulesMlSchemaMock(); + await expect( + applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }) + ).rejects.toThrowError('anomaly_threshold: Expected number, received string'); + }); + + it('accepts suppression params', async () => { + const rulePatch = { + alert_suppression: { + group_by: ['agent.name'], + missing_fields_strategy: 'suppress' as const, + }, + }; + const existingRule = getRulesMlSchemaMock(); + const patchedRule = await applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }); + + expect(patchedRule).toEqual( + expect.objectContaining({ + alert_suppression: { + group_by: ['agent.name'], + missing_fields_strategy: 'suppress', + }, + }) + ); + }); + }); + + test('should accept new terms params when existing rule type is new terms', async () => { + const rulePatch = { + new_terms_fields: ['event.new_field'], + }; + const existingRule = getRulesNewTermsSchemaMock(); + const patchedRule = await applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }); + expect(patchedRule).toEqual( + expect.objectContaining({ + new_terms_fields: ['event.new_field'], + }) + ); + }); + + test('should reject invalid new terms params when existing rule type is new terms', async () => { + const rulePatch = { + new_terms_fields: 'invalid', + } as PatchRuleRequestBody; + const existingRule = getRulesNewTermsSchemaMock(); + await expect( + applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }) + ).rejects.toThrowError('new_terms_fields: Expected array, received string'); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.ts new file mode 100644 index 0000000000000..9d02cd8dbb9df --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.ts @@ -0,0 +1,334 @@ +/* + * 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 { BadRequestError } from '@kbn/securitysolution-es-utils'; +import { stringifyZodError } from '@kbn/zod-helpers'; +import type { + EqlRule, + EqlRuleResponseFields, + EsqlRule, + EsqlRuleResponseFields, + MachineLearningRule, + MachineLearningRuleResponseFields, + NewTermsRule, + NewTermsRuleResponseFields, + QueryRule, + QueryRuleResponseFields, + RuleResponse, + SavedQueryRule, + SavedQueryRuleResponseFields, + ThreatMatchRule, + ThreatMatchRuleResponseFields, + ThresholdRule, + ThresholdRuleResponseFields, + TypeSpecificResponse, +} from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import { + EqlRulePatchFields, + EsqlRulePatchFields, + MachineLearningRulePatchFields, + NewTermsRulePatchFields, + QueryRulePatchFields, + SavedQueryRulePatchFields, + ThreatMatchRulePatchFields, + ThresholdRulePatchFields, +} from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import type { PatchRuleRequestBody } from '../../../../../../../common/api/detection_engine/rule_management'; +import { + normalizeMachineLearningJobIds, + normalizeThresholdObject, +} from '../../../../../../../common/detection_engine/utils'; +import { assertUnreachable } from '../../../../../../../common/utility_types'; +import type { IPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client'; +import { addEcsToRequiredFields } from '../../../utils/utils'; +import { calculateRuleSource } from './rule_source/calculate_rule_source'; + +interface ApplyRulePatchProps { + prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient; + existingRule: RuleResponse; + rulePatch: PatchRuleRequestBody; +} + +// eslint-disable-next-line complexity +export const applyRulePatch = async ({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, +}: ApplyRulePatchProps): Promise => { + const typeSpecificParams = patchTypeSpecificParams(rulePatch, existingRule); + + const nextRule: RuleResponse = { + // Keep existing values for these fields + id: existingRule.id, + rule_id: existingRule.rule_id, + revision: existingRule.revision, + immutable: existingRule.immutable, + rule_source: existingRule.rule_source, + updated_at: existingRule.updated_at, + updated_by: existingRule.updated_by, + created_at: existingRule.created_at, + created_by: existingRule.created_by, + + // Update values for these fields + enabled: rulePatch.enabled ?? existingRule.enabled, + name: rulePatch.name ?? existingRule.name, + tags: rulePatch.tags ?? existingRule.tags, + author: rulePatch.author ?? existingRule.author, + building_block_type: rulePatch.building_block_type ?? existingRule.building_block_type, + description: rulePatch.description ?? existingRule.description, + false_positives: rulePatch.false_positives ?? existingRule.false_positives, + investigation_fields: rulePatch.investigation_fields ?? existingRule.investigation_fields, + from: rulePatch.from ?? existingRule.from, + license: rulePatch.license ?? existingRule.license, + output_index: rulePatch.output_index ?? existingRule.output_index, + timeline_id: rulePatch.timeline_id ?? existingRule.timeline_id, + timeline_title: rulePatch.timeline_title ?? existingRule.timeline_title, + meta: rulePatch.meta ?? existingRule.meta, + max_signals: rulePatch.max_signals ?? existingRule.max_signals, + related_integrations: rulePatch.related_integrations ?? existingRule.related_integrations, + required_fields: addEcsToRequiredFields(rulePatch.required_fields), + risk_score: rulePatch.risk_score ?? existingRule.risk_score, + risk_score_mapping: rulePatch.risk_score_mapping ?? existingRule.risk_score_mapping, + rule_name_override: rulePatch.rule_name_override ?? existingRule.rule_name_override, + setup: rulePatch.setup ?? existingRule.setup, + severity: rulePatch.severity ?? existingRule.severity, + severity_mapping: rulePatch.severity_mapping ?? existingRule.severity_mapping, + threat: rulePatch.threat ?? existingRule.threat, + timestamp_override: rulePatch.timestamp_override ?? existingRule.timestamp_override, + timestamp_override_fallback_disabled: + rulePatch.timestamp_override_fallback_disabled ?? + existingRule.timestamp_override_fallback_disabled, + to: rulePatch.to ?? existingRule.to, + references: rulePatch.references ?? existingRule.references, + namespace: rulePatch.namespace ?? existingRule.namespace, + note: rulePatch.note ?? existingRule.note, + version: rulePatch.version ?? existingRule.version, + exceptions_list: rulePatch.exceptions_list ?? existingRule.exceptions_list, + interval: rulePatch.interval ?? existingRule.interval, + throttle: rulePatch.throttle ?? existingRule.throttle, + actions: rulePatch.actions ?? existingRule.actions, + ...typeSpecificParams, + }; + + nextRule.rule_source = await calculateRuleSource({ + rule: nextRule, + prebuiltRuleAssetClient, + }); + + return nextRule; +}; + +const patchEqlParams = ( + rulePatch: EqlRulePatchFields, + existingRule: EqlRule +): EqlRuleResponseFields => { + return { + type: existingRule.type, + language: rulePatch.language ?? existingRule.language, + index: rulePatch.index ?? existingRule.index, + data_view_id: rulePatch.data_view_id ?? existingRule.data_view_id, + query: rulePatch.query ?? existingRule.query, + filters: rulePatch.filters ?? existingRule.filters, + timestamp_field: rulePatch.timestamp_field ?? existingRule.timestamp_field, + event_category_override: + rulePatch.event_category_override ?? existingRule.event_category_override, + tiebreaker_field: rulePatch.tiebreaker_field ?? existingRule.tiebreaker_field, + alert_suppression: rulePatch.alert_suppression ?? existingRule.alert_suppression, + }; +}; + +const patchEsqlParams = ( + rulePatch: EsqlRulePatchFields, + existingRule: EsqlRule +): EsqlRuleResponseFields => { + return { + type: existingRule.type, + language: rulePatch.language ?? existingRule.language, + query: rulePatch.query ?? existingRule.query, + alert_suppression: rulePatch.alert_suppression ?? existingRule.alert_suppression, + }; +}; + +const patchThreatMatchParams = ( + rulePatch: ThreatMatchRulePatchFields, + existingRule: ThreatMatchRule +): ThreatMatchRuleResponseFields => { + return { + type: existingRule.type, + language: rulePatch.language ?? existingRule.language, + index: rulePatch.index ?? existingRule.index, + data_view_id: rulePatch.data_view_id ?? existingRule.data_view_id, + query: rulePatch.query ?? existingRule.query, + filters: rulePatch.filters ?? existingRule.filters, + saved_id: rulePatch.saved_id ?? existingRule.saved_id, + threat_filters: rulePatch.threat_filters ?? existingRule.threat_filters, + threat_query: rulePatch.threat_query ?? existingRule.threat_query, + threat_mapping: rulePatch.threat_mapping ?? existingRule.threat_mapping, + threat_language: rulePatch.threat_language ?? existingRule.threat_language, + threat_index: rulePatch.threat_index ?? existingRule.threat_index, + threat_indicator_path: rulePatch.threat_indicator_path ?? existingRule.threat_indicator_path, + concurrent_searches: rulePatch.concurrent_searches ?? existingRule.concurrent_searches, + items_per_search: rulePatch.items_per_search ?? existingRule.items_per_search, + alert_suppression: rulePatch.alert_suppression ?? existingRule.alert_suppression, + }; +}; + +const patchQueryParams = ( + rulePatch: QueryRulePatchFields, + existingRule: QueryRule +): QueryRuleResponseFields => { + return { + type: existingRule.type, + language: rulePatch.language ?? existingRule.language, + index: rulePatch.index ?? existingRule.index, + data_view_id: rulePatch.data_view_id ?? existingRule.data_view_id, + query: rulePatch.query ?? existingRule.query, + filters: rulePatch.filters ?? existingRule.filters, + saved_id: rulePatch.saved_id ?? existingRule.saved_id, + response_actions: rulePatch.response_actions ?? existingRule.response_actions, + alert_suppression: rulePatch.alert_suppression ?? existingRule.alert_suppression, + }; +}; + +const patchSavedQueryParams = ( + rulePatch: SavedQueryRulePatchFields, + existingRule: SavedQueryRule +): SavedQueryRuleResponseFields => { + return { + type: existingRule.type, + language: rulePatch.language ?? existingRule.language, + index: rulePatch.index ?? existingRule.index, + data_view_id: rulePatch.data_view_id ?? existingRule.data_view_id, + query: rulePatch.query ?? existingRule.query, + filters: rulePatch.filters ?? existingRule.filters, + saved_id: rulePatch.saved_id ?? existingRule.saved_id, + response_actions: rulePatch.response_actions ?? existingRule.response_actions, + alert_suppression: rulePatch.alert_suppression ?? existingRule.alert_suppression, + }; +}; + +const patchThresholdParams = ( + rulePatch: ThresholdRulePatchFields, + existingRule: ThresholdRule +): ThresholdRuleResponseFields => { + return { + type: existingRule.type, + language: rulePatch.language ?? existingRule.language, + index: rulePatch.index ?? existingRule.index, + data_view_id: rulePatch.data_view_id ?? existingRule.data_view_id, + query: rulePatch.query ?? existingRule.query, + filters: rulePatch.filters ?? existingRule.filters, + saved_id: rulePatch.saved_id ?? existingRule.saved_id, + threshold: rulePatch.threshold + ? normalizeThresholdObject(rulePatch.threshold) + : existingRule.threshold, + alert_suppression: rulePatch.alert_suppression ?? existingRule.alert_suppression, + }; +}; + +const patchMachineLearningParams = ( + params: MachineLearningRulePatchFields, + existingRule: MachineLearningRule +): MachineLearningRuleResponseFields => { + return { + type: existingRule.type, + anomaly_threshold: params.anomaly_threshold ?? existingRule.anomaly_threshold, + machine_learning_job_id: params.machine_learning_job_id + ? normalizeMachineLearningJobIds(params.machine_learning_job_id) + : existingRule.machine_learning_job_id, + alert_suppression: params.alert_suppression ?? existingRule.alert_suppression, + }; +}; + +const patchNewTermsParams = ( + params: NewTermsRulePatchFields, + existingRule: NewTermsRule +): NewTermsRuleResponseFields => { + return { + type: existingRule.type, + language: params.language ?? existingRule.language, + index: params.index ?? existingRule.index, + data_view_id: params.data_view_id ?? existingRule.data_view_id, + query: params.query ?? existingRule.query, + filters: params.filters ?? existingRule.filters, + new_terms_fields: params.new_terms_fields ?? existingRule.new_terms_fields, + history_window_start: params.history_window_start ?? existingRule.history_window_start, + alert_suppression: params.alert_suppression ?? existingRule.alert_suppression, + }; +}; + +export const patchTypeSpecificParams = ( + params: PatchRuleRequestBody, + existingRule: RuleResponse +): TypeSpecificResponse => { + // Here we do the validation of patch params by rule type to ensure that the fields that are + // passed in to patch are of the correct type, e.g. `query` is a string. Since the combined patch schema + // is a union of types where everything is optional, it's hard to do the validation before we know the rule type - + // a patch request that defines `event_category_override` as a number would not be assignable to the EQL patch schema, + // but would be assignable to the other rule types since they don't specify `event_category_override`. + switch (existingRule.type) { + case 'eql': { + const result = EqlRulePatchFields.safeParse(params); + if (!result.success) { + throw new BadRequestError(stringifyZodError(result.error)); + } + return patchEqlParams(result.data, existingRule); + } + case 'esql': { + const result = EsqlRulePatchFields.safeParse(params); + if (!result.success) { + throw new BadRequestError(stringifyZodError(result.error)); + } + return patchEsqlParams(result.data, existingRule); + } + case 'threat_match': { + const result = ThreatMatchRulePatchFields.safeParse(params); + if (!result.success) { + throw new BadRequestError(stringifyZodError(result.error)); + } + return patchThreatMatchParams(result.data, existingRule); + } + case 'query': { + const result = QueryRulePatchFields.safeParse(params); + if (!result.success) { + throw new BadRequestError(stringifyZodError(result.error)); + } + return patchQueryParams(result.data, existingRule); + } + case 'saved_query': { + const result = SavedQueryRulePatchFields.safeParse(params); + if (!result.success) { + throw new BadRequestError(stringifyZodError(result.error)); + } + return patchSavedQueryParams(result.data, existingRule); + } + case 'threshold': { + const result = ThresholdRulePatchFields.safeParse(params); + if (!result.success) { + throw new BadRequestError(stringifyZodError(result.error)); + } + return patchThresholdParams(result.data, existingRule); + } + case 'machine_learning': { + const result = MachineLearningRulePatchFields.safeParse(params); + if (!result.success) { + throw new BadRequestError(stringifyZodError(result.error)); + } + return patchMachineLearningParams(result.data, existingRule); + } + case 'new_terms': { + const result = NewTermsRulePatchFields.safeParse(params); + if (!result.success) { + throw new BadRequestError(stringifyZodError(result.error)); + } + return patchNewTermsParams(result.data, existingRule); + } + default: { + return assertUnreachable(existingRule); + } + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_update.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_update.ts new file mode 100644 index 0000000000000..b911e66a1fc45 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_update.ts @@ -0,0 +1,52 @@ +/* + * 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 { + RuleResponse, + RuleUpdateProps, +} from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import type { IPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client'; +import { applyRuleDefaults } from './apply_rule_defaults'; +import { calculateRuleSource } from './rule_source/calculate_rule_source'; + +interface ApplyRuleUpdateProps { + prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient; + existingRule: RuleResponse; + ruleUpdate: RuleUpdateProps; +} + +export const applyRuleUpdate = async ({ + prebuiltRuleAssetClient, + existingRule, + ruleUpdate, +}: ApplyRuleUpdateProps): Promise => { + const nextRule: RuleResponse = { + ...applyRuleDefaults(ruleUpdate), + + // Use existing values + enabled: ruleUpdate.enabled ?? existingRule.enabled, + version: ruleUpdate.version ?? existingRule.version, + + // Always keep existing values for these fields + id: existingRule.id, + rule_id: existingRule.rule_id, + revision: existingRule.revision, + immutable: existingRule.immutable, + rule_source: existingRule.rule_source, + updated_at: existingRule.updated_at, + updated_by: existingRule.updated_by, + created_at: existingRule.created_at, + created_by: existingRule.created_by, + }; + + nextRule.rule_source = await calculateRuleSource({ + rule: nextRule, + prebuiltRuleAssetClient, + }); + + return nextRule; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_is_customized.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_is_customized.ts new file mode 100644 index 0000000000000..4f9bb4a060f6f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_is_customized.ts @@ -0,0 +1,33 @@ +/* + * 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 { RuleResponse } from '../../../../../../../../common/api/detection_engine'; +import { MissingVersion } from '../../../../../../../../common/api/detection_engine'; +import type { PrebuiltRuleAsset } from '../../../../../prebuilt_rules'; +import { calculateRuleFieldsDiff } from '../../../../../prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff'; +import { convertRuleToDiffable } from '../../../../../prebuilt_rules/logic/diff/normalization/convert_rule_to_diffable'; +import { convertPrebuiltRuleAssetToRuleResponse } from '../../converters/convert_prebuilt_rule_asset_to_rule_response'; + +export function calculateIsCustomized( + baseRule: PrebuiltRuleAsset | undefined, + nextRule: RuleResponse +) { + if (baseRule == null) { + // If the base version is missing, we consider the rule to be customized + return true; + } + + const baseRuleWithDefaults = convertPrebuiltRuleAssetToRuleResponse(baseRule); + + const fieldsDiff = calculateRuleFieldsDiff({ + base_version: MissingVersion, + current_version: convertRuleToDiffable(baseRuleWithDefaults), + target_version: convertRuleToDiffable(nextRule), + }); + + return Object.values(fieldsDiff).some((diff) => diff.has_update); +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.test.ts new file mode 100644 index 0000000000000..e44c69d2705d5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.test.ts @@ -0,0 +1,112 @@ +/* + * 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 { createPrebuiltRuleAssetsClient } from '../../../../../prebuilt_rules/logic/rule_assets/__mocks__/prebuilt_rule_assets_client'; +import { applyRuleDefaults } from '../apply_rule_defaults'; +import { calculateRuleSource } from './calculate_rule_source'; + +const prebuiltRuleAssetClient = createPrebuiltRuleAssetsClient(); + +const getSampleRuleAsset = () => { + return applyRuleDefaults({ + rule_id: 'test-rule-id', + name: 'Test rule', + description: 'Test description', + type: 'query', + query: 'user.name: root or user.name: admin', + severity: 'high', + risk_score: 55, + }); +}; + +const getSampleRule = () => { + return { + ...getSampleRuleAsset(), + id: 'test-rule-id', + updated_at: '2021-01-01T00:00:00Z', + updated_by: 'test-user', + created_at: '2021-01-01T00:00:00Z', + created_by: 'test-user', + revision: 1, + }; +}; + +describe('calculateRuleSource', () => { + it('returns an internal rule source when the rule is not prebuilt', async () => { + const rule = getSampleRule(); + rule.immutable = false; + + const result = await calculateRuleSource({ + prebuiltRuleAssetClient, + rule, + }); + expect(result).toEqual({ + type: 'internal', + }); + }); + + it('returns an external rule source with customized false when the rule is prebuilt', async () => { + const rule = getSampleRule(); + rule.immutable = true; + + const baseRule = getSampleRuleAsset(); + prebuiltRuleAssetClient.fetchAssetsByVersion.mockResolvedValueOnce([baseRule]); + + const result = await calculateRuleSource({ + prebuiltRuleAssetClient, + rule, + }); + expect(result).toEqual( + expect.objectContaining({ + type: 'external', + is_customized: false, + }) + ); + }); + + it('returns is_customized true when the rule is prebuilt and has been customized', async () => { + const rule = getSampleRule(); + rule.immutable = true; + rule.name = 'Updated name'; + + const baseRule = getSampleRuleAsset(); + prebuiltRuleAssetClient.fetchAssetsByVersion.mockResolvedValueOnce([baseRule]); + + const result = await calculateRuleSource({ + prebuiltRuleAssetClient, + rule, + }); + expect(result).toEqual( + expect.objectContaining({ + type: 'external', + is_customized: true, + }) + ); + }); + + it('returns is_customized false when the rule has only changes to revision, updated_at, updated_by', async () => { + const rule = getSampleRule(); + rule.immutable = true; + rule.revision = 5; + rule.updated_at = '2024-01-01T00:00:00Z'; + rule.updated_by = 'new-user'; + + const baseRule = getSampleRuleAsset(); + prebuiltRuleAssetClient.fetchAssetsByVersion.mockResolvedValueOnce([baseRule]); + + const result = await calculateRuleSource({ + prebuiltRuleAssetClient, + rule, + }); + expect(result).toEqual( + expect.objectContaining({ + type: 'external', + is_customized: false, + }) + ); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.ts new file mode 100644 index 0000000000000..742cd20544a60 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.ts @@ -0,0 +1,47 @@ +/* + * 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 { + RuleResponse, + RuleSource, +} from '../../../../../../../../common/api/detection_engine/model/rule_schema'; +import type { PrebuiltRuleAsset } from '../../../../../prebuilt_rules'; +import type { IPrebuiltRuleAssetsClient } from '../../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client'; +import { calculateIsCustomized } from './calculate_is_customized'; + +interface CalculateRuleSourceProps { + prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient; + rule: RuleResponse; +} + +export async function calculateRuleSource({ + prebuiltRuleAssetClient, + rule, +}: CalculateRuleSourceProps): Promise { + if (rule.immutable) { + // This is a prebuilt rule and, despite the name, they are not immutable. So + // we need to recalculate `ruleSource.isCustomized` based on the rule's contents. + const prebuiltRulesResponse = await prebuiltRuleAssetClient.fetchAssetsByVersion([ + { + rule_id: rule.rule_id, + version: rule.version, + }, + ]); + const baseRule: PrebuiltRuleAsset | undefined = prebuiltRulesResponse.at(0); + + const isCustomized = calculateIsCustomized(baseRule, rule); + + return { + type: 'external', + is_customized: isCustomized, + }; + } + + return { + type: 'internal', + }; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/__mocks__/get_rule_by_rule_id.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/__mocks__/get_rule_by_rule_id.ts new file mode 100644 index 0000000000000..251cd7f699195 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/__mocks__/get_rule_by_rule_id.ts @@ -0,0 +1,13 @@ +/* + * 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 { RuleResponse } from '../../../../../../../../common/api/detection_engine'; +import { getRulesSchemaMock } from '../../../../../../../../common/api/detection_engine/model/rule_schema/rule_response_schema.mock'; + +export const getRuleByRuleId = jest + .fn() + .mockImplementation(async (): Promise => getRulesSchemaMock()); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/create_custom_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/create_custom_rule.ts deleted file mode 100644 index 963cac7e10dd1..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/create_custom_rule.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { RulesClient } from '@kbn/alerting-plugin/server'; -import { stringifyZodError } from '@kbn/zod-helpers'; -import type { CreateCustomRuleArgs } from '../detection_rules_client_interface'; -import type { MlAuthz } from '../../../../../machine_learning/authz'; -import type { RuleParams } from '../../../../rule_schema'; -import { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; -import { - convertCreateAPIToInternalSchema, - internalRuleToAPIResponse, -} from '../../../normalization/rule_converters'; -import { validateMlAuth, RuleResponseValidationError } from '../utils'; - -export const createCustomRule = async ( - rulesClient: RulesClient, - args: CreateCustomRuleArgs, - mlAuthz: MlAuthz -): Promise => { - const { params } = args; - await validateMlAuth(mlAuthz, params.type); - - const internalRule = convertCreateAPIToInternalSchema(params, { immutable: false }); - const rule = await rulesClient.create({ - data: internalRule, - }); - - /* Trying to convert the rule to a RuleResponse object */ - const parseResult = RuleResponse.safeParse(internalRuleToAPIResponse(rule)); - - if (!parseResult.success) { - throw new RuleResponseValidationError({ - message: stringifyZodError(parseResult.error), - ruleId: rule.params.ruleId, - }); - } - - return parseResult.data; -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/create_prebuilt_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/create_prebuilt_rule.ts deleted file mode 100644 index 0f0a4aea12d7b..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/create_prebuilt_rule.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { RulesClient } from '@kbn/alerting-plugin/server'; -import { stringifyZodError } from '@kbn/zod-helpers'; -import type { CreatePrebuiltRuleArgs } from '../detection_rules_client_interface'; -import type { MlAuthz } from '../../../../../machine_learning/authz'; -import { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; -import type { RuleParams } from '../../../../rule_schema'; -import { - convertCreateAPIToInternalSchema, - internalRuleToAPIResponse, -} from '../../../normalization/rule_converters'; -import { validateMlAuth, RuleResponseValidationError } from '../utils'; - -export const createPrebuiltRule = async ( - rulesClient: RulesClient, - args: CreatePrebuiltRuleArgs, - mlAuthz: MlAuthz -): Promise => { - const { params } = args; - - await validateMlAuth(mlAuthz, params.type); - - const internalRule = convertCreateAPIToInternalSchema(params, { - immutable: true, - defaultEnabled: false, - }); - - const rule = await rulesClient.create({ - data: internalRule, - }); - - /* Trying to convert the rule to a RuleResponse object */ - const parseResult = RuleResponse.safeParse(internalRuleToAPIResponse(rule)); - - if (!parseResult.success) { - throw new RuleResponseValidationError({ - message: stringifyZodError(parseResult.error), - ruleId: rule.params.ruleId, - }); - } - - return parseResult.data; -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/create_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/create_rule.ts new file mode 100644 index 0000000000000..772e0c775d8b4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/create_rule.ts @@ -0,0 +1,55 @@ +/* + * 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 { RulesClient } from '@kbn/alerting-plugin/server'; +import { ruleTypeMappings } from '@kbn/securitysolution-rules'; +import { SERVER_APP_ID } from '../../../../../../../common'; +import type { + RuleCreateProps, + RuleResponse, +} from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import type { MlAuthz } from '../../../../../machine_learning/authz'; +import type { RuleParams } from '../../../../rule_schema'; +import { convertAlertingRuleToRuleResponse } from '../converters/convert_alerting_rule_to_rule_response'; +import { convertRuleResponseToAlertingRule } from '../converters/convert_rule_response_to_alerting_rule'; +import { applyRuleDefaults } from '../mergers/apply_rule_defaults'; +import { validateMlAuth } from '../utils'; + +interface CreateRuleOptions { + rulesClient: RulesClient; + mlAuthz: MlAuthz; + rule: RuleCreateProps & { immutable: boolean }; + id?: string; + allowMissingConnectorSecrets?: boolean; +} + +export const createRule = async ({ + rulesClient, + mlAuthz, + rule, + id, + allowMissingConnectorSecrets, +}: CreateRuleOptions): Promise => { + await validateMlAuth(mlAuthz, rule.type); + + const ruleWithDefaults = applyRuleDefaults(rule); + + const payload = { + ...convertRuleResponseToAlertingRule(ruleWithDefaults), + alertTypeId: ruleTypeMappings[rule.type], + consumer: SERVER_APP_ID, + enabled: rule.enabled ?? false, + }; + + const createdRule = await rulesClient.create({ + data: payload, + options: { id }, + allowMissingConnectorSecrets, + }); + + return convertAlertingRuleToRuleResponse(createdRule); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/delete_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/delete_rule.ts index ec1491e8159d7..4a9ca8abcdeb1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/delete_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/delete_rule.ts @@ -6,9 +6,13 @@ */ import type { RulesClient } from '@kbn/alerting-plugin/server'; -import type { DeleteRuleArgs } from '../detection_rules_client_interface'; +import type { RuleObjectId } from '../../../../../../../common/api/detection_engine'; -export const deleteRule = async (rulesClient: RulesClient, args: DeleteRuleArgs): Promise => { - const { ruleId } = args; +interface DeleteRuleParams { + rulesClient: RulesClient; + ruleId: RuleObjectId; +} + +export const deleteRule = async ({ rulesClient, ruleId }: DeleteRuleParams): Promise => { await rulesClient.delete({ id: ruleId }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/get_rule_by_id.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/get_rule_by_id.ts new file mode 100644 index 0000000000000..39ca15fda42f3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/get_rule_by_id.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 type { RulesClient } from '@kbn/alerting-plugin/server'; +import type { + RuleObjectId, + RuleResponse, +} from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import type { RuleParams } from '../../../../rule_schema'; +import { convertAlertingRuleToRuleResponse } from '../converters/convert_alerting_rule_to_rule_response'; + +interface GethRuleByIdOptions { + rulesClient: RulesClient; + id: RuleObjectId; +} + +export const getRuleById = async ({ + rulesClient, + id, +}: GethRuleByIdOptions): Promise => { + try { + const rule = await rulesClient.resolve({ id }); + return convertAlertingRuleToRuleResponse(rule); + } catch (err) { + if (err?.output?.statusCode === 404) { + return null; + } + throw err; + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/get_rule_by_id_or_rule_id.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/get_rule_by_id_or_rule_id.ts new file mode 100644 index 0000000000000..fce28d1a1c030 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/get_rule_by_id_or_rule_id.ts @@ -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 { RulesClient } from '@kbn/alerting-plugin/server'; +import type { + RuleObjectId, + RuleResponse, + RuleSignatureId, +} from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import { invariant } from '../../../../../../../common/utils/invariant'; +import { getRuleById } from './get_rule_by_id'; +import { getRuleByRuleId } from './get_rule_by_rule_id'; + +interface GetRuleByIdOptions { + rulesClient: RulesClient; + id: RuleObjectId | undefined; + ruleId: RuleSignatureId | undefined; +} + +export const getRuleByIdOrRuleId = async ({ + rulesClient, + id, + ruleId, +}: GetRuleByIdOptions): Promise => { + if (id != null) { + return getRuleById({ rulesClient, id }); + } + if (ruleId != null) { + return getRuleByRuleId({ rulesClient, ruleId }); + } + invariant(false, 'Either id or ruleId must be provided'); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/get_rule_by_rule_id.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/get_rule_by_rule_id.ts new file mode 100644 index 0000000000000..fda00cd292b88 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/get_rule_by_rule_id.ts @@ -0,0 +1,38 @@ +/* + * 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 { RulesClient } from '@kbn/alerting-plugin/server'; +import type { + RuleResponse, + RuleSignatureId, +} from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import { findRules } from '../../search/find_rules'; +import { convertAlertingRuleToRuleResponse } from '../converters/convert_alerting_rule_to_rule_response'; + +interface GetRuleByRuleIdOptions { + rulesClient: RulesClient; + ruleId: RuleSignatureId; +} + +export const getRuleByRuleId = async ({ + rulesClient, + ruleId, +}: GetRuleByRuleIdOptions): Promise => { + const findRuleResponse = await findRules({ + rulesClient, + filter: `alert.attributes.params.ruleId: "${ruleId}"`, + page: 1, + fields: undefined, + perPage: undefined, + sortField: undefined, + sortOrder: undefined, + }); + if (findRuleResponse.data.length === 0) { + return null; + } + return convertAlertingRuleToRuleResponse(findRuleResponse.data[0]); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/import_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/import_rule.ts index 55a0399f1a528..adb28133b62f3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/import_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/import_rule.ts @@ -6,78 +6,67 @@ */ import type { RulesClient } from '@kbn/alerting-plugin/server'; -import { stringifyZodError } from '@kbn/zod-helpers'; +import type { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; import type { MlAuthz } from '../../../../../machine_learning/authz'; -import type { ImportRuleArgs } from '../detection_rules_client_interface'; -import type { RuleAlertType, RuleParams } from '../../../../rule_schema'; +import type { IPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client'; import { createBulkErrorObject } from '../../../../routes/utils'; -import { - convertCreateAPIToInternalSchema, - convertUpdateAPIToInternalSchema, - internalRuleToAPIResponse, -} from '../../../normalization/rule_converters'; -import { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; - -import { validateMlAuth, RuleResponseValidationError } from '../utils'; +import { convertAlertingRuleToRuleResponse } from '../converters/convert_alerting_rule_to_rule_response'; +import { convertRuleResponseToAlertingRule } from '../converters/convert_rule_response_to_alerting_rule'; +import type { ImportRuleArgs } from '../detection_rules_client_interface'; +import { applyRuleUpdate } from '../mergers/apply_rule_update'; +import { validateMlAuth } from '../utils'; +import { createRule } from './create_rule'; +import { getRuleByRuleId } from './get_rule_by_rule_id'; -import { readRules } from '../read_rules'; +interface ImportRuleOptions { + rulesClient: RulesClient; + prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient; + importRulePayload: ImportRuleArgs; + mlAuthz: MlAuthz; +} -export const importRule = async ( - rulesClient: RulesClient, - importRulePayload: ImportRuleArgs, - mlAuthz: MlAuthz -): Promise => { +export const importRule = async ({ + rulesClient, + importRulePayload, + prebuiltRuleAssetClient, + mlAuthz, +}: ImportRuleOptions): Promise => { const { ruleToImport, overwriteRules, allowMissingConnectorSecrets } = importRulePayload; await validateMlAuth(mlAuthz, ruleToImport.type); - const existingRule = await readRules({ + const existingRule = await getRuleByRuleId({ rulesClient, ruleId: ruleToImport.rule_id, - id: undefined, }); if (existingRule && !overwriteRules) { throw createBulkErrorObject({ - ruleId: existingRule.params.ruleId, + ruleId: existingRule.rule_id, statusCode: 409, - message: `rule_id: "${existingRule.params.ruleId}" already exists`, + message: `rule_id: "${existingRule.rule_id}" already exists`, }); } - let importedInternalRule: RuleAlertType; - if (existingRule && overwriteRules) { - const ruleUpdateParams = convertUpdateAPIToInternalSchema({ + const ruleWithUpdates = await applyRuleUpdate({ + prebuiltRuleAssetClient, existingRule, ruleUpdate: ruleToImport, }); - importedInternalRule = await rulesClient.update({ + const updatedRule = await rulesClient.update({ id: existingRule.id, - data: ruleUpdateParams, - }); - } else { - /* Rule does not exist, so we'll create it */ - const ruleCreateParams = convertCreateAPIToInternalSchema(ruleToImport, { - immutable: false, - }); - - importedInternalRule = await rulesClient.create({ - data: ruleCreateParams, - allowMissingConnectorSecrets, + data: convertRuleResponseToAlertingRule(ruleWithUpdates), }); + return convertAlertingRuleToRuleResponse(updatedRule); } - /* Trying to convert an internal rule to a RuleResponse object */ - const parseResult = RuleResponse.safeParse(internalRuleToAPIResponse(importedInternalRule)); - - if (!parseResult.success) { - throw new RuleResponseValidationError({ - message: stringifyZodError(parseResult.error), - ruleId: importedInternalRule.params.ruleId, - }); - } - - return parseResult.data; + /* Rule does not exist, so we'll create it */ + return createRule({ + rulesClient, + mlAuthz, + rule: ruleToImport, + allowMissingConnectorSecrets, + }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/patch_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/patch_rule.ts index ce9956c5eec84..d615d5fc5a817 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/patch_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/patch_rule.ts @@ -6,34 +6,35 @@ */ import type { RulesClient } from '@kbn/alerting-plugin/server'; -import { stringifyZodError } from '@kbn/zod-helpers'; +import type { + RulePatchProps, + RuleResponse, +} from '../../../../../../../common/api/detection_engine/model/rule_schema'; import type { MlAuthz } from '../../../../../machine_learning/authz'; -import type { PatchRuleArgs } from '../detection_rules_client_interface'; +import type { IPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client'; +import { applyRulePatch } from '../mergers/apply_rule_patch'; import { getIdError } from '../../../utils/utils'; -import { - convertPatchAPIToInternalSchema, - internalRuleToAPIResponse, -} from '../../../normalization/rule_converters'; -import { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import { convertAlertingRuleToRuleResponse } from '../converters/convert_alerting_rule_to_rule_response'; +import { convertRuleResponseToAlertingRule } from '../converters/convert_rule_response_to_alerting_rule'; +import { ClientError, toggleRuleEnabledOnUpdate, validateMlAuth } from '../utils'; +import { getRuleByIdOrRuleId } from './get_rule_by_id_or_rule_id'; -import { - validateMlAuth, - ClientError, - toggleRuleEnabledOnUpdate, - RuleResponseValidationError, -} from '../utils'; +interface PatchRuleOptions { + rulesClient: RulesClient; + prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient; + rulePatch: RulePatchProps; + mlAuthz: MlAuthz; +} -import { readRules } from '../read_rules'; +export const patchRule = async ({ + rulesClient, + prebuiltRuleAssetClient, + rulePatch, + mlAuthz, +}: PatchRuleOptions): Promise => { + const { rule_id: ruleId, id } = rulePatch; -export const patchRule = async ( - rulesClient: RulesClient, - args: PatchRuleArgs, - mlAuthz: MlAuthz -): Promise => { - const { nextParams } = args; - const { rule_id: ruleId, id } = nextParams; - - const existingRule = await readRules({ + const existingRule = await getRuleByIdOrRuleId({ rulesClient, ruleId, id, @@ -44,32 +45,20 @@ export const patchRule = async ( throw new ClientError(error.message, error.statusCode); } - await validateMlAuth(mlAuthz, nextParams.type ?? existingRule.params.type); + await validateMlAuth(mlAuthz, rulePatch.type ?? existingRule.type); - const patchedRule = convertPatchAPIToInternalSchema(nextParams, existingRule); + const patchedRule = await applyRulePatch({ + prebuiltRuleAssetClient, + existingRule, + rulePatch, + }); const patchedInternalRule = await rulesClient.update({ id: existingRule.id, - data: patchedRule, + data: convertRuleResponseToAlertingRule(patchedRule), }); - const { enabled } = await toggleRuleEnabledOnUpdate( - rulesClient, - existingRule, - nextParams.enabled - ); - - /* Trying to convert the internal rule to a RuleResponse object */ - const parseResult = RuleResponse.safeParse( - internalRuleToAPIResponse({ ...patchedInternalRule, enabled }) - ); - - if (!parseResult.success) { - throw new RuleResponseValidationError({ - message: stringifyZodError(parseResult.error), - ruleId: patchedInternalRule.params.ruleId, - }); - } + const { enabled } = await toggleRuleEnabledOnUpdate(rulesClient, existingRule, patchedRule); - return parseResult.data; + return convertAlertingRuleToRuleResponse({ ...patchedInternalRule, enabled }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/update_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/update_rule.ts index 8684a7ccd2c61..cf42074c2a042 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/update_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/update_rule.ts @@ -6,36 +6,37 @@ */ import type { RulesClient } from '@kbn/alerting-plugin/server'; -import { stringifyZodError } from '@kbn/zod-helpers'; +import type { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; import type { MlAuthz } from '../../../../../machine_learning/authz'; -import type { UpdateRuleArgs } from '../detection_rules_client_interface'; +import { applyRuleUpdate } from '../mergers/apply_rule_update'; import { getIdError } from '../../../utils/utils'; -import { - convertUpdateAPIToInternalSchema, - internalRuleToAPIResponse, -} from '../../../normalization/rule_converters'; -import { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import { convertRuleResponseToAlertingRule } from '../converters/convert_rule_response_to_alerting_rule'; -import { - validateMlAuth, - ClientError, - toggleRuleEnabledOnUpdate, - RuleResponseValidationError, -} from '../utils'; +import { ClientError, toggleRuleEnabledOnUpdate, validateMlAuth } from '../utils'; -import { readRules } from '../read_rules'; +import type { RuleUpdateProps } from '../../../../../../../common/api/detection_engine'; +import type { IPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client'; +import { getRuleByIdOrRuleId } from './get_rule_by_id_or_rule_id'; +import { convertAlertingRuleToRuleResponse } from '../converters/convert_alerting_rule_to_rule_response'; -export const updateRule = async ( - rulesClient: RulesClient, - args: UpdateRuleArgs, - mlAuthz: MlAuthz -): Promise => { - const { ruleUpdate } = args; +interface UpdateRuleArguments { + rulesClient: RulesClient; + prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient; + ruleUpdate: RuleUpdateProps; + mlAuthz: MlAuthz; +} + +export const updateRule = async ({ + rulesClient, + prebuiltRuleAssetClient, + ruleUpdate, + mlAuthz, +}: UpdateRuleArguments): Promise => { const { rule_id: ruleId, id } = ruleUpdate; await validateMlAuth(mlAuthz, ruleUpdate.type); - const existingRule = await readRules({ + const existingRule = await getRuleByIdOrRuleId({ rulesClient, ruleId, id, @@ -46,33 +47,21 @@ export const updateRule = async ( throw new ClientError(error.message, error.statusCode); } - const newInternalRule = convertUpdateAPIToInternalSchema({ + const ruleWithUpdates = await applyRuleUpdate({ + prebuiltRuleAssetClient, existingRule, ruleUpdate, }); - const updatedInternalRule = await rulesClient.update({ + const updatedRule = await rulesClient.update({ id: existingRule.id, - data: newInternalRule, + data: convertRuleResponseToAlertingRule(ruleWithUpdates), }); - const { enabled } = await toggleRuleEnabledOnUpdate( - rulesClient, - existingRule, - ruleUpdate.enabled - ); - - /* Trying to convert the internal rule to a RuleResponse object */ - const parseResult = RuleResponse.safeParse( - internalRuleToAPIResponse({ ...updatedInternalRule, enabled }) - ); + const { enabled } = await toggleRuleEnabledOnUpdate(rulesClient, existingRule, ruleWithUpdates); - if (!parseResult.success) { - throw new RuleResponseValidationError({ - message: stringifyZodError(parseResult.error), - ruleId: updatedInternalRule.params.ruleId, - }); - } - - return parseResult.data; + return convertAlertingRuleToRuleResponse({ + ...updatedRule, + enabled, + }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/upgrade_prebuilt_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/upgrade_prebuilt_rule.ts index 8c1079f5716db..4eef323be2fd9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/upgrade_prebuilt_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/upgrade_prebuilt_rule.ts @@ -6,94 +6,74 @@ */ import type { RulesClient } from '@kbn/alerting-plugin/server'; -import { stringifyZodError } from '@kbn/zod-helpers'; +import type { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; import type { MlAuthz } from '../../../../../machine_learning/authz'; -import type { RuleParams } from '../../../../rule_schema'; -import type { UpgradePrebuiltRuleArgs } from '../detection_rules_client_interface'; -import { - convertPatchAPIToInternalSchema, - convertCreateAPIToInternalSchema, - internalRuleToAPIResponse, -} from '../../../normalization/rule_converters'; -import { transformAlertToRuleAction } from '../../../../../../../common/detection_engine/transform_actions'; -import { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; - -import { validateMlAuth, ClientError, RuleResponseValidationError } from '../utils'; - -import { readRules } from '../read_rules'; - -export const upgradePrebuiltRule = async ( - rulesClient: RulesClient, - upgradePrebuiltRulePayload: UpgradePrebuiltRuleArgs, - mlAuthz: MlAuthz -): Promise => { - const { ruleAsset } = upgradePrebuiltRulePayload; - +import type { PrebuiltRuleAsset } from '../../../../prebuilt_rules'; +import type { IPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client'; +import { convertAlertingRuleToRuleResponse } from '../converters/convert_alerting_rule_to_rule_response'; +import { convertRuleResponseToAlertingRule } from '../converters/convert_rule_response_to_alerting_rule'; +import { applyRulePatch } from '../mergers/apply_rule_patch'; +import { ClientError, validateMlAuth } from '../utils'; +import { createRule } from './create_rule'; +import { getRuleByRuleId } from './get_rule_by_rule_id'; + +export const upgradePrebuiltRule = async ({ + rulesClient, + ruleAsset, + mlAuthz, + prebuiltRuleAssetClient, +}: { + rulesClient: RulesClient; + ruleAsset: PrebuiltRuleAsset; + mlAuthz: MlAuthz; + prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient; +}): Promise => { await validateMlAuth(mlAuthz, ruleAsset.type); - const existingRule = await readRules({ + const existingRule = await getRuleByRuleId({ rulesClient, ruleId: ruleAsset.rule_id, - id: undefined, }); if (!existingRule) { throw new ClientError(`Failed to find rule ${ruleAsset.rule_id}`, 500); } - if (ruleAsset.type !== existingRule.params.type) { + if (ruleAsset.type !== existingRule.type) { // If we're trying to change the type of a prepackaged rule, we need to delete the old one // and replace it with the new rule, keeping the enabled setting, actions, throttle, id, // and exception lists from the old rule await rulesClient.delete({ id: existingRule.id }); - const internalRule = convertCreateAPIToInternalSchema( - { + const createdRule = await createRule({ + rulesClient, + mlAuthz, + rule: { ...ruleAsset, + immutable: true, enabled: existingRule.enabled, - exceptions_list: existingRule.params.exceptionsList, - actions: existingRule.actions.map(transformAlertToRuleAction), - timeline_id: existingRule.params.timelineId, - timeline_title: existingRule.params.timelineTitle, + exceptions_list: existingRule.exceptions_list, + actions: existingRule.actions, + timeline_id: existingRule.timeline_id, + timeline_title: existingRule.timeline_title, }, - { immutable: true, defaultEnabled: existingRule.enabled } - ); - - const createdRule = await rulesClient.create({ - data: internalRule, - options: { id: existingRule.id }, + id: existingRule.id, }); - /* Trying to convert the rule to a RuleResponse object */ - const parseResult = RuleResponse.safeParse(internalRuleToAPIResponse(createdRule)); - - if (!parseResult.success) { - throw new RuleResponseValidationError({ - message: stringifyZodError(parseResult.error), - ruleId: createdRule.params.ruleId, - }); - } - - return parseResult.data; + return createdRule; } // Else, simply patch it. - const patchedRule = convertPatchAPIToInternalSchema(ruleAsset, existingRule); + const patchedRule = await applyRulePatch({ + prebuiltRuleAssetClient, + existingRule, + rulePatch: ruleAsset, + }); const patchedInternalRule = await rulesClient.update({ id: existingRule.id, - data: patchedRule, + data: convertRuleResponseToAlertingRule(patchedRule), }); - /* Trying to convert the internal rule to a RuleResponse object */ - const parseResult = RuleResponse.safeParse(internalRuleToAPIResponse(patchedInternalRule)); - - if (!parseResult.success) { - throw new RuleResponseValidationError({ - message: stringifyZodError(parseResult.error), - ruleId: patchedInternalRule.params.ruleId, - }); - } - - return parseResult.data; + return convertAlertingRuleToRuleResponse(patchedInternalRule); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/read_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/read_rules.ts index d699d5ee7dd55..67c7746bd0eb3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/read_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/read_rules.ts @@ -30,6 +30,8 @@ export interface ReadRuleOptions { * be returned as a not-found or a thrown error that is not 404. * @param ruleId - This is a close second to being fast as long as it can find the rule_id from * a filter query against the ruleId property in params using `alert.attributes.params.ruleId: "${ruleId}"` + * + * @deprecated Should be replaced with DetectionRulesClient.getRuleById once it's implemented */ export const readRules = async ({ rulesClient, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/utils.ts index 4f25497b30564..db2af377eb5b1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/utils.ts @@ -12,21 +12,21 @@ import type { RulesClient } from '@kbn/alerting-plugin/server'; import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; import type { MlAuthz } from '../../../../machine_learning/authz'; -import type { RuleAlertType } from '../../../rule_schema'; import type { RuleSignatureId } from '../../../../../../common/api/detection_engine/model/rule_schema/common_attributes.gen'; import { throwAuthzError } from '../../../../machine_learning/validation'; +import type { RuleResponse } from '../../../../../../common/api/detection_engine'; export const toggleRuleEnabledOnUpdate = async ( rulesClient: RulesClient, - existingRule: RuleAlertType, - updatedRuleEnabled?: boolean + existingRule: RuleResponse, + updatedRule: RuleResponse ): Promise<{ enabled: boolean }> => { - if (existingRule.enabled && updatedRuleEnabled === false) { + if (existingRule.enabled && !updatedRule.enabled) { await rulesClient.disable({ id: existingRule.id }); return { enabled: false }; } - if (!existingRule.enabled && updatedRuleEnabled === true) { + if (!existingRule.enabled && updatedRule.enabled) { await rulesClient.enable({ id: existingRule.id }); return { enabled: true }; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts index 08794143ce161..5093393d6d657 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts @@ -15,7 +15,7 @@ import { getRuleMock, } from '../../../routes/__mocks__/request_responses'; import { getThreatMock } from '../../../../../../common/detection_engine/schemas/types/threat.mock'; -import { internalRuleToAPIResponse } from '../../normalization/rule_converters'; +import { internalRuleToAPIResponse } from '../detection_rules_client/converters/internal_rule_to_api_response'; import { getEqlRuleParams, getQueryRuleParams } from '../../../rule_schema/mocks'; import { getExportByObjectIds } from './get_export_by_object_ids'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.ts index ce57b33227ca4..7c3142aed85f6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.ts @@ -13,7 +13,7 @@ import type { ExceptionListClient } from '@kbn/lists-plugin/server'; import type { RulesClient, PartialRule } from '@kbn/alerting-plugin/server'; import type { ActionsClient } from '@kbn/actions-plugin/server'; import { withSecuritySpan } from '../../../../../utils/with_security_span'; -import { internalRuleToAPIResponse } from '../../normalization/rule_converters'; +import { internalRuleToAPIResponse } from '../detection_rules_client/converters/internal_rule_to_api_response'; import type { RuleParams } from '../../../rule_schema'; import { hasValidRuleType } from '../../../rule_schema'; import { findRules } from '../search/find_rules'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.test.ts deleted file mode 100644 index 5df02371befa2..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.test.ts +++ /dev/null @@ -1,491 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - commonParamsCamelToSnake, - patchTypeSpecificSnakeToCamel, - typeSpecificCamelToSnake, -} from './rule_converters'; -import { - getBaseRuleParams, - getEqlRuleParams, - getEsqlRuleParams, - getMlRuleParams, - getNewTermsRuleParams, - getQueryRuleParams, - getSavedQueryRuleParams, - getThreatRuleParams, - getThresholdRuleParams, -} from '../../rule_schema/mocks'; -import type { - AlertSuppressionDuration, - PatchRuleRequestBody, - AlertSuppressionMissingFieldsStrategy, -} from '../../../../../common/api/detection_engine'; - -describe('rule_converters', () => { - describe('patchTypeSpecificSnakeToCamel', () => { - describe('EQL', () => { - test('should accept EQL params when existing rule type is EQL', () => { - const patchParams = { - timestamp_field: 'event.created', - event_category_override: 'event.not_category', - tiebreaker_field: 'event.created', - }; - const rule = getEqlRuleParams(); - const patchedParams = patchTypeSpecificSnakeToCamel(patchParams, rule); - expect(patchedParams).toEqual( - expect.objectContaining({ - timestampField: 'event.created', - eventCategoryOverride: 'event.not_category', - tiebreakerField: 'event.created', - }) - ); - }); - test('should accept EQL params with suppression in snake case and convert to camel case when rule type is EQL', () => { - const patchParams = { - timestamp_field: 'event.created', - event_category_override: 'event.not_category', - tiebreaker_field: 'event.created', - alert_suppression: { - group_by: ['event.type'], - duration: { - value: 10, - unit: 'm', - } as AlertSuppressionDuration, - missing_fields_strategy: 'suppress', - }, - }; - const rule = getEqlRuleParams(); - const patchedParams = patchTypeSpecificSnakeToCamel(patchParams, rule); - expect(patchedParams).toEqual( - expect.objectContaining({ - timestampField: 'event.created', - eventCategoryOverride: 'event.not_category', - tiebreakerField: 'event.created', - alertSuppression: { - groupBy: ['event.type'], - duration: { - value: 10, - unit: 'm', - }, - missingFieldsStrategy: 'suppress', - }, - }) - ); - }); - test('should reject invalid EQL params when existing rule type is EQL', () => { - const patchParams = { - timestamp_field: 1, - event_category_override: 1, - tiebreaker_field: 1, - } as PatchRuleRequestBody; - const rule = getEqlRuleParams(); - expect(() => patchTypeSpecificSnakeToCamel(patchParams, rule)).toThrowError( - 'event_category_override: Expected string, received number, tiebreaker_field: Expected string, received number, timestamp_field: Expected string, received number' - ); - }); - test('should reject EQL params with invalid suppression group_by field', () => { - const patchParams = { - timestamp_field: 'event.created', - event_category_override: 'event.not_category', - tiebreaker_field: 'event.created', - alert_suppression: { - group_by: 'event.type', - duration: { - value: 10, - unit: 'm', - } as AlertSuppressionDuration, - missing_fields_strategy: 'suppress', - }, - }; - const rule = getEqlRuleParams(); - expect(() => patchTypeSpecificSnakeToCamel(patchParams, rule)).toThrowError( - 'alert_suppression.group_by: Expected array, received string' - ); - }); - }); - - describe('machine learning rules', () => { - test('should accept machine learning params when existing rule type is machine learning', () => { - const patchParams = { - anomaly_threshold: 5, - }; - const rule = getMlRuleParams(); - const patchedParams = patchTypeSpecificSnakeToCamel(patchParams, rule); - expect(patchedParams).toEqual( - expect.objectContaining({ - anomalyThreshold: 5, - }) - ); - }); - - test('should reject invalid machine learning params when existing rule type is machine learning', () => { - const patchParams = { - anomaly_threshold: 'invalid', - } as PatchRuleRequestBody; - const rule = getMlRuleParams(); - expect(() => patchTypeSpecificSnakeToCamel(patchParams, rule)).toThrowError( - 'anomaly_threshold: Expected number, received string' - ); - }); - - it('accepts suppression params', () => { - const patchParams = { - alert_suppression: { - group_by: ['agent.name'], - missing_fields_strategy: 'suppress' as const, - }, - }; - const rule = getMlRuleParams(); - const patchedParams = patchTypeSpecificSnakeToCamel(patchParams, rule); - - expect(patchedParams).toEqual( - expect.objectContaining({ - alertSuppression: { - groupBy: ['agent.name'], - missingFieldsStrategy: 'suppress', - }, - }) - ); - }); - }); - - test('should accept threat match params when existing rule type is threat match', () => { - const patchParams = { - threat_indicator_path: 'my.indicator', - threat_query: 'test-query', - }; - const rule = getThreatRuleParams(); - const patchedParams = patchTypeSpecificSnakeToCamel(patchParams, rule); - expect(patchedParams).toEqual( - expect.objectContaining({ - threatIndicatorPath: 'my.indicator', - threatQuery: 'test-query', - }) - ); - }); - - test('should reject invalid threat match params when existing rule type is threat match', () => { - const patchParams = { - threat_indicator_path: 1, - threat_query: 1, - } as PatchRuleRequestBody; - const rule = getThreatRuleParams(); - expect(() => patchTypeSpecificSnakeToCamel(patchParams, rule)).toThrowError( - 'threat_query: Expected string, received number, threat_indicator_path: Expected string, received number' - ); - }); - - test('should accept query params when existing rule type is query', () => { - const patchParams = { - index: ['new-test-index'], - language: 'lucene', - } as PatchRuleRequestBody; - const rule = getQueryRuleParams(); - const patchedParams = patchTypeSpecificSnakeToCamel(patchParams, rule); - expect(patchedParams).toEqual( - expect.objectContaining({ - index: ['new-test-index'], - language: 'lucene', - }) - ); - }); - - test('should reject invalid query params when existing rule type is query', () => { - const patchParams = { - index: [1], - language: 'non-language', - } as PatchRuleRequestBody; - const rule = getQueryRuleParams(); - expect(() => patchTypeSpecificSnakeToCamel(patchParams, rule)).toThrowError( - "index.0: Expected string, received number, language: Invalid enum value. Expected 'kuery' | 'lucene', received 'non-language'" - ); - }); - - test('should accept saved query params when existing rule type is saved query', () => { - const patchParams = { - index: ['new-test-index'], - language: 'lucene', - } as PatchRuleRequestBody; - const rule = getSavedQueryRuleParams(); - const patchedParams = patchTypeSpecificSnakeToCamel(patchParams, rule); - expect(patchedParams).toEqual( - expect.objectContaining({ - index: ['new-test-index'], - language: 'lucene', - }) - ); - }); - - test('should reject invalid saved query params when existing rule type is saved query', () => { - const patchParams = { - index: [1], - language: 'non-language', - } as PatchRuleRequestBody; - const rule = getSavedQueryRuleParams(); - expect(() => patchTypeSpecificSnakeToCamel(patchParams, rule)).toThrowError( - "index.0: Expected string, received number, language: Invalid enum value. Expected 'kuery' | 'lucene', received 'non-language'" - ); - }); - - test('should accept threshold params when existing rule type is threshold', () => { - const patchParams = { - threshold: { - field: ['host.name'], - value: 107, - }, - }; - const rule = getThresholdRuleParams(); - const patchedParams = patchTypeSpecificSnakeToCamel(patchParams, rule); - expect(patchedParams).toEqual( - expect.objectContaining({ - threshold: { - field: ['host.name'], - value: 107, - }, - }) - ); - }); - - test('should reject invalid threshold params when existing rule type is threshold', () => { - const patchParams = { - threshold: { - field: ['host.name'], - value: 'invalid', - }, - } as PatchRuleRequestBody; - const rule = getThresholdRuleParams(); - expect(() => patchTypeSpecificSnakeToCamel(patchParams, rule)).toThrowError( - 'threshold.value: Expected number, received string' - ); - }); - - test('should accept ES|QL alerts suppression params', () => { - const patchParams = { - alert_suppression: { - group_by: ['agent.name'], - duration: { value: 4, unit: 'h' as const }, - missing_fields_strategy: 'doNotSuppress' as const, - }, - }; - const rule = getEsqlRuleParams(); - const patchedParams = patchTypeSpecificSnakeToCamel(patchParams, rule); - expect(patchedParams).toEqual( - expect.objectContaining({ - alertSuppression: { - groupBy: ['agent.name'], - missingFieldsStrategy: 'doNotSuppress', - duration: { value: 4, unit: 'h' }, - }, - }) - ); - }); - - test('should accept threshold alerts suppression params', () => { - const patchParams = { - alert_suppression: { - duration: { value: 4, unit: 'h' as const }, - }, - }; - const rule = getThresholdRuleParams(); - const patchedParams = patchTypeSpecificSnakeToCamel(patchParams, rule); - expect(patchedParams).toEqual( - expect.objectContaining({ - alertSuppression: { - duration: { value: 4, unit: 'h' }, - }, - }) - ); - }); - - test('should accept threat_match alerts suppression params', () => { - const patchParams = { - alert_suppression: { - group_by: ['agent.name'], - missing_fields_strategy: 'suppress' as const, - }, - }; - const rule = getThreatRuleParams(); - const patchedParams = patchTypeSpecificSnakeToCamel(patchParams, rule); - expect(patchedParams).toEqual( - expect.objectContaining({ - alertSuppression: { - groupBy: ['agent.name'], - missingFieldsStrategy: 'suppress', - }, - }) - ); - }); - - test('should accept new_terms alerts suppression params', () => { - const patchParams = { - alert_suppression: { - group_by: ['agent.name'], - duration: { value: 4, unit: 'h' as const }, - missing_fields_strategy: 'suppress' as const, - }, - }; - const rule = getNewTermsRuleParams(); - const patchedParams = patchTypeSpecificSnakeToCamel(patchParams, rule); - expect(patchedParams).toEqual( - expect.objectContaining({ - alertSuppression: { - groupBy: ['agent.name'], - missingFieldsStrategy: 'suppress', - duration: { value: 4, unit: 'h' }, - }, - }) - ); - }); - - test('should accept new terms params when existing rule type is new terms', () => { - const patchParams = { - new_terms_fields: ['event.new_field'], - }; - const rule = getNewTermsRuleParams(); - const patchedParams = patchTypeSpecificSnakeToCamel(patchParams, rule); - expect(patchedParams).toEqual( - expect.objectContaining({ - newTermsFields: ['event.new_field'], - }) - ); - }); - - test('should reject invalid new terms params when existing rule type is new terms', () => { - const patchParams = { - new_terms_fields: 'invalid', - } as PatchRuleRequestBody; - const rule = getNewTermsRuleParams(); - expect(() => patchTypeSpecificSnakeToCamel(patchParams, rule)).toThrowError( - 'new_terms_fields: Expected array, received string' - ); - }); - }); - - describe('typeSpecificCamelToSnake', () => { - describe('EQL', () => { - test('should accept EQL params when existing rule type is EQL', () => { - const params = { - timestampField: 'event.created', - eventCategoryOverride: 'event.not_category', - tiebreakerField: 'event.created', - }; - const eqlRule = { ...getEqlRuleParams(), ...params }; - const transformedParams = typeSpecificCamelToSnake(eqlRule); - expect(transformedParams).toEqual( - expect.objectContaining({ - timestamp_field: 'event.created', - event_category_override: 'event.not_category', - tiebreaker_field: 'event.created', - }) - ); - }); - - test('should accept EQL params with suppression in camel case and convert to snake case when rule type is EQL', () => { - const params = { - timestampField: 'event.created', - eventCategoryOverride: 'event.not_category', - tiebreakerField: 'event.created', - alertSuppression: { - groupBy: ['event.type'], - duration: { - value: 10, - unit: 'm', - } as AlertSuppressionDuration, - missingFieldsStrategy: 'suppress' as AlertSuppressionMissingFieldsStrategy, - }, - }; - const eqlRule = { ...getEqlRuleParams(), ...params }; - const transformedParams = typeSpecificCamelToSnake(eqlRule); - expect(transformedParams).toEqual( - expect.objectContaining({ - timestamp_field: 'event.created', - event_category_override: 'event.not_category', - tiebreaker_field: 'event.created', - alert_suppression: { - group_by: ['event.type'], - duration: { - value: 10, - unit: 'm', - } as AlertSuppressionDuration, - missing_fields_strategy: 'suppress', - }, - }) - ); - }); - }); - - describe('machine learning rules', () => { - it('accepts normal params', () => { - const params = { - anomalyThreshold: 74, - machineLearningJobId: ['job-1'], - }; - const ruleParams = { ...getMlRuleParams(), ...params }; - const transformedParams = typeSpecificCamelToSnake(ruleParams); - expect(transformedParams).toEqual( - expect.objectContaining({ - anomaly_threshold: 74, - machine_learning_job_id: ['job-1'], - }) - ); - }); - - it('accepts suppression params', () => { - const params = { - anomalyThreshold: 74, - machineLearningJobId: ['job-1'], - alertSuppression: { - groupBy: ['event.type'], - duration: { - value: 10, - unit: 'm', - } as AlertSuppressionDuration, - missingFieldsStrategy: 'suppress' as AlertSuppressionMissingFieldsStrategy, - }, - }; - const ruleParams = { ...getMlRuleParams(), ...params }; - const transformedParams = typeSpecificCamelToSnake(ruleParams); - expect(transformedParams).toEqual( - expect.objectContaining({ - anomaly_threshold: 74, - machine_learning_job_id: ['job-1'], - alert_suppression: { - group_by: ['event.type'], - duration: { - value: 10, - unit: 'm', - }, - missing_fields_strategy: 'suppress', - }, - }) - ); - }); - }); - }); - - describe('commonParamsCamelToSnake', () => { - test('should convert rule_source params to snake case', () => { - const transformedParams = commonParamsCamelToSnake({ - ...getBaseRuleParams(), - ruleSource: { - type: 'external', - isCustomized: false, - }, - }); - expect(transformedParams).toEqual( - expect.objectContaining({ - rule_source: { - type: 'external', - is_customized: false, - }, - }) - ); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts deleted file mode 100644 index db815f32fb5ed..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts +++ /dev/null @@ -1,869 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { v4 as uuidv4 } from 'uuid'; - -import { stringifyZodError } from '@kbn/zod-helpers'; -import { BadRequestError } from '@kbn/securitysolution-es-utils'; -import { ruleTypeMappings } from '@kbn/securitysolution-rules'; -import type { ResolvedSanitizedRule, SanitizedRule } from '@kbn/alerting-plugin/common'; - -import type { RequiredOptional } from '@kbn/zod-helpers'; -import { - DEFAULT_INDICATOR_SOURCE_PATH, - DEFAULT_MAX_SIGNALS, - SERVER_APP_ID, -} from '../../../../../common/constants'; - -import type { PatchRuleRequestBody } from '../../../../../common/api/detection_engine/rule_management'; -import type { - RuleCreateProps, - RuleUpdateProps, - TypeSpecificCreateProps, - TypeSpecificResponse, -} from '../../../../../common/api/detection_engine/model/rule_schema'; -import { - EqlRulePatchFields, - EsqlRulePatchFields, - MachineLearningRulePatchFields, - NewTermsRulePatchFields, - QueryRulePatchFields, - SavedQueryRulePatchFields, - ThreatMatchRulePatchFields, - ThresholdRulePatchFields, - RuleResponse, -} from '../../../../../common/api/detection_engine/model/rule_schema'; - -import { - transformAlertToRuleAction, - transformAlertToRuleResponseAction, - transformRuleToAlertAction, - transformRuleToAlertResponseAction, -} from '../../../../../common/detection_engine/transform_actions'; - -import { - normalizeMachineLearningJobIds, - normalizeThresholdObject, -} from '../../../../../common/detection_engine/utils'; - -import { assertUnreachable } from '../../../../../common/utility_types'; - -import type { - InternalRuleCreate, - RuleParams, - TypeSpecificRuleParams, - BaseRuleParams, - EqlRuleParams, - EqlSpecificRuleParams, - EsqlRuleParams, - EsqlSpecificRuleParams, - ThreatRuleParams, - ThreatSpecificRuleParams, - QueryRuleParams, - QuerySpecificRuleParams, - SavedQuerySpecificRuleParams, - SavedQueryRuleParams, - ThresholdRuleParams, - ThresholdSpecificRuleParams, - MachineLearningRuleParams, - MachineLearningSpecificRuleParams, - InternalRuleUpdate, - NewTermsRuleParams, - NewTermsSpecificRuleParams, - RuleSourceCamelCased, -} from '../../rule_schema'; -import { transformFromAlertThrottle, transformToActionFrequency } from './rule_actions'; -import { - addEcsToRequiredFields, - convertAlertSuppressionToCamel, - convertAlertSuppressionToSnake, - migrateLegacyInvestigationFields, -} from '../utils/utils'; -import { createRuleExecutionSummary } from '../../rule_monitoring'; -import type { PrebuiltRuleAsset } from '../../prebuilt_rules'; -import { convertObjectKeysToSnakeCase } from '../../../../utils/object_case_converters'; - -const DEFAULT_FROM = 'now-6m' as const; -const DEFAULT_TO = 'now' as const; -const DEFAULT_INTERVAL = '5m' as const; - -// These functions provide conversions from the request API schema to the internal rule schema and from the internal rule schema -// to the response API schema. This provides static type-check assurances that the internal schema is in sync with the API schema for -// required and default-able fields. However, it is still possible to add an optional field to the API schema -// without causing a type-check error here. - -// Converts params from the snake case API format to the internal camel case format AND applies default values where needed. -// Notice that params.language is possibly undefined for most rule types in the API but we default it to kuery to match -// the legacy API behavior -export const typeSpecificSnakeToCamel = ( - params: TypeSpecificCreateProps -): TypeSpecificRuleParams => { - switch (params.type) { - case 'eql': { - return { - type: params.type, - language: params.language, - index: params.index, - dataViewId: params.data_view_id, - query: params.query, - filters: params.filters, - timestampField: params.timestamp_field, - eventCategoryOverride: params.event_category_override, - tiebreakerField: params.tiebreaker_field, - alertSuppression: convertAlertSuppressionToCamel(params.alert_suppression), - }; - } - case 'esql': { - return { - type: params.type, - language: params.language, - query: params.query, - alertSuppression: convertAlertSuppressionToCamel(params.alert_suppression), - }; - } - case 'threat_match': { - return { - type: params.type, - language: params.language ?? 'kuery', - index: params.index, - dataViewId: params.data_view_id, - query: params.query, - filters: params.filters, - savedId: params.saved_id, - threatFilters: params.threat_filters, - threatQuery: params.threat_query, - threatMapping: params.threat_mapping, - threatLanguage: params.threat_language, - threatIndex: params.threat_index, - threatIndicatorPath: params.threat_indicator_path ?? DEFAULT_INDICATOR_SOURCE_PATH, - concurrentSearches: params.concurrent_searches, - itemsPerSearch: params.items_per_search, - alertSuppression: convertAlertSuppressionToCamel(params.alert_suppression), - }; - } - case 'query': { - return { - type: params.type, - language: params.language ?? 'kuery', - index: params.index, - dataViewId: params.data_view_id, - query: params.query ?? '', - filters: params.filters, - savedId: params.saved_id, - responseActions: params.response_actions?.map(transformRuleToAlertResponseAction), - alertSuppression: convertAlertSuppressionToCamel(params.alert_suppression), - }; - } - case 'saved_query': { - return { - type: params.type, - language: params.language ?? 'kuery', - index: params.index, - query: params.query, - filters: params.filters, - savedId: params.saved_id, - dataViewId: params.data_view_id, - responseActions: params.response_actions?.map(transformRuleToAlertResponseAction), - alertSuppression: convertAlertSuppressionToCamel(params.alert_suppression), - }; - } - case 'threshold': { - return { - type: params.type, - language: params.language ?? 'kuery', - index: params.index, - dataViewId: params.data_view_id, - query: params.query, - filters: params.filters, - savedId: params.saved_id, - threshold: normalizeThresholdObject(params.threshold), - alertSuppression: params.alert_suppression?.duration - ? { duration: params.alert_suppression.duration } - : undefined, - }; - } - case 'machine_learning': { - return { - type: params.type, - anomalyThreshold: params.anomaly_threshold, - machineLearningJobId: normalizeMachineLearningJobIds(params.machine_learning_job_id), - alertSuppression: convertAlertSuppressionToCamel(params.alert_suppression), - }; - } - case 'new_terms': { - return { - type: params.type, - query: params.query, - newTermsFields: params.new_terms_fields, - historyWindowStart: params.history_window_start, - index: params.index, - filters: params.filters, - language: params.language ?? 'kuery', - dataViewId: params.data_view_id, - alertSuppression: convertAlertSuppressionToCamel(params.alert_suppression), - }; - } - default: { - return assertUnreachable(params); - } - } -}; - -const patchEqlParams = ( - params: EqlRulePatchFields, - existingRule: EqlRuleParams -): EqlSpecificRuleParams => { - return { - type: existingRule.type, - language: params.language ?? existingRule.language, - index: params.index ?? existingRule.index, - dataViewId: params.data_view_id ?? existingRule.dataViewId, - query: params.query ?? existingRule.query, - filters: params.filters ?? existingRule.filters, - timestampField: params.timestamp_field ?? existingRule.timestampField, - eventCategoryOverride: params.event_category_override ?? existingRule.eventCategoryOverride, - tiebreakerField: params.tiebreaker_field ?? existingRule.tiebreakerField, - alertSuppression: - convertAlertSuppressionToCamel(params.alert_suppression) ?? existingRule.alertSuppression, - }; -}; - -const patchEsqlParams = ( - params: EsqlRulePatchFields, - existingRule: EsqlRuleParams -): EsqlSpecificRuleParams => { - return { - type: existingRule.type, - language: params.language ?? existingRule.language, - query: params.query ?? existingRule.query, - alertSuppression: - convertAlertSuppressionToCamel(params.alert_suppression) ?? existingRule.alertSuppression, - }; -}; - -const patchThreatMatchParams = ( - params: ThreatMatchRulePatchFields, - existingRule: ThreatRuleParams -): ThreatSpecificRuleParams => { - return { - type: existingRule.type, - language: params.language ?? existingRule.language, - index: params.index ?? existingRule.index, - dataViewId: params.data_view_id ?? existingRule.dataViewId, - query: params.query ?? existingRule.query, - filters: params.filters ?? existingRule.filters, - savedId: params.saved_id ?? existingRule.savedId, - threatFilters: params.threat_filters ?? existingRule.threatFilters, - threatQuery: params.threat_query ?? existingRule.threatQuery, - threatMapping: params.threat_mapping ?? existingRule.threatMapping, - threatLanguage: params.threat_language ?? existingRule.threatLanguage, - threatIndex: params.threat_index ?? existingRule.threatIndex, - threatIndicatorPath: params.threat_indicator_path ?? existingRule.threatIndicatorPath, - concurrentSearches: params.concurrent_searches ?? existingRule.concurrentSearches, - itemsPerSearch: params.items_per_search ?? existingRule.itemsPerSearch, - alertSuppression: - convertAlertSuppressionToCamel(params.alert_suppression) ?? existingRule.alertSuppression, - }; -}; - -const patchQueryParams = ( - params: QueryRulePatchFields, - existingRule: QueryRuleParams -): QuerySpecificRuleParams => { - return { - type: existingRule.type, - language: params.language ?? existingRule.language, - index: params.index ?? existingRule.index, - dataViewId: params.data_view_id ?? existingRule.dataViewId, - query: params.query ?? existingRule.query, - filters: params.filters ?? existingRule.filters, - savedId: params.saved_id ?? existingRule.savedId, - responseActions: - params.response_actions?.map(transformRuleToAlertResponseAction) ?? - existingRule.responseActions, - alertSuppression: - convertAlertSuppressionToCamel(params.alert_suppression) ?? existingRule.alertSuppression, - }; -}; - -const patchSavedQueryParams = ( - params: SavedQueryRulePatchFields, - existingRule: SavedQueryRuleParams -): SavedQuerySpecificRuleParams => { - return { - type: existingRule.type, - language: params.language ?? existingRule.language, - index: params.index ?? existingRule.index, - dataViewId: params.data_view_id ?? existingRule.dataViewId, - query: params.query ?? existingRule.query, - filters: params.filters ?? existingRule.filters, - savedId: params.saved_id ?? existingRule.savedId, - responseActions: - params.response_actions?.map(transformRuleToAlertResponseAction) ?? - existingRule.responseActions, - alertSuppression: - convertAlertSuppressionToCamel(params.alert_suppression) ?? existingRule.alertSuppression, - }; -}; - -const patchThresholdParams = ( - params: ThresholdRulePatchFields, - existingRule: ThresholdRuleParams -): ThresholdSpecificRuleParams => { - return { - type: existingRule.type, - language: params.language ?? existingRule.language, - index: params.index ?? existingRule.index, - dataViewId: params.data_view_id ?? existingRule.dataViewId, - query: params.query ?? existingRule.query, - filters: params.filters ?? existingRule.filters, - savedId: params.saved_id ?? existingRule.savedId, - threshold: params.threshold - ? normalizeThresholdObject(params.threshold) - : existingRule.threshold, - alertSuppression: params.alert_suppression ?? existingRule.alertSuppression, - }; -}; - -const patchMachineLearningParams = ( - params: MachineLearningRulePatchFields, - existingRule: MachineLearningRuleParams -): MachineLearningSpecificRuleParams => { - return { - type: existingRule.type, - anomalyThreshold: params.anomaly_threshold ?? existingRule.anomalyThreshold, - machineLearningJobId: params.machine_learning_job_id - ? normalizeMachineLearningJobIds(params.machine_learning_job_id) - : existingRule.machineLearningJobId, - alertSuppression: - convertAlertSuppressionToCamel(params.alert_suppression) ?? existingRule.alertSuppression, - }; -}; - -const patchNewTermsParams = ( - params: NewTermsRulePatchFields, - existingRule: NewTermsRuleParams -): NewTermsSpecificRuleParams => { - return { - type: existingRule.type, - language: params.language ?? existingRule.language, - index: params.index ?? existingRule.index, - dataViewId: params.data_view_id ?? existingRule.dataViewId, - query: params.query ?? existingRule.query, - filters: params.filters ?? existingRule.filters, - newTermsFields: params.new_terms_fields ?? existingRule.newTermsFields, - historyWindowStart: params.history_window_start ?? existingRule.historyWindowStart, - alertSuppression: - convertAlertSuppressionToCamel(params.alert_suppression) ?? existingRule.alertSuppression, - }; -}; - -export const patchTypeSpecificSnakeToCamel = ( - params: PatchRuleRequestBody, - existingRule: RuleParams -): TypeSpecificRuleParams => { - // Here we do the validation of patch params by rule type to ensure that the fields that are - // passed in to patch are of the correct type, e.g. `query` is a string. Since the combined patch schema - // is a union of types where everything is optional, it's hard to do the validation before we know the rule type - - // a patch request that defines `event_category_override` as a number would not be assignable to the EQL patch schema, - // but would be assignable to the other rule types since they don't specify `event_category_override`. - switch (existingRule.type) { - case 'eql': { - const result = EqlRulePatchFields.safeParse(params); - if (!result.success) { - throw new BadRequestError(stringifyZodError(result.error)); - } - return patchEqlParams(result.data, existingRule); - } - case 'esql': { - const result = EsqlRulePatchFields.safeParse(params); - if (!result.success) { - throw new BadRequestError(stringifyZodError(result.error)); - } - return patchEsqlParams(result.data, existingRule); - } - case 'threat_match': { - const result = ThreatMatchRulePatchFields.safeParse(params); - if (!result.success) { - throw new BadRequestError(stringifyZodError(result.error)); - } - return patchThreatMatchParams(result.data, existingRule); - } - case 'query': { - const result = QueryRulePatchFields.safeParse(params); - if (!result.success) { - throw new BadRequestError(stringifyZodError(result.error)); - } - return patchQueryParams(result.data, existingRule); - } - case 'saved_query': { - const result = SavedQueryRulePatchFields.safeParse(params); - if (!result.success) { - throw new BadRequestError(stringifyZodError(result.error)); - } - return patchSavedQueryParams(result.data, existingRule); - } - case 'threshold': { - const result = ThresholdRulePatchFields.safeParse(params); - if (!result.success) { - throw new BadRequestError(stringifyZodError(result.error)); - } - return patchThresholdParams(result.data, existingRule); - } - case 'machine_learning': { - const result = MachineLearningRulePatchFields.safeParse(params); - if (!result.success) { - throw new BadRequestError(stringifyZodError(result.error)); - } - return patchMachineLearningParams(result.data, existingRule); - } - case 'new_terms': { - const result = NewTermsRulePatchFields.safeParse(params); - if (!result.success) { - throw new BadRequestError(stringifyZodError(result.error)); - } - return patchNewTermsParams(result.data, existingRule); - } - default: { - return assertUnreachable(existingRule); - } - } -}; - -interface ConvertUpdateAPIToInternalSchemaProps { - existingRule: SanitizedRule; - ruleUpdate: RuleUpdateProps; -} - -export const convertUpdateAPIToInternalSchema = ({ - existingRule, - ruleUpdate, -}: ConvertUpdateAPIToInternalSchemaProps) => { - const alertActions = - ruleUpdate.actions?.map((action) => transformRuleToAlertAction(action)) ?? []; - const actions = transformToActionFrequency(alertActions, ruleUpdate.throttle); - - const typeSpecificParams = typeSpecificSnakeToCamel(ruleUpdate); - - const newInternalRule: InternalRuleUpdate = { - name: ruleUpdate.name, - tags: ruleUpdate.tags ?? [], - params: { - author: ruleUpdate.author ?? [], - buildingBlockType: ruleUpdate.building_block_type, - description: ruleUpdate.description, - ruleId: existingRule.params.ruleId, - falsePositives: ruleUpdate.false_positives ?? [], - from: ruleUpdate.from ?? 'now-6m', - investigationFields: ruleUpdate.investigation_fields, - immutable: existingRule.params.immutable, - ruleSource: convertImmutableToRuleSource(existingRule.params.immutable), - license: ruleUpdate.license, - outputIndex: ruleUpdate.output_index ?? '', - timelineId: ruleUpdate.timeline_id, - timelineTitle: ruleUpdate.timeline_title, - meta: ruleUpdate.meta, - maxSignals: ruleUpdate.max_signals ?? DEFAULT_MAX_SIGNALS, - relatedIntegrations: ruleUpdate.related_integrations ?? [], - requiredFields: addEcsToRequiredFields(ruleUpdate.required_fields), - riskScore: ruleUpdate.risk_score, - riskScoreMapping: ruleUpdate.risk_score_mapping ?? [], - ruleNameOverride: ruleUpdate.rule_name_override, - setup: ruleUpdate.setup, - severity: ruleUpdate.severity, - severityMapping: ruleUpdate.severity_mapping ?? [], - threat: ruleUpdate.threat ?? [], - timestampOverride: ruleUpdate.timestamp_override, - timestampOverrideFallbackDisabled: ruleUpdate.timestamp_override_fallback_disabled, - to: ruleUpdate.to ?? 'now', - references: ruleUpdate.references ?? [], - namespace: ruleUpdate.namespace, - note: ruleUpdate.note, - version: ruleUpdate.version ?? existingRule.params.version, - exceptionsList: ruleUpdate.exceptions_list ?? [], - ...typeSpecificParams, - }, - schedule: { interval: ruleUpdate.interval ?? '5m' }, - actions, - }; - - return newInternalRule; -}; - -// eslint-disable-next-line complexity -export const convertPatchAPIToInternalSchema = ( - nextParams: PatchRuleRequestBody, - existingRule: SanitizedRule -): InternalRuleUpdate => { - const typeSpecificParams = patchTypeSpecificSnakeToCamel(nextParams, existingRule.params); - const existingParams = existingRule.params; - - const alertActions = - nextParams.actions?.map((action) => transformRuleToAlertAction(action)) ?? existingRule.actions; - const throttle = nextParams.throttle ?? transformFromAlertThrottle(existingRule); - const actions = transformToActionFrequency(alertActions, throttle); - - return { - name: nextParams.name ?? existingRule.name, - tags: nextParams.tags ?? existingRule.tags, - params: { - author: nextParams.author ?? existingParams.author, - buildingBlockType: nextParams.building_block_type ?? existingParams.buildingBlockType, - description: nextParams.description ?? existingParams.description, - ruleId: existingParams.ruleId, - falsePositives: nextParams.false_positives ?? existingParams.falsePositives, - investigationFields: nextParams.investigation_fields ?? existingParams.investigationFields, - from: nextParams.from ?? existingParams.from, - immutable: existingParams.immutable, - ruleSource: convertImmutableToRuleSource(existingParams.immutable), - license: nextParams.license ?? existingParams.license, - outputIndex: nextParams.output_index ?? existingParams.outputIndex, - timelineId: nextParams.timeline_id ?? existingParams.timelineId, - timelineTitle: nextParams.timeline_title ?? existingParams.timelineTitle, - meta: nextParams.meta ?? existingParams.meta, - maxSignals: nextParams.max_signals ?? existingParams.maxSignals, - relatedIntegrations: nextParams.related_integrations ?? existingParams.relatedIntegrations, - requiredFields: addEcsToRequiredFields(nextParams.required_fields), - riskScore: nextParams.risk_score ?? existingParams.riskScore, - riskScoreMapping: nextParams.risk_score_mapping ?? existingParams.riskScoreMapping, - ruleNameOverride: nextParams.rule_name_override ?? existingParams.ruleNameOverride, - setup: nextParams.setup ?? existingParams.setup, - severity: nextParams.severity ?? existingParams.severity, - severityMapping: nextParams.severity_mapping ?? existingParams.severityMapping, - threat: nextParams.threat ?? existingParams.threat, - timestampOverride: nextParams.timestamp_override ?? existingParams.timestampOverride, - timestampOverrideFallbackDisabled: - nextParams.timestamp_override_fallback_disabled ?? - existingParams.timestampOverrideFallbackDisabled, - to: nextParams.to ?? existingParams.to, - references: nextParams.references ?? existingParams.references, - namespace: nextParams.namespace ?? existingParams.namespace, - note: nextParams.note ?? existingParams.note, - version: nextParams.version ?? existingParams.version, - exceptionsList: nextParams.exceptions_list ?? existingParams.exceptionsList, - ...typeSpecificParams, - }, - schedule: { interval: nextParams.interval ?? existingRule.schedule.interval }, - actions, - }; -}; - -interface RuleCreateOptions { - immutable?: boolean; - defaultEnabled?: boolean; -} - -// eslint-disable-next-line complexity -export const convertCreateAPIToInternalSchema = ( - input: RuleCreateProps, - options?: RuleCreateOptions -): InternalRuleCreate => { - const { immutable = false, defaultEnabled = true } = options ?? {}; - - const typeSpecificParams = typeSpecificSnakeToCamel(input); - const newRuleId = input.rule_id ?? uuidv4(); - - const alertActions = input.actions?.map((action) => transformRuleToAlertAction(action)) ?? []; - const actions = transformToActionFrequency(alertActions, input.throttle); - - return { - name: input.name, - tags: input.tags ?? [], - alertTypeId: ruleTypeMappings[input.type], - consumer: SERVER_APP_ID, - params: { - author: input.author ?? [], - buildingBlockType: input.building_block_type, - description: input.description, - ruleId: newRuleId, - falsePositives: input.false_positives ?? [], - investigationFields: input.investigation_fields, - from: input.from ?? DEFAULT_FROM, - immutable, - ruleSource: convertImmutableToRuleSource(immutable), - license: input.license, - outputIndex: input.output_index ?? '', - timelineId: input.timeline_id, - timelineTitle: input.timeline_title, - meta: input.meta, - maxSignals: input.max_signals ?? DEFAULT_MAX_SIGNALS, - riskScore: input.risk_score, - riskScoreMapping: input.risk_score_mapping ?? [], - ruleNameOverride: input.rule_name_override, - severity: input.severity, - severityMapping: input.severity_mapping ?? [], - threat: input.threat ?? [], - timestampOverride: input.timestamp_override, - timestampOverrideFallbackDisabled: input.timestamp_override_fallback_disabled, - to: input.to ?? DEFAULT_TO, - references: input.references ?? [], - namespace: input.namespace, - note: input.note, - version: input.version ?? 1, - exceptionsList: input.exceptions_list ?? [], - relatedIntegrations: input.related_integrations ?? [], - requiredFields: addEcsToRequiredFields(input.required_fields), - setup: input.setup ?? '', - ...typeSpecificParams, - }, - schedule: { interval: input.interval ?? '5m' }, - enabled: input.enabled ?? defaultEnabled, - actions, - }; -}; - -// Converts the internal rule data structure to the response API schema -export const typeSpecificCamelToSnake = ( - params: TypeSpecificRuleParams -): RequiredOptional => { - switch (params.type) { - case 'eql': { - return { - type: params.type, - language: params.language, - index: params.index, - data_view_id: params.dataViewId, - query: params.query, - filters: params.filters, - timestamp_field: params.timestampField, - event_category_override: params.eventCategoryOverride, - tiebreaker_field: params.tiebreakerField, - alert_suppression: convertAlertSuppressionToSnake(params.alertSuppression), - }; - } - case 'esql': { - return { - type: params.type, - language: params.language, - query: params.query, - alert_suppression: convertAlertSuppressionToSnake(params.alertSuppression), - }; - } - case 'threat_match': { - return { - type: params.type, - language: params.language, - index: params.index, - data_view_id: params.dataViewId, - query: params.query, - filters: params.filters, - saved_id: params.savedId, - threat_filters: params.threatFilters, - threat_query: params.threatQuery, - threat_mapping: params.threatMapping, - threat_language: params.threatLanguage, - threat_index: params.threatIndex, - threat_indicator_path: params.threatIndicatorPath, - concurrent_searches: params.concurrentSearches, - items_per_search: params.itemsPerSearch, - alert_suppression: convertAlertSuppressionToSnake(params.alertSuppression), - }; - } - case 'query': { - return { - type: params.type, - language: params.language, - index: params.index, - data_view_id: params.dataViewId, - query: params.query, - filters: params.filters, - saved_id: params.savedId, - response_actions: params.responseActions?.map(transformAlertToRuleResponseAction), - alert_suppression: convertAlertSuppressionToSnake(params.alertSuppression), - }; - } - case 'saved_query': { - return { - type: params.type, - language: params.language, - index: params.index, - query: params.query, - filters: params.filters, - saved_id: params.savedId, - data_view_id: params.dataViewId, - response_actions: params.responseActions?.map(transformAlertToRuleResponseAction), - alert_suppression: convertAlertSuppressionToSnake(params.alertSuppression), - }; - } - case 'threshold': { - return { - type: params.type, - language: params.language, - index: params.index, - data_view_id: params.dataViewId, - query: params.query, - filters: params.filters, - saved_id: params.savedId, - threshold: params.threshold, - alert_suppression: params.alertSuppression?.duration - ? { duration: params.alertSuppression?.duration } - : undefined, - }; - } - case 'machine_learning': { - return { - type: params.type, - anomaly_threshold: params.anomalyThreshold, - machine_learning_job_id: params.machineLearningJobId, - alert_suppression: convertAlertSuppressionToSnake(params.alertSuppression), - }; - } - case 'new_terms': { - return { - type: params.type, - query: params.query, - new_terms_fields: params.newTermsFields, - history_window_start: params.historyWindowStart, - index: params.index, - filters: params.filters, - language: params.language, - data_view_id: params.dataViewId, - alert_suppression: convertAlertSuppressionToSnake(params.alertSuppression), - }; - } - default: { - return assertUnreachable(params); - } - } -}; - -// TODO: separate out security solution defined common params from Alerting framework common params -// so we can explicitly specify the return type of this function -export const commonParamsCamelToSnake = (params: BaseRuleParams) => { - return { - description: params.description, - risk_score: params.riskScore, - severity: params.severity, - building_block_type: params.buildingBlockType, - namespace: params.namespace, - note: params.note, - license: params.license, - output_index: params.outputIndex, - timeline_id: params.timelineId, - timeline_title: params.timelineTitle, - meta: params.meta, - rule_name_override: params.ruleNameOverride, - timestamp_override: params.timestampOverride, - timestamp_override_fallback_disabled: params.timestampOverrideFallbackDisabled, - investigation_fields: migrateLegacyInvestigationFields(params.investigationFields), - author: params.author, - false_positives: params.falsePositives, - from: params.from, - rule_id: params.ruleId, - max_signals: params.maxSignals, - risk_score_mapping: params.riskScoreMapping, - severity_mapping: params.severityMapping, - threat: params.threat, - to: params.to, - references: params.references, - version: params.version, - exceptions_list: params.exceptionsList, - immutable: params.immutable, - rule_source: convertObjectKeysToSnakeCase(params.ruleSource), - related_integrations: params.relatedIntegrations ?? [], - required_fields: params.requiredFields ?? [], - setup: params.setup ?? '', - }; -}; - -export const internalRuleToAPIResponse = ( - rule: SanitizedRule | ResolvedSanitizedRule -): RequiredOptional => { - const executionSummary = createRuleExecutionSummary(rule); - - const isResolvedRule = (obj: unknown): obj is ResolvedSanitizedRule => - (obj as ResolvedSanitizedRule).outcome != null; - - const alertActions = rule.actions.map(transformAlertToRuleAction); - const throttle = transformFromAlertThrottle(rule); - const actions = transformToActionFrequency(alertActions, throttle); - - return { - // saved object properties - outcome: isResolvedRule(rule) ? rule.outcome : undefined, - alias_target_id: isResolvedRule(rule) ? rule.alias_target_id : undefined, - alias_purpose: isResolvedRule(rule) ? rule.alias_purpose : undefined, - // Alerting framework params - id: rule.id, - updated_at: rule.updatedAt.toISOString(), - updated_by: rule.updatedBy ?? 'elastic', - created_at: rule.createdAt.toISOString(), - created_by: rule.createdBy ?? 'elastic', - name: rule.name, - tags: rule.tags, - interval: rule.schedule.interval, - enabled: rule.enabled, - revision: rule.revision, - // Security solution shared rule params - ...commonParamsCamelToSnake(rule.params), - // Type specific security solution rule params - ...typeSpecificCamelToSnake(rule.params), - // Actions - throttle: undefined, - actions, - // Execution summary - execution_summary: executionSummary ?? undefined, - }; -}; - -export const convertPrebuiltRuleAssetToRuleResponse = ( - prebuiltRuleAsset: PrebuiltRuleAsset -): RuleResponse => { - const prebuiltRuleAssetDefaults = { - enabled: false, - risk_score_mapping: [], - severity_mapping: [], - interval: DEFAULT_INTERVAL, - to: DEFAULT_TO, - from: DEFAULT_FROM, - exceptions_list: [], - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - actions: [], - related_integrations: [], - required_fields: [], - setup: '', - note: '', - references: [], - threat: [], - tags: [], - author: [], - }; - - const immutable = true; - - const ruleResponseSpecificFields = { - id: uuidv4(), - updated_at: new Date(0).toISOString(), - updated_by: '', - created_at: new Date(0).toISOString(), - created_by: '', - immutable, - rule_source: convertObjectKeysToSnakeCase(convertImmutableToRuleSource(immutable)), - revision: 1, - }; - - return RuleResponse.parse({ - ...prebuiltRuleAssetDefaults, - ...prebuiltRuleAsset, - required_fields: addEcsToRequiredFields(prebuiltRuleAsset.required_fields), - ...ruleResponseSpecificFields, - }); -}; - -export const convertImmutableToRuleSource = (immutable: boolean): RuleSourceCamelCased => { - if (immutable) { - return { - type: 'external', - isCustomized: false, - }; - } - - return { - type: 'internal', - }; -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/utils.test.ts index 61436a04c2675..536a314fa6c09 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/utils.test.ts @@ -38,7 +38,7 @@ import { getMlRuleParams, getQueryRuleParams, getThreatRuleParams } from '../../ import { createRulesAndExceptionsStreamFromNdJson } from '../logic/import/create_rules_stream_from_ndjson'; import type { RuleExceptionsPromiseFromStreams } from '../logic/import/import_rules_utils'; -import { internalRuleToAPIResponse } from '../normalization/rule_converters'; +import { internalRuleToAPIResponse } from '../logic/detection_rules_client/converters/internal_rule_to_api_response'; type PromiseFromStreams = RuleToImport | Error; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/utils.ts index 66fa635e768ad..bf6227ddcbfe8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/utils.ts @@ -17,8 +17,6 @@ import type { SavedObjectsClientContract } from '@kbn/core/server'; import type { RuleAction } from '@kbn/securitysolution-io-ts-alerting-types'; import type { - AlertSuppression, - AlertSuppressionCamel, InvestigationFields, RequiredField, RequiredFieldInput, @@ -33,7 +31,7 @@ import type { BulkError, OutputError } from '../../routes/utils'; import { createBulkErrorObject } from '../../routes/utils'; import type { InvestigationFieldsCombined, RuleAlertType, RuleParams } from '../../rule_schema'; import { hasValidRuleType } from '../../rule_schema'; -import { internalRuleToAPIResponse } from '../normalization/rule_converters'; +import { internalRuleToAPIResponse } from '../logic/detection_rules_client/converters/internal_rule_to_api_response'; type PromiseFromStreams = RuleToImport | Error; const MAX_CONCURRENT_SEARCHES = 10; @@ -347,28 +345,6 @@ export const getInvalidConnectors = async ( return [Array.from(errors.values()), Array.from(rulesAcc.values())]; }; -export const convertAlertSuppressionToCamel = ( - input: AlertSuppression | undefined -): AlertSuppressionCamel | undefined => - input - ? { - groupBy: input.group_by, - duration: input.duration, - missingFieldsStrategy: input.missing_fields_strategy, - } - : undefined; - -export const convertAlertSuppressionToSnake = ( - input: AlertSuppressionCamel | undefined -): AlertSuppression | undefined => - input - ? { - group_by: input.groupBy, - duration: input.duration, - missing_fields_strategy: input.missingFieldsStrategy, - } - : undefined; - /** * In ESS 8.10.x "investigation_fields" are mapped as string[]. * For 8.11+ logic is added on read in our endpoints to migrate diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.ts index 298b7f62d2973..dd77122ac4560 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.ts @@ -31,7 +31,7 @@ import { type UnifiedQueryRuleParams, } from '../../rule_schema'; import { type BulkError, createBulkErrorObject } from '../../routes/utils'; -import { internalRuleToAPIResponse } from '../normalization/rule_converters'; +import { internalRuleToAPIResponse } from '../logic/detection_rules_client/converters/internal_rule_to_api_response'; export const transformValidateBulkError = ( ruleId: string, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts index c2faa464b75da..7b48e32bf9962 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts @@ -23,6 +23,7 @@ import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import { DEFAULT_PREVIEW_INDEX, DETECTION_ENGINE_RULES_PREVIEW, + SERVER_APP_ID, } from '../../../../../../common/constants'; import { validateCreateRuleProps } from '../../../../../../common/api/detection_engine/rule_management'; import { RuleExecutionStatusEnum } from '../../../../../../common/api/detection_engine/rule_monitoring'; @@ -34,7 +35,6 @@ import { PreviewRulesSchema } from '../../../../../../common/api/detection_engin import type { StartPlugins, SetupPlugins } from '../../../../../plugin'; import { buildSiemResponse } from '../../../routes/utils'; -import { convertCreateAPIToInternalSchema } from '../../../rule_management'; import type { RuleParams } from '../../../rule_schema'; import { createPreviewRuleExecutionLogger } from './preview_rule_execution_logger'; import { parseInterval } from '../../../rule_types/utils/utils'; @@ -64,6 +64,8 @@ import { createSecurityRuleTypeWrapper } from '../../../rule_types/create_securi import { assertUnreachable } from '../../../../../../common/utility_types'; import { wrapScopedClusterClient } from './wrap_scoped_cluster_client'; import { wrapSearchSourceClient } from './wrap_search_source_client'; +import { applyRuleDefaults } from '../../../rule_management/logic/detection_rules_client/mergers/apply_rule_defaults'; +import { convertRuleResponseToAlertingRule } from '../../../rule_management/logic/detection_rules_client/converters/convert_rule_response_to_alerting_rule'; const PREVIEW_TIMEOUT_SECONDS = 60; const MAX_ROUTE_CONCURRENCY = 10; @@ -118,7 +120,7 @@ export const previewRulesRoute = ( }); } - const internalRule = convertCreateAPIToInternalSchema(request.body); + const internalRule = convertRuleResponseToAlertingRule(applyRuleDefaults(request.body)); const previewRuleParams = internalRule.params; const mlAuthz = buildMlAuthz({ @@ -237,6 +239,8 @@ export const previewRulesRoute = ( createdAt: new Date(), createdBy: username ?? 'preview-created-by', producer: 'preview-producer', + consumer: SERVER_APP_ID, + enabled: true, revision: 0, ruleTypeId, ruleTypeName, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/endpoint_response_action.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/endpoint_response_action.ts index 6ebc378dfd315..a310cb33497e8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/endpoint_response_action.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/endpoint_response_action.ts @@ -12,10 +12,13 @@ import type { RuleResponseEndpointAction, ProcessesParams, } from '../../../../common/api/detection_engine'; -import type { KillOrSuspendProcessRequestBody } from '../../../../common/endpoint/types'; import { getErrorProcessAlerts, getIsolateAlerts, getProcessAlerts } from './utils'; import type { AlertsAction, ResponseActionAlerts } from './types'; import type { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services'; +import type { + ResponseActionParametersWithEntityId, + ResponseActionParametersWithPid, +} from '../../../../common/endpoint/types'; export const endpointResponseAction = async ( responseAction: RuleResponseEndpointAction, @@ -115,7 +118,9 @@ export const endpointResponseAction = async ( comment, endpoint_ids, alert_ids, - parameters: parameters as KillOrSuspendProcessRequestBody['parameters'], + parameters: parameters as + | ResponseActionParametersWithPid + | ResponseActionParametersWithEntityId, }, { hosts, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts index b3000edf895dc..5d5065170deff 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts @@ -5,11 +5,6 @@ * 2.0. */ import type { SanitizedRuleConfig } from '@kbn/alerting-plugin/common'; -import type { - RuleActionArrayCamel, - RuleActionNotifyWhen, - RuleActionThrottle, -} from '@kbn/securitysolution-io-ts-alerting-types'; import type { EQL_RULE_TYPE_ID, ESQL_RULE_TYPE_ID, @@ -22,12 +17,9 @@ import type { THRESHOLD_RULE_TYPE_ID, } from '@kbn/securitysolution-rules'; import * as z from 'zod'; +import type { CreateRuleData } from '@kbn/alerting-plugin/server/application/rule/methods/create'; +import type { UpdateRuleData } from '@kbn/alerting-plugin/server/application/rule/methods/update'; import { RuleResponseAction } from '../../../../../common/api/detection_engine'; -import type { - IsRuleEnabled, - RuleName, - RuleTagArray, -} from '../../../../../common/api/detection_engine/model/rule_schema'; import { AlertsIndex, AlertsIndexNamespace, @@ -334,29 +326,8 @@ export type AllRuleTypes = | typeof THRESHOLD_RULE_TYPE_ID | typeof NEW_TERMS_RULE_TYPE_ID; -export interface InternalRuleCreate { - name: RuleName; - tags: RuleTagArray; - alertTypeId: AllRuleTypes; +export type InternalRuleCreate = CreateRuleData & { consumer: typeof SERVER_APP_ID; - schedule: { - interval: string; - }; - enabled: IsRuleEnabled; - actions: RuleActionArrayCamel; - params: RuleParams; - throttle?: RuleActionThrottle | null; - notifyWhen?: RuleActionNotifyWhen | null; -} +}; -export interface InternalRuleUpdate { - name: RuleName; - tags: RuleTagArray; - schedule: { - interval: string; - }; - actions: RuleActionArrayCamel; - params: RuleParams; - throttle?: RuleActionThrottle | null; - notifyWhen?: RuleActionNotifyWhen | null; -} +export type InternalRuleUpdate = UpdateRuleData; diff --git a/x-pack/plugins/security_solution/server/lib/risk_score/onboarding/routes/install_risk_scores.test.ts b/x-pack/plugins/security_solution/server/lib/risk_score/onboarding/routes/install_risk_scores.test.ts index 526f3d595d5a0..114d51bfae6a4 100644 --- a/x-pack/plugins/security_solution/server/lib/risk_score/onboarding/routes/install_risk_scores.test.ts +++ b/x-pack/plugins/security_solution/server/lib/risk_score/onboarding/routes/install_risk_scores.test.ts @@ -55,7 +55,6 @@ describe(`installRiskScoresRoute - ${RiskScoreEntity.host}`, () => { let server: ReturnType; let { context } = requestContextMock.createTools(); const logger = { error: jest.fn() } as unknown as Logger; - const security = undefined; const mockSpaceId = 'mockSpaceId'; beforeAll(async () => { @@ -71,7 +70,7 @@ describe(`installRiskScoresRoute - ${RiskScoreEntity.host}`, () => { }, }); - installRiskScoresRoute(server.router, logger, security); + installRiskScoresRoute(server.router, logger); await server.inject(request, requestContextMock.convertContext(context)); }); @@ -120,7 +119,6 @@ describe(`installRiskScoresRoute - ${RiskScoreEntity.user}`, () => { let server: ReturnType; let { context } = requestContextMock.createTools(); const logger = { error: jest.fn() } as unknown as Logger; - const security = undefined; const mockSpaceId = 'mockSpaceId'; beforeAll(async () => { @@ -136,7 +134,7 @@ describe(`installRiskScoresRoute - ${RiskScoreEntity.user}`, () => { }, }); - installRiskScoresRoute(server.router, logger, security); + installRiskScoresRoute(server.router, logger); await server.inject(request, requestContextMock.convertContext(context)); }); diff --git a/x-pack/plugins/security_solution/server/lib/risk_score/onboarding/routes/install_risk_scores.ts b/x-pack/plugins/security_solution/server/lib/risk_score/onboarding/routes/install_risk_scores.ts index 627a9afd4e871..de6675985fc00 100644 --- a/x-pack/plugins/security_solution/server/lib/risk_score/onboarding/routes/install_risk_scores.ts +++ b/x-pack/plugins/security_solution/server/lib/risk_score/onboarding/routes/install_risk_scores.ts @@ -11,18 +11,12 @@ import type { Logger } from '@kbn/core/server'; import { APP_ID, INTERNAL_RISK_SCORE_URL } from '../../../../../common/constants'; import type { SecuritySolutionPluginRouter } from '../../../../types'; -import type { SetupPlugins } from '../../../../plugin'; - import { buildSiemResponse } from '../../../detection_engine/routes/utils'; import { installRiskScoreModule } from '../helpers/install_risk_score_module'; import { onboardingRiskScoreRequestBody } from '../../../../../common/api/entity_analytics/risk_score'; -export const installRiskScoresRoute = ( - router: SecuritySolutionPluginRouter, - logger: Logger, - security: SetupPlugins['security'] -) => { +export const installRiskScoresRoute = (router: SecuritySolutionPluginRouter, logger: Logger) => { router.versioned .post({ access: 'internal', diff --git a/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/routes/create_prebuilt_saved_objects.test.ts b/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/routes/create_prebuilt_saved_objects.test.ts index bf265e8638de1..300012738134d 100644 --- a/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/routes/create_prebuilt_saved_objects.test.ts +++ b/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/routes/create_prebuilt_saved_objects.test.ts @@ -5,12 +5,10 @@ * 2.0. */ import type { Logger } from '@kbn/core/server'; -import type { SecurityPluginSetup } from '@kbn/security-plugin/server'; import { PREBUILT_SAVED_OBJECTS_BULK_CREATE } from '../../../../../common/constants'; import { serverMock, requestContextMock, - mockGetCurrentUser, requestMock, } from '../../../detection_engine/routes/__mocks__'; import { getEmptySavedObjectsResponse } from '../../../detection_engine/routes/__mocks__/request_responses'; @@ -52,7 +50,6 @@ const createPrebuiltSavedObjectsRequest = (savedObjectTemplate: string) => describe('createPrebuiltSavedObjects', () => { let server: ReturnType; - let securitySetup: SecurityPluginSetup; let { clients, context } = requestContextMock.createTools(); const logger = { error: jest.fn() } as unknown as Logger; @@ -62,16 +59,9 @@ describe('createPrebuiltSavedObjects', () => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); - securitySetup = { - authc: { - getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), - }, - authz: {}, - } as unknown as SecurityPluginSetup; - clients.savedObjectsClient.bulkCreate.mockResolvedValue(getEmptySavedObjectsResponse()); - createPrebuiltSavedObjectsRoute(server.router, logger, securitySetup); + createPrebuiltSavedObjectsRoute(server.router, logger); }); it.each([['hostRiskScoreDashboards'], ['userRiskScoreDashboards']])( diff --git a/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/routes/create_prebuilt_saved_objects.ts b/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/routes/create_prebuilt_saved_objects.ts index 568489155707a..a17669af734fe 100644 --- a/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/routes/create_prebuilt_saved_objects.ts +++ b/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/routes/create_prebuilt_saved_objects.ts @@ -10,8 +10,6 @@ import type { Logger } from '@kbn/core/server'; import { PREBUILT_SAVED_OBJECTS_BULK_CREATE } from '../../../../../common/constants'; import type { SecuritySolutionPluginRouter } from '../../../../types'; -import type { SetupPlugins } from '../../../../plugin'; - import { buildSiemResponse } from '../../../detection_engine/routes/utils'; import { buildFrameworkRequest } from '../../../timeline/utils/common'; @@ -20,8 +18,7 @@ import { createPrebuiltSavedObjectsRequestBody } from '../../../../../common/api export const createPrebuiltSavedObjectsRoute = ( router: SecuritySolutionPluginRouter, - logger: Logger, - security: SetupPlugins['security'] + logger: Logger ) => { router.versioned .post({ @@ -46,7 +43,7 @@ export const createPrebuiltSavedObjectsRoute = ( const spaceId = securitySolution?.getSpaceId(); - const frameworkRequest = await buildFrameworkRequest(context, security, request); + const frameworkRequest = await buildFrameworkRequest(context, request); const savedObjectsClient = (await frameworkRequest.context.core).savedObjects.client; const result = await bulkCreateSavedObjects({ savedObjectsClient, diff --git a/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/routes/delete_prebuilt_saved_objects.test.ts b/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/routes/delete_prebuilt_saved_objects.test.ts index 363aba02536ea..37c2417340fcd 100644 --- a/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/routes/delete_prebuilt_saved_objects.test.ts +++ b/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/routes/delete_prebuilt_saved_objects.test.ts @@ -4,12 +4,10 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { SecurityPluginSetup } from '@kbn/security-plugin/server'; import { PREBUILT_SAVED_OBJECTS_BULK_DELETE } from '../../../../../common/constants'; import { serverMock, requestContextMock, - mockGetCurrentUser, requestMock, } from '../../../detection_engine/routes/__mocks__'; import { deletePrebuiltSavedObjectsRoute } from './delete_prebuilt_saved_objects'; @@ -48,7 +46,6 @@ const deletePrebuiltSavedObjectsRequest = (savedObjectTemplate: string) => describe('deletePrebuiltSavedObjects', () => { let server: ReturnType; - let securitySetup: SecurityPluginSetup; let { clients, context } = requestContextMock.createTools(); beforeEach(() => { @@ -57,16 +54,9 @@ describe('deletePrebuiltSavedObjects', () => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); - securitySetup = { - authc: { - getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), - }, - authz: {}, - } as unknown as SecurityPluginSetup; - clients.savedObjectsClient.delete.mockResolvedValue(''); - deletePrebuiltSavedObjectsRoute(server.router, securitySetup); + deletePrebuiltSavedObjectsRoute(server.router); }); it('should delete legacy hostRiskScoreDashboards', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/routes/delete_prebuilt_saved_objects.ts b/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/routes/delete_prebuilt_saved_objects.ts index c78d1f292afe6..bd7ae03191ea5 100644 --- a/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/routes/delete_prebuilt_saved_objects.ts +++ b/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/routes/delete_prebuilt_saved_objects.ts @@ -10,18 +10,13 @@ import { transformError } from '@kbn/securitysolution-es-utils'; import { PREBUILT_SAVED_OBJECTS_BULK_DELETE } from '../../../../../common/constants'; import type { SecuritySolutionPluginRouter } from '../../../../types'; -import type { SetupPlugins } from '../../../../plugin'; - import { buildSiemResponse } from '../../../detection_engine/routes/utils'; import { buildFrameworkRequest } from '../../../timeline/utils/common'; import { bulkDeleteSavedObjects } from '../helpers/bulk_delete_saved_objects'; import { deletePrebuiltSavedObjectsRequestBody } from '../../../../../common/api/entity_analytics/risk_score'; -export const deletePrebuiltSavedObjectsRoute = ( - router: SecuritySolutionPluginRouter, - security: SetupPlugins['security'] -) => { +export const deletePrebuiltSavedObjectsRoute = (router: SecuritySolutionPluginRouter) => { router.versioned .post({ access: 'internal', @@ -42,7 +37,7 @@ export const deletePrebuiltSavedObjectsRoute = ( const spaceId = securitySolution?.getSpaceId(); - const frameworkRequest = await buildFrameworkRequest(context, security, request); + const frameworkRequest = await buildFrameworkRequest(context, request); const savedObjectsClient = (await frameworkRequest.context.core).savedObjects.client; const res = await bulkDeleteSavedObjects({ diff --git a/x-pack/plugins/security_solution/server/lib/tags/routes/create_tag.test.ts b/x-pack/plugins/security_solution/server/lib/tags/routes/create_tag.test.ts index c546d2fe63a49..232c09e76ee32 100644 --- a/x-pack/plugins/security_solution/server/lib/tags/routes/create_tag.test.ts +++ b/x-pack/plugins/security_solution/server/lib/tags/routes/create_tag.test.ts @@ -5,12 +5,10 @@ * 2.0. */ import type { Logger } from '@kbn/core/server'; -import type { SecurityPluginSetup } from '@kbn/security-plugin/server'; import { INTERNAL_TAGS_URL } from '../../../../common/constants'; import { serverMock, requestContextMock, - mockGetCurrentUser, requestMock, } from '../../detection_engine/routes/__mocks__'; import { mockGetTagsResult } from '../__mocks__'; @@ -18,7 +16,6 @@ import { createTagRoute } from './create_tag'; describe('createTagRoute', () => { let server: ReturnType; - let securitySetup: SecurityPluginSetup; const { context } = requestContextMock.createTools(); const logger = { error: jest.fn() } as unknown as Logger; @@ -34,14 +31,7 @@ describe('createTagRoute', () => { jest.clearAllMocks(); server = serverMock.create(); - securitySetup = { - authc: { - getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), - }, - authz: {}, - } as unknown as SecurityPluginSetup; - - createTagRoute(server.router, logger, securitySetup); + createTagRoute(server.router, logger); }); it('should return tags with the exact name', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/tags/routes/create_tag.ts b/x-pack/plugins/security_solution/server/lib/tags/routes/create_tag.ts index 97499b76ae354..1604b4374b984 100644 --- a/x-pack/plugins/security_solution/server/lib/tags/routes/create_tag.ts +++ b/x-pack/plugins/security_solution/server/lib/tags/routes/create_tag.ts @@ -10,18 +10,13 @@ import { i18n } from '@kbn/i18n'; import { transformError } from '@kbn/securitysolution-es-utils'; import { createTagRequest } from '../../../../common/api/tags'; import { INTERNAL_TAGS_URL } from '../../../../common/constants'; -import type { SetupPlugins } from '../../../plugin'; import type { SecuritySolutionPluginRouter } from '../../../types'; import { buildRouteValidationWithExcess } from '../../../utils/build_validation/route_validation'; import { buildSiemResponse } from '../../detection_engine/routes/utils'; import { buildFrameworkRequest } from '../../timeline/utils/common'; import { createTag } from '../saved_objects'; -export const createTagRoute = ( - router: SecuritySolutionPluginRouter, - logger: Logger, - security: SetupPlugins['security'] -) => { +export const createTagRoute = (router: SecuritySolutionPluginRouter, logger: Logger) => { router.versioned .put({ path: INTERNAL_TAGS_URL, @@ -36,7 +31,7 @@ export const createTagRoute = ( validate: { request: { body: buildRouteValidationWithExcess(createTagRequest) } }, }, async (context, request, response) => { - const frameworkRequest = await buildFrameworkRequest(context, security, request); + const frameworkRequest = await buildFrameworkRequest(context, request); const savedObjectsClient = (await frameworkRequest.context.core).savedObjects.client; const { name: tagName, description, color } = request.body; try { diff --git a/x-pack/plugins/security_solution/server/lib/tags/routes/get_tags_by_name.test.ts b/x-pack/plugins/security_solution/server/lib/tags/routes/get_tags_by_name.test.ts index dd18ad8257867..fab00d4db059b 100644 --- a/x-pack/plugins/security_solution/server/lib/tags/routes/get_tags_by_name.test.ts +++ b/x-pack/plugins/security_solution/server/lib/tags/routes/get_tags_by_name.test.ts @@ -5,12 +5,10 @@ * 2.0. */ import type { Logger, SavedObjectsFindResponse } from '@kbn/core/server'; -import type { SecurityPluginSetup } from '@kbn/security-plugin/server'; import { INTERNAL_TAGS_URL, SECURITY_TAG_NAME } from '../../../../common/constants'; import { serverMock, requestContextMock, - mockGetCurrentUser, requestMock, } from '../../detection_engine/routes/__mocks__'; import { mockGetTagsResult } from '../__mocks__'; @@ -18,7 +16,6 @@ import { getTagsByNameRoute } from './get_tags_by_name'; describe('getTagsByNameRoute', () => { let server: ReturnType; - let securitySetup: SecurityPluginSetup; const { context } = requestContextMock.createTools(); const logger = { error: jest.fn() } as unknown as Logger; @@ -36,14 +33,7 @@ describe('getTagsByNameRoute', () => { jest.clearAllMocks(); server = serverMock.create(); - securitySetup = { - authc: { - getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), - }, - authz: {}, - } as unknown as SecurityPluginSetup; - - getTagsByNameRoute(server.router, logger, securitySetup); + getTagsByNameRoute(server.router, logger); }); it('should return tags with the exact name', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/tags/routes/get_tags_by_name.ts b/x-pack/plugins/security_solution/server/lib/tags/routes/get_tags_by_name.ts index cd07a71db5a1c..75ae24d0eacd5 100644 --- a/x-pack/plugins/security_solution/server/lib/tags/routes/get_tags_by_name.ts +++ b/x-pack/plugins/security_solution/server/lib/tags/routes/get_tags_by_name.ts @@ -10,18 +10,13 @@ import { i18n } from '@kbn/i18n'; import { transformError } from '@kbn/securitysolution-es-utils'; import { getTagsByNameRequest } from '../../../../common/api/tags'; import { INTERNAL_TAGS_URL } from '../../../../common/constants'; -import type { SetupPlugins } from '../../../plugin'; import type { SecuritySolutionPluginRouter } from '../../../types'; import { buildRouteValidationWithExcess } from '../../../utils/build_validation/route_validation'; import { buildSiemResponse } from '../../detection_engine/routes/utils'; import { buildFrameworkRequest } from '../../timeline/utils/common'; import { findTagsByName } from '../saved_objects'; -export const getTagsByNameRoute = ( - router: SecuritySolutionPluginRouter, - logger: Logger, - security: SetupPlugins['security'] -) => { +export const getTagsByNameRoute = (router: SecuritySolutionPluginRouter, logger: Logger) => { router.versioned .get({ path: INTERNAL_TAGS_URL, @@ -36,7 +31,7 @@ export const getTagsByNameRoute = ( validate: { request: { query: buildRouteValidationWithExcess(getTagsByNameRequest) } }, }, async (context, request, response) => { - const frameworkRequest = await buildFrameworkRequest(context, security, request); + const frameworkRequest = await buildFrameworkRequest(context, request); const savedObjectsClient = (await frameworkRequest.context.core).savedObjects.client; const { name: tagName } = request.query; diff --git a/x-pack/plugins/security_solution/server/lib/tags/routes/index.ts b/x-pack/plugins/security_solution/server/lib/tags/routes/index.ts index bfcdf518ee905..f7677388856a7 100644 --- a/x-pack/plugins/security_solution/server/lib/tags/routes/index.ts +++ b/x-pack/plugins/security_solution/server/lib/tags/routes/index.ts @@ -6,16 +6,11 @@ */ import type { Logger } from '@kbn/core/server'; -import type { SetupPlugins } from '../../../plugin_contract'; import type { SecuritySolutionPluginRouter } from '../../../types'; import { createTagRoute } from './create_tag'; import { getTagsByNameRoute } from './get_tags_by_name'; -export const registerTagsRoutes = ( - router: SecuritySolutionPluginRouter, - logger: Logger, - security: SetupPlugins['security'] -) => { - getTagsByNameRoute(router, logger, security); - createTagRoute(router, logger, security); +export const registerTagsRoutes = (router: SecuritySolutionPluginRouter, logger: Logger) => { + getTagsByNameRoute(router, logger); + createTagRoute(router, logger); }; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/clean_draft_timelines/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/clean_draft_timelines/index.ts index 604e9dabe0d62..ae1cff768fded 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/clean_draft_timelines/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/clean_draft_timelines/index.ts @@ -13,7 +13,6 @@ import { buildSiemResponse } from '../../../../detection_engine/routes/utils'; import { TIMELINE_DRAFT_URL } from '../../../../../../common/constants'; import { buildFrameworkRequest } from '../../../utils/common'; -import type { SetupPlugins } from '../../../../../plugin'; import { buildRouteValidationWithExcess } from '../../../../../utils/build_validation/route_validation'; import { getDraftTimeline, @@ -24,11 +23,7 @@ import { import { draftTimelineDefaults } from '../../../utils/default_timeline'; import { cleanDraftTimelineSchema, TimelineType } from '../../../../../../common/api/timeline'; -export const cleanDraftTimelinesRoute = ( - router: SecuritySolutionPluginRouter, - _: ConfigType, - security: SetupPlugins['security'] -) => { +export const cleanDraftTimelinesRoute = (router: SecuritySolutionPluginRouter, _: ConfigType) => { router.versioned .post({ path: TIMELINE_DRAFT_URL, @@ -45,7 +40,7 @@ export const cleanDraftTimelinesRoute = ( version: '2023-10-31', }, async (context, request, response) => { - const frameworkRequest = await buildFrameworkRequest(context, security, request); + const frameworkRequest = await buildFrameworkRequest(context, request); const siemResponse = buildSiemResponse(response); try { diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/get_draft_timelines/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/get_draft_timelines/index.ts index 93e5d9fe84b00..6619e4a0eb18b 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/get_draft_timelines/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/get_draft_timelines/index.ts @@ -12,17 +12,12 @@ import { buildSiemResponse } from '../../../../detection_engine/routes/utils'; import { TIMELINE_DRAFT_URL } from '../../../../../../common/constants'; import { buildFrameworkRequest } from '../../../utils/common'; -import type { SetupPlugins } from '../../../../../plugin'; import { buildRouteValidationWithExcess } from '../../../../../utils/build_validation/route_validation'; import { getDraftTimeline, persistTimeline } from '../../../saved_object/timelines'; import { draftTimelineDefaults } from '../../../utils/default_timeline'; import { getDraftTimelineSchema } from '../../../../../../common/api/timeline'; -export const getDraftTimelinesRoute = ( - router: SecuritySolutionPluginRouter, - _: ConfigType, - security: SetupPlugins['security'] -) => { +export const getDraftTimelinesRoute = (router: SecuritySolutionPluginRouter, _: ConfigType) => { router.versioned .get({ path: TIMELINE_DRAFT_URL, @@ -39,7 +34,7 @@ export const getDraftTimelinesRoute = ( version: '2023-10-31', }, async (context, request, response) => { - const frameworkRequest = await buildFrameworkRequest(context, security, request); + const frameworkRequest = await buildFrameworkRequest(context, request); const siemResponse = buildSiemResponse(response); try { diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/index.ts index 8fa7e7a8f31d6..6b44496b6c3c4 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -import type { SetupPlugins } from '../../../plugin'; import type { SecuritySolutionPluginRouter } from '../../../types'; import type { ConfigType } from '../../..'; import { @@ -28,30 +27,26 @@ import { persistNoteRoute, deleteNoteRoute, getNotesRoute } from './notes'; import { persistPinnedEventRoute } from './pinned_events'; -export function registerTimelineRoutes( - router: SecuritySolutionPluginRouter, - config: ConfigType, - security: SetupPlugins['security'] -) { - createTimelinesRoute(router, config, security); - patchTimelinesRoute(router, config, security); - - importTimelinesRoute(router, config, security); - exportTimelinesRoute(router, config, security); - getDraftTimelinesRoute(router, config, security); - getTimelineRoute(router, config, security); - resolveTimelineRoute(router, config, security); - getTimelinesRoute(router, config, security); - cleanDraftTimelinesRoute(router, config, security); - deleteTimelinesRoute(router, config, security); - persistFavoriteRoute(router, config, security); - copyTimelineRoute(router, config, security); - - installPrepackedTimelinesRoute(router, config, security); - - persistNoteRoute(router, config, security); - deleteNoteRoute(router, config, security); - getNotesRoute(router, config, security); - - persistPinnedEventRoute(router, config, security); +export function registerTimelineRoutes(router: SecuritySolutionPluginRouter, config: ConfigType) { + createTimelinesRoute(router, config); + patchTimelinesRoute(router, config); + + importTimelinesRoute(router, config); + exportTimelinesRoute(router, config); + getDraftTimelinesRoute(router, config); + getTimelineRoute(router, config); + resolveTimelineRoute(router, config); + getTimelinesRoute(router, config); + cleanDraftTimelinesRoute(router, config); + deleteTimelinesRoute(router, config); + persistFavoriteRoute(router, config); + copyTimelineRoute(router, config); + + installPrepackedTimelinesRoute(router, config); + + persistNoteRoute(router, config); + deleteNoteRoute(router, config); + getNotesRoute(router, config); + + persistPinnedEventRoute(router, config); } diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/delete_note.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/delete_note.ts index 6cab82872b924..318d8950bc619 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/delete_note.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/delete_note.ts @@ -10,7 +10,6 @@ import type { SecuritySolutionPluginRouter } from '../../../../types'; import { NOTE_URL } from '../../../../../common/constants'; -import type { SetupPlugins } from '../../../../plugin'; import { buildRouteValidationWithExcess } from '../../../../utils/build_validation/route_validation'; import type { ConfigType } from '../../../..'; @@ -20,11 +19,7 @@ import { buildFrameworkRequest } from '../../utils/common'; import { deleteNoteSchema } from '../../../../../common/api/timeline'; import { deleteNote } from '../../saved_object/notes'; -export const deleteNoteRoute = ( - router: SecuritySolutionPluginRouter, - config: ConfigType, - security: SetupPlugins['security'] -) => { +export const deleteNoteRoute = (router: SecuritySolutionPluginRouter, config: ConfigType) => { router.versioned .delete({ path: NOTE_URL, @@ -44,7 +39,7 @@ export const deleteNoteRoute = ( const siemResponse = buildSiemResponse(response); try { - const frameworkRequest = await buildFrameworkRequest(context, security, request); + const frameworkRequest = await buildFrameworkRequest(context, request); const noteId = request.body?.noteId ?? ''; const noteIds = request.body?.noteIds ?? null; if (noteIds != null) { diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/get_notes.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/get_notes.ts index 97aecc06ef198..f230d0832a96c 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/get_notes.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/get_notes.ts @@ -10,18 +10,13 @@ import type { SecuritySolutionPluginRouter } from '../../../../types'; import { NOTE_URL } from '../../../../../common/constants'; import type { ConfigType } from '../../../..'; -import type { SetupPlugins } from '../../../../plugin'; import { buildSiemResponse } from '../../../detection_engine/routes/utils'; import { buildFrameworkRequest, getNotesPaginated } from '../../utils/common'; import { getAllSavedNote, MAX_UNASSOCIATED_NOTES } from '../../saved_object/notes'; import { noteSavedObjectType } from '../../saved_object_mappings/notes'; -export const getNotesRoute = ( - router: SecuritySolutionPluginRouter, - _: ConfigType, - security: SetupPlugins['security'] -) => { +export const getNotesRoute = (router: SecuritySolutionPluginRouter, _: ConfigType) => { router.versioned .get({ path: NOTE_URL, @@ -40,7 +35,7 @@ export const getNotesRoute = ( async (context, request, response) => { try { const queryParams = request.query; - const frameworkRequest = await buildFrameworkRequest(context, security, request); + const frameworkRequest = await buildFrameworkRequest(context, request); const documentIds = queryParams.documentIds ?? null; if (documentIds != null) { if (Array.isArray(documentIds)) { diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/persist_note.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/persist_note.ts index e4cf4b8a9c777..1bd91306a7419 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/persist_note.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/persist_note.ts @@ -10,7 +10,6 @@ import type { SecuritySolutionPluginRouter } from '../../../../types'; import { NOTE_URL } from '../../../../../common/constants'; -import type { SetupPlugins } from '../../../../plugin'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; import type { ConfigType } from '../../../..'; @@ -20,11 +19,7 @@ import { buildFrameworkRequest } from '../../utils/common'; import { persistNoteWithoutRefSchema } from '../../../../../common/api/timeline'; import { persistNote } from '../../saved_object/notes'; -export const persistNoteRoute = ( - router: SecuritySolutionPluginRouter, - _: ConfigType, - security: SetupPlugins['security'] -) => { +export const persistNoteRoute = (router: SecuritySolutionPluginRouter, _: ConfigType) => { router.versioned .patch({ path: NOTE_URL, @@ -44,7 +39,7 @@ export const persistNoteRoute = ( const siemResponse = buildSiemResponse(response); try { - const frameworkRequest = await buildFrameworkRequest(context, security, request); + const frameworkRequest = await buildFrameworkRequest(context, request); const { note } = request.body; const noteId = request.body?.noteId ?? null; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/pinned_events/persist_pinned_event.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/pinned_events/persist_pinned_event.ts index 529f5d830e803..a5739c49a34ea 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/pinned_events/persist_pinned_event.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/pinned_events/persist_pinned_event.ts @@ -10,7 +10,6 @@ import type { SecuritySolutionPluginRouter } from '../../../../types'; import { PINNED_EVENT_URL } from '../../../../../common/constants'; -import type { SetupPlugins } from '../../../../plugin'; import { buildRouteValidationWithExcess } from '../../../../utils/build_validation/route_validation'; import type { ConfigType } from '../../../..'; @@ -22,8 +21,7 @@ import { persistPinnedEventOnTimeline } from '../../saved_object/pinned_events'; export const persistPinnedEventRoute = ( router: SecuritySolutionPluginRouter, - config: ConfigType, - security: SetupPlugins['security'] + config: ConfigType ) => { router.versioned .patch({ @@ -44,7 +42,7 @@ export const persistPinnedEventRoute = ( const siemResponse = buildSiemResponse(response); try { - const frameworkRequest = await buildFrameworkRequest(context, security, request); + const frameworkRequest = await buildFrameworkRequest(context, request); const { eventId } = request.body; const pinnedEventId = request.body?.pinnedEventId ?? null; const timelineId = request.body?.timelineId ?? null; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/helpers.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/helpers.test.ts index 763833a0ab4e8..7a4e7a41a1ee7 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/helpers.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/helpers.test.ts @@ -6,13 +6,11 @@ */ import { createPromiseFromStreams } from '@kbn/utils'; -import type { SecurityPluginSetup } from '@kbn/security-plugin/server'; import type { FrameworkRequest } from '../../../../framework'; import { createMockConfig, requestContextMock, - mockGetCurrentUser, } from '../../../../detection_engine/routes/__mocks__'; import { addPrepackagedRulesRequest, @@ -27,7 +25,6 @@ import type { ImportTimelineResultSchema } from '../../../../../../common/api/ti jest.mock('../../timelines/import_timelines/helpers'); describe('installPrepackagedTimelines', () => { - let securitySetup: SecurityPluginSetup; let frameworkRequest: FrameworkRequest; const spyInstallPrepackagedTimelines = jest.spyOn(helpers, 'installPrepackagedTimelines'); @@ -37,13 +34,6 @@ describe('installPrepackagedTimelines', () => { const mockFileName = 'prepackaged_timelines.ndjson'; beforeEach(async () => { - securitySetup = { - authc: { - getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), - }, - authz: {}, - } as unknown as SecurityPluginSetup; - clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); jest.doMock('./helpers', () => { @@ -56,7 +46,6 @@ describe('installPrepackagedTimelines', () => { const request = addPrepackagedRulesRequest(); frameworkRequest = await buildFrameworkRequest( requestContextMock.convertContext(context), - securitySetup, request ); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/index.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/index.test.ts index 17086ca5a317d..9513bd49b4e6d 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/index.test.ts @@ -5,8 +5,6 @@ * 2.0. */ -import type { SecurityPluginSetup } from '@kbn/security-plugin/server'; - import { serverMock, requestContextMock, @@ -14,7 +12,6 @@ import { } from '../../../../detection_engine/routes/__mocks__'; import { - mockGetCurrentUser, mockCheckTimelinesStatusBeforeInstallResult, mockCheckTimelinesStatusAfterInstallResult, } from '../../../__mocks__/import_timelines'; @@ -39,7 +36,6 @@ jest.mock('../../../utils/check_timelines_status', () => { describe('installPrepackagedTimelines', () => { let server: ReturnType; - let securitySetup: SecurityPluginSetup; let { context } = requestContextMock.createTools(); beforeEach(() => { @@ -49,14 +45,7 @@ describe('installPrepackagedTimelines', () => { server = serverMock.create(); context = requestContextMock.createTools().context; - securitySetup = { - authc: { - getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), - }, - authz: {}, - } as unknown as SecurityPluginSetup; - - installPrepackedTimelinesRoute(server.router, createMockConfig(), securitySetup); + installPrepackedTimelinesRoute(server.router, createMockConfig()); }); test('should call installPrepackagedTimelines ', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/index.ts index 88d868968e9bb..0b150b4e47f56 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/index.ts @@ -12,7 +12,6 @@ import type { SecuritySolutionPluginRouter } from '../../../../../types'; import { TIMELINE_PREPACKAGED_URL } from '../../../../../../common/constants'; -import type { SetupPlugins } from '../../../../../plugin'; import type { ConfigType } from '../../../../../config'; import { buildSiemResponse } from '../../../../detection_engine/routes/utils'; @@ -27,8 +26,7 @@ export { installPrepackagedTimelines } from './helpers'; export const installPrepackedTimelinesRoute = ( router: SecuritySolutionPluginRouter, - config: ConfigType, - security: SetupPlugins['security'] + config: ConfigType ) => { router.versioned .post({ @@ -49,7 +47,7 @@ export const installPrepackedTimelinesRoute = ( }, async (context, request, response) => { try { - const frameworkRequest = await buildFrameworkRequest(context, security, request); + const frameworkRequest = await buildFrameworkRequest(context, request); const prepackagedTimelineStatus = await checkTimelinesStatus(frameworkRequest); const [validatedprepackagedTimelineStatus, prepackagedTimelineStatusError] = validate( prepackagedTimelineStatus, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/copy_timeline/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/copy_timeline/index.ts index 0e288e0099596..5bdb4e0f93d67 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/copy_timeline/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/copy_timeline/index.ts @@ -11,17 +11,12 @@ import type { ConfigType } from '../../../../..'; import { copyTimelineSchema } from '../../../../../../common/api/timeline'; import { copyTimeline } from '../../../saved_object/timelines'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; -import type { SetupPlugins } from '../../../../../plugin'; import { TIMELINE_COPY_URL } from '../../../../../../common/constants'; import { buildSiemResponse } from '../../../../detection_engine/routes/utils'; import { buildFrameworkRequest } from '../../../utils/common'; -export const copyTimelineRoute = ( - router: SecuritySolutionPluginRouter, - _: ConfigType, - security: SetupPlugins['security'] -) => { +export const copyTimelineRoute = (router: SecuritySolutionPluginRouter, _: ConfigType) => { router.versioned .post({ path: TIMELINE_COPY_URL, @@ -41,7 +36,7 @@ export const copyTimelineRoute = ( const siemResponse = buildSiemResponse(response); try { - const frameworkRequest = await buildFrameworkRequest(context, security, request); + const frameworkRequest = await buildFrameworkRequest(context, request); const { timeline, timelineIdToCopy } = request.body; const copiedTimeline = await copyTimeline(frameworkRequest, timeline, timelineIdToCopy); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/helpers.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/helpers.test.ts index d444983e7b00a..9799d2c6bdf3b 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/helpers.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/helpers.test.ts @@ -12,7 +12,6 @@ import type { FrameworkRequest } from '../../../../framework'; import type { SavedTimeline, Note } from '../../../../../../common/api/timeline'; import { mockTemplate, mockTimeline } from '../../../__mocks__/create_timelines'; import { buildFrameworkRequest } from '../../../utils/common'; -import type { SecurityPluginSetup } from '@kbn/security-plugin/server'; import { requestContextMock } from '../../../../detection_engine/routes/__mocks__'; import { getCreateTimelinesRequest, @@ -62,23 +61,14 @@ jest.mock('../../../saved_object/notes/persist_notes', () => ({ })); describe('createTimelines', () => { - let securitySetup: SecurityPluginSetup; let frameworkRequest: FrameworkRequest; beforeAll(async () => { - securitySetup = { - authc: { - getCurrentUser: jest.fn(), - }, - authz: {}, - } as unknown as SecurityPluginSetup; - const { context } = requestContextMock.createTools(); const mockRequest = getCreateTimelinesRequest(createTimelineWithoutTimelineId); frameworkRequest = await buildFrameworkRequest( requestContextMock.convertContext(context), - securitySetup, mockRequest ); Date.now = jest.fn().mockReturnValue(new Date('2020-11-04T11:37:31.655Z')); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/index.ts index 91b191c6ead0f..0fcf286661b96 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/index.ts @@ -12,7 +12,6 @@ import type { SecuritySolutionPluginRouter } from '../../../../../types'; import { TIMELINE_URL } from '../../../../../../common/constants'; import type { ConfigType } from '../../../../..'; -import type { SetupPlugins } from '../../../../../plugin'; import { buildRouteValidationWithExcess } from '../../../../../utils/build_validation/route_validation'; import { buildSiemResponse } from '../../../../detection_engine/routes/utils'; @@ -29,11 +28,7 @@ import type { CreateTimelinesResponse } from '../../../../../../common/api/timel export * from './helpers'; -export const createTimelinesRoute = ( - router: SecuritySolutionPluginRouter, - _: ConfigType, - security: SetupPlugins['security'] -) => { +export const createTimelinesRoute = (router: SecuritySolutionPluginRouter, _: ConfigType) => { router.versioned .post({ path: TIMELINE_URL, @@ -55,7 +50,7 @@ export const createTimelinesRoute = ( const siemResponse = buildSiemResponse(response); try { - const frameworkRequest = await buildFrameworkRequest(context, security, request); + const frameworkRequest = await buildFrameworkRequest(context, request); const { timelineId, timeline, version } = request.body; const { templateTimelineId, templateTimelineVersion, timelineType, title, status } = diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/delete_timelines/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/delete_timelines/index.ts index 602d29ae061ab..7f6339ee25929 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/delete_timelines/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/delete_timelines/index.ts @@ -10,18 +10,13 @@ import { buildRouteValidationWithExcess } from '../../../../../utils/build_valid import type { ConfigType } from '../../../../..'; import { deleteTimelinesSchema } from '../../../../../../common/api/timeline'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; -import type { SetupPlugins } from '../../../../../plugin'; import { TIMELINE_URL } from '../../../../../../common/constants'; import { buildSiemResponse } from '../../../../detection_engine/routes/utils'; import { buildFrameworkRequest } from '../../../utils/common'; import { deleteTimeline } from '../../../saved_object/timelines'; -export const deleteTimelinesRoute = ( - router: SecuritySolutionPluginRouter, - config: ConfigType, - security: SetupPlugins['security'] -) => { +export const deleteTimelinesRoute = (router: SecuritySolutionPluginRouter, config: ConfigType) => { router.versioned .delete({ path: TIMELINE_URL, @@ -41,7 +36,7 @@ export const deleteTimelinesRoute = ( const siemResponse = buildSiemResponse(response); try { - const frameworkRequest = await buildFrameworkRequest(context, security, request); + const frameworkRequest = await buildFrameworkRequest(context, request); const { savedObjectIds, searchIds } = request.body; await deleteTimeline(frameworkRequest, savedObjectIds, searchIds); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.test.ts index 044fc77caef80..13b6e0f68fefc 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.test.ts @@ -23,8 +23,6 @@ import { TIMELINE_EXPORT_URL } from '../../../../../../common/constants'; import { convertSavedObjectToSavedNote } from '../../../saved_object/notes/saved_object'; import { convertSavedObjectToSavedPinnedEvent } from '../../../saved_object/pinned_events'; import { convertSavedObjectToSavedTimeline } from '../../../saved_object/timelines/convert_saved_object_to_savedtimeline'; -import { mockGetCurrentUser } from '../../../__mocks__/import_timelines'; -import type { SecurityPluginSetup } from '@kbn/security-plugin/server'; jest.mock('../../../saved_object/timelines/convert_saved_object_to_savedtimeline', () => { return { @@ -48,17 +46,10 @@ jest.mock('../../../saved_object/pinned_events', () => { describe('export timelines', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); - let securitySetup: SecurityPluginSetup; beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); - securitySetup = { - authc: { - getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), - }, - authz: {}, - } as unknown as SecurityPluginSetup; clients.savedObjectsClient.bulkGet.mockResolvedValue(mockTimelinesSavedObjects()); (convertSavedObjectToSavedTimeline as unknown as jest.Mock).mockReturnValue(mockTimelines()); @@ -66,7 +57,7 @@ describe('export timelines', () => { (convertSavedObjectToSavedPinnedEvent as unknown as jest.Mock).mockReturnValue( mockPinnedEvents() ); - exportTimelinesRoute(server.router, createMockConfig(), securitySetup); + exportTimelinesRoute(server.router, createMockConfig()); }); describe('status codes', () => { diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.ts index 6f02c312b7bc7..7af6b7be0cdd0 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.ts @@ -17,17 +17,12 @@ import { } from '../../../../../../common/api/timeline'; import { buildRouteValidationWithExcess } from '../../../../../utils/build_validation/route_validation'; import { buildFrameworkRequest } from '../../../utils/common'; -import type { SetupPlugins } from '../../../../../plugin'; import { getExportTimelineByObjectIds } from './helpers'; export * from './helpers'; -export const exportTimelinesRoute = ( - router: SecuritySolutionPluginRouter, - config: ConfigType, - security: SetupPlugins['security'] -) => { +export const exportTimelinesRoute = (router: SecuritySolutionPluginRouter, config: ConfigType) => { router.versioned .post({ path: TIMELINE_EXPORT_URL, @@ -49,7 +44,7 @@ export const exportTimelinesRoute = ( async (context, request, response) => { try { const siemResponse = buildSiemResponse(response); - const frameworkRequest = await buildFrameworkRequest(context, security, request); + const frameworkRequest = await buildFrameworkRequest(context, request); const exportSizeLimit = config.maxTimelineImportExportSize; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timeline/index.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timeline/index.test.ts index d439bdd300c4b..78df97fa441be 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timeline/index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timeline/index.test.ts @@ -5,8 +5,6 @@ * 2.0. */ -import type { SecurityPluginSetup } from '@kbn/security-plugin/server'; - import { serverMock, requestContextMock, @@ -14,7 +12,6 @@ import { } from '../../../../detection_engine/routes/__mocks__'; import { getTimelineOrNull, getTimelineTemplateOrNull } from '../../../saved_object/timelines'; -import { mockGetCurrentUser } from '../../../__mocks__/import_timelines'; import { getTimelineRequest } from '../../../__mocks__/request_responses'; import { getTimelineRoute } from '.'; @@ -27,7 +24,6 @@ jest.mock('../../../saved_object/timelines', () => ({ describe('get timeline', () => { let server: ReturnType; - let securitySetup: SecurityPluginSetup; let { context } = requestContextMock.createTools(); beforeEach(() => { @@ -37,14 +33,7 @@ describe('get timeline', () => { server = serverMock.create(); context = requestContextMock.createTools().context; - securitySetup = { - authc: { - getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), - }, - authz: {}, - } as unknown as SecurityPluginSetup; - - getTimelineRoute(server.router, createMockConfig(), securitySetup); + getTimelineRoute(server.router, createMockConfig()); }); test('should call getTimelineTemplateOrNull if templateTimelineId is given', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timeline/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timeline/index.ts index 25cf3c895f291..321e6aafe4903 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timeline/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timeline/index.ts @@ -11,7 +11,6 @@ import type { SecuritySolutionPluginRouter } from '../../../../../types'; import { TIMELINE_URL } from '../../../../../../common/constants'; import type { ConfigType } from '../../../../..'; -import type { SetupPlugins } from '../../../../../plugin'; import { buildRouteValidationWithExcess } from '../../../../../utils/build_validation/route_validation'; import { buildSiemResponse } from '../../../../detection_engine/routes/utils'; @@ -24,11 +23,7 @@ import type { ResolvedTimelineWithOutcomeSavedObject, } from '../../../../../../common/api/timeline'; -export const getTimelineRoute = ( - router: SecuritySolutionPluginRouter, - _: ConfigType, - security: SetupPlugins['security'] -) => { +export const getTimelineRoute = (router: SecuritySolutionPluginRouter, _: ConfigType) => { router.versioned .get({ path: TIMELINE_URL, @@ -46,7 +41,7 @@ export const getTimelineRoute = ( }, async (context, request, response) => { try { - const frameworkRequest = await buildFrameworkRequest(context, security, request); + const frameworkRequest = await buildFrameworkRequest(context, request); const query = request.query ?? {}; const { template_timeline_id: templateTimelineId, id } = query; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timelines/index.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timelines/index.test.ts index 012427846f35e..b006b77c9a595 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timelines/index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timelines/index.test.ts @@ -5,18 +5,13 @@ * 2.0. */ -import type { SecurityPluginSetup } from '@kbn/security-plugin/server'; - import { serverMock, requestContextMock, createMockConfig, } from '../../../../detection_engine/routes/__mocks__'; import { getAllTimeline } from '../../../saved_object/timelines'; - -import { mockGetCurrentUser } from '../../../__mocks__/import_timelines'; import { getTimelineRequest } from '../../../__mocks__/request_responses'; - import { getTimelinesRoute } from '.'; jest.mock('../../../saved_object/timelines', () => ({ @@ -25,7 +20,6 @@ jest.mock('../../../saved_object/timelines', () => ({ describe('get all timelines', () => { let server: ReturnType; - let securitySetup: SecurityPluginSetup; let { context } = requestContextMock.createTools(); beforeEach(() => { @@ -35,14 +29,7 @@ describe('get all timelines', () => { server = serverMock.create(); context = requestContextMock.createTools().context; - securitySetup = { - authc: { - getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), - }, - authz: {}, - } as unknown as SecurityPluginSetup; - - getTimelinesRoute(server.router, createMockConfig(), securitySetup); + getTimelinesRoute(server.router, createMockConfig()); }); test('should get the total count', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timelines/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timelines/index.ts index 02cb8ff41f1a3..97d9bf51a0876 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timelines/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timelines/index.ts @@ -14,7 +14,6 @@ import type { SecuritySolutionPluginRouter } from '../../../../../types'; import { TIMELINES_URL } from '../../../../../../common/constants'; import type { ConfigType } from '../../../../..'; -import type { SetupPlugins } from '../../../../../plugin'; import { buildSiemResponse } from '../../../../detection_engine/routes/utils'; @@ -23,11 +22,7 @@ import { buildFrameworkRequest, escapeHatch, throwErrors } from '../../../utils/ import { getAllTimeline } from '../../../saved_object/timelines'; import { getTimelinesQuerySchema } from '../../../../../../common/api/timeline'; -export const getTimelinesRoute = ( - router: SecuritySolutionPluginRouter, - _: ConfigType, - security: SetupPlugins['security'] -) => { +export const getTimelinesRoute = (router: SecuritySolutionPluginRouter, _: ConfigType) => { router.versioned .get({ path: TIMELINES_URL, @@ -47,7 +42,7 @@ export const getTimelinesRoute = ( const customHttpRequestError = (message: string) => new CustomHttpRequestError(message, 400); try { - const frameworkRequest = await buildFrameworkRequest(context, security, request); + const frameworkRequest = await buildFrameworkRequest(context, request); const queryParams = pipe( getTimelinesQuerySchema.decode(request.query), fold(throwErrors(customHttpRequestError), identity) diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/index.ts index 8d6326af6c17c..be5ebc44e7faa 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/index.ts @@ -13,7 +13,6 @@ import type { SecuritySolutionPluginRouter } from '../../../../../types'; import { TIMELINE_IMPORT_URL } from '../../../../../../common/constants'; -import type { SetupPlugins } from '../../../../../plugin'; import type { ConfigType } from '../../../../../config'; import { buildRouteValidationWithExcess } from '../../../../../utils/build_validation/route_validation'; import { buildSiemResponse } from '../../../../detection_engine/routes/utils'; @@ -24,11 +23,7 @@ import { buildFrameworkRequest } from '../../../utils/common'; export { importTimelines } from './helpers'; -export const importTimelinesRoute = ( - router: SecuritySolutionPluginRouter, - config: ConfigType, - security: SetupPlugins['security'] -) => { +export const importTimelinesRoute = (router: SecuritySolutionPluginRouter, config: ConfigType) => { router.versioned .post({ path: `${TIMELINE_IMPORT_URL}`, @@ -66,7 +61,7 @@ export const importTimelinesRoute = ( body: `Invalid file extension ${fileExtension}`, }); } - const frameworkRequest = await buildFrameworkRequest(context, security, request); + const frameworkRequest = await buildFrameworkRequest(context, request); const res = await importTimelines( file as unknown as Readable, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/patch_timelines/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/patch_timelines/index.ts index c8b9c26536444..1c3cba9fdcb91 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/patch_timelines/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/patch_timelines/index.ts @@ -11,7 +11,6 @@ import type { SecuritySolutionPluginRouter } from '../../../../../types'; import { TIMELINE_URL } from '../../../../../../common/constants'; -import type { SetupPlugins } from '../../../../../plugin'; import { buildRouteValidationWithExcess } from '../../../../../utils/build_validation/route_validation'; import type { ConfigType } from '../../../../..'; @@ -23,11 +22,7 @@ import { createTimelines } from '../create_timelines'; import { CompareTimelinesStatus } from '../../../utils/compare_timelines_status'; import type { PatchTimelinesResponse } from '../../../../../../common/api/timeline'; -export const patchTimelinesRoute = ( - router: SecuritySolutionPluginRouter, - _: ConfigType, - security: SetupPlugins['security'] -) => { +export const patchTimelinesRoute = (router: SecuritySolutionPluginRouter, _: ConfigType) => { router.versioned .patch({ path: TIMELINE_URL, @@ -47,7 +42,7 @@ export const patchTimelinesRoute = ( const siemResponse = buildSiemResponse(response); try { - const frameworkRequest = await buildFrameworkRequest(context, security, request); + const frameworkRequest = await buildFrameworkRequest(context, request); const { timelineId, timeline, version } = request.body; const { templateTimelineId, templateTimelineVersion, timelineType, title, status } = timeline; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/persist_favorite/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/persist_favorite/index.ts index 746987caa3759..3b220ccf57e20 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/persist_favorite/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/persist_favorite/index.ts @@ -10,7 +10,6 @@ import type { SecuritySolutionPluginRouter } from '../../../../../types'; import { TIMELINE_FAVORITE_URL } from '../../../../../../common/constants'; -import type { SetupPlugins } from '../../../../../plugin'; import { buildRouteValidationWithExcess } from '../../../../../utils/build_validation/route_validation'; import type { ConfigType } from '../../../../..'; @@ -20,11 +19,7 @@ import { buildFrameworkRequest } from '../../../utils/common'; import { persistFavorite } from '../../../saved_object/timelines'; import { TimelineType, persistFavoriteSchema } from '../../../../../../common/api/timeline'; -export const persistFavoriteRoute = ( - router: SecuritySolutionPluginRouter, - _: ConfigType, - security: SetupPlugins['security'] -) => { +export const persistFavoriteRoute = (router: SecuritySolutionPluginRouter, _: ConfigType) => { router.versioned .patch({ path: TIMELINE_FAVORITE_URL, @@ -44,7 +39,7 @@ export const persistFavoriteRoute = ( const siemResponse = buildSiemResponse(response); try { - const frameworkRequest = await buildFrameworkRequest(context, security, request); + const frameworkRequest = await buildFrameworkRequest(context, request); const { timelineId, templateTimelineId, templateTimelineVersion, timelineType } = request.body; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/resolve_timeline/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/resolve_timeline/index.ts index 7572e72cfb9ac..09549f9b9034f 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/resolve_timeline/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/resolve_timeline/index.ts @@ -11,7 +11,6 @@ import type { SecuritySolutionPluginRouter } from '../../../../../types'; import { TIMELINE_RESOLVE_URL } from '../../../../../../common/constants'; import type { ConfigType } from '../../../../..'; -import type { SetupPlugins } from '../../../../../plugin'; import { buildRouteValidationWithExcess } from '../../../../../utils/build_validation/route_validation'; import { buildSiemResponse } from '../../../../detection_engine/routes/utils'; @@ -24,11 +23,7 @@ import type { ResolvedTimelineWithOutcomeSavedObject, } from '../../../../../../common/api/timeline'; -export const resolveTimelineRoute = ( - router: SecuritySolutionPluginRouter, - _: ConfigType, - security: SetupPlugins['security'] -) => { +export const resolveTimelineRoute = (router: SecuritySolutionPluginRouter, _: ConfigType) => { router.versioned .get({ path: TIMELINE_RESOLVE_URL, @@ -46,7 +41,7 @@ export const resolveTimelineRoute = ( }, async (context, request, response) => { try { - const frameworkRequest = await buildFrameworkRequest(context, security, request); + const frameworkRequest = await buildFrameworkRequest(context, request); const query = request.query ?? {}; const { template_timeline_id: templateTimelineId, id } = query; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts b/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts index b9f1aaafbf969..cfa804b848fcd 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts @@ -14,17 +14,16 @@ import { schema } from '@kbn/config-schema'; import type { KibanaRequest, RequestHandlerContext } from '@kbn/core/server'; import { formatErrors } from '@kbn/securitysolution-io-ts-utils'; -import type { SetupPlugins, StartPlugins } from '../../../plugin'; import type { FrameworkRequest } from '../../framework'; export const buildFrameworkRequest = async ( context: RequestHandlerContext, - security: StartPlugins['security'] | SetupPlugins['security'] | undefined, request: KibanaRequest ): Promise => { - const savedObjectsClient = (await context.core).savedObjects.client; - const user = await security?.authc.getCurrentUser(request); + const coreContext = await context.core; + const savedObjectsClient = coreContext.savedObjects.client; + const user = coreContext.security.authc.getCurrentUser(); return set( 'user', diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 5b5b833dd2d4b..be4d5ae275d83 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -643,7 +643,7 @@ export class Plugin implements ISecuritySolutionPlugin { logger ), endpointFleetServicesFactory, - security: plugins.security, + security: core.security, alerting: plugins.alerting, config, cases: plugins.cases, diff --git a/x-pack/plugins/security_solution/server/request_context_factory.ts b/x-pack/plugins/security_solution/server/request_context_factory.ts index 1f36f7ecff234..00db7dcb2c3e8 100644 --- a/x-pack/plugins/security_solution/server/request_context_factory.ts +++ b/x-pack/plugins/security_solution/server/request_context_factory.ts @@ -65,8 +65,8 @@ export class RequestContextFactory implements IRequestContextFactory { const { lists, ruleRegistry, security } = plugins; - const [, startPlugins] = await core.getStartServices(); - const frameworkRequest = await buildFrameworkRequest(context, security, request); + const [_, startPlugins] = await core.getStartServices(); + const frameworkRequest = await buildFrameworkRequest(context, request); const coreContext = await context.core; const licensing = await context.licensing; @@ -122,10 +122,11 @@ export class RequestContextFactory implements IRequestContextFactory { savedObjectsClient: coreContext.savedObjects.client, }); - return createDetectionRulesClient( - startPlugins.alerting.getRulesClientWithRequest(request), - mlAuthz - ); + return createDetectionRulesClient({ + rulesClient: startPlugins.alerting.getRulesClientWithRequest(request), + savedObjectsClient: coreContext.savedObjects.client, + mlAuthz, + }); }), getDetectionEngineHealthClient: memoize(() => @@ -148,7 +149,7 @@ export class RequestContextFactory implements IRequestContextFactory { return null; } - const username = security?.authc.getCurrentUser(request)?.username || 'elastic'; + const username = coreContext.security.authc.getCurrentUser()?.username || 'elastic'; return lists.getExceptionListClient(coreContext.savedObjects.client, username); }, diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index bc1a26534cd6c..6f245bd04a02b 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -80,7 +80,7 @@ export const initRoutes = ( ) => { registerFleetIntegrationsRoutes(router); registerLegacyRuleActionsRoutes(router, logger); - registerPrebuiltRulesRoutes(router, security); + registerPrebuiltRulesRoutes(router); registerRuleExceptionsRoutes(router); registerManageExceptionsRoutes(router); registerRuleManagementRoutes(router, config, ml, logger); @@ -99,19 +99,19 @@ export const initRoutes = ( registerResolverRoutes(router, getStartServices, config); - registerTimelineRoutes(router, config, security); + registerTimelineRoutes(router, config); // Detection Engine Signals routes that have the REST endpoints of /api/detection_engine/signals // POST /api/detection_engine/signals/status // Example usage can be found in security_solution/server/lib/detection_engine/scripts/signals - setSignalsStatusRoute(router, logger, telemetrySender, getStartServices); + setSignalsStatusRoute(router, logger, telemetrySender); setAlertTagsRoute(router); setAlertAssigneesRoute(router); querySignalsRoute(router, ruleDataClient); getSignalsMigrationStatusRoute(router); - createSignalsMigrationRoute(router, security); - finalizeSignalsMigrationRoute(router, ruleDataService, security); - deleteSignalsMigrationRoute(router, security); + createSignalsMigrationRoute(router); + finalizeSignalsMigrationRoute(router, ruleDataService); + deleteSignalsMigrationRoute(router); suggestUserProfilesRoute(router, getStartServices); // Detection Engine index routes that have the REST endpoints of /api/detection_engine/index @@ -130,14 +130,14 @@ export const initRoutes = ( createStoredScriptRoute(router, logger); deleteStoredScriptRoute(router); readPrebuiltDevToolContentRoute(router); - createPrebuiltSavedObjectsRoute(router, logger, security); - deletePrebuiltSavedObjectsRoute(router, security); + createPrebuiltSavedObjectsRoute(router, logger); + deletePrebuiltSavedObjectsRoute(router); getRiskScoreIndexStatusRoute(router); - installRiskScoresRoute(router, logger, security); + installRiskScoresRoute(router, logger); // Dashboards - registerDashboardsRoutes(router, logger, security); - registerTagsRoutes(router, logger, security); + registerDashboardsRoutes(router, logger); + registerTagsRoutes(router, logger); const { previewTelemetryUrlEnabled } = config.experimentalFeatures; if (previewTelemetryUrlEnabled) { // telemetry preview endpoint for e2e integration tests only at the moment. diff --git a/x-pack/plugins/stack_alerts/kibana.jsonc b/x-pack/plugins/stack_alerts/kibana.jsonc index 9f2f33abf1f6e..4d000228b0e07 100644 --- a/x-pack/plugins/stack_alerts/kibana.jsonc +++ b/x-pack/plugins/stack_alerts/kibana.jsonc @@ -22,7 +22,7 @@ ], "requiredBundles": [ "esUiShared", - "textBasedLanguages" + "esql" ], "extraPublicDirs": ["common"] } diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/esql_query_expression.tsx b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/esql_query_expression.tsx index 3ff2b70522e9a..e9ceabf4639b2 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/esql_query_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/esql_query_expression.tsx @@ -17,7 +17,7 @@ import { EuiSpacer, } from '@elastic/eui'; import { getFields, RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public'; -import { TextBasedLangEditor } from '@kbn/text-based-languages/public'; +import { TextBasedLangEditor } from '@kbn/esql/public'; import { fetchFieldsFromESQL } from '@kbn/text-based-editor'; import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils'; import type { AggregateQuery } from '@kbn/es-query'; diff --git a/x-pack/plugins/stack_alerts/tsconfig.json b/x-pack/plugins/stack_alerts/tsconfig.json index 081b0588d39aa..f187bf466e375 100644 --- a/x-pack/plugins/stack_alerts/tsconfig.json +++ b/x-pack/plugins/stack_alerts/tsconfig.json @@ -38,7 +38,7 @@ "@kbn/discover-plugin", "@kbn/rule-data-utils", "@kbn/alerts-as-data-utils", - "@kbn/text-based-languages", + "@kbn/esql", "@kbn/text-based-editor", "@kbn/expressions-plugin", "@kbn/core-http-browser", diff --git a/x-pack/plugins/stack_connectors/common/sentinelone/schema.ts b/x-pack/plugins/stack_connectors/common/sentinelone/schema.ts index 66a50a42dc419..80f86a5c05ad8 100644 --- a/x-pack/plugins/stack_connectors/common/sentinelone/schema.ts +++ b/x-pack/plugins/stack_connectors/common/sentinelone/schema.ts @@ -162,7 +162,10 @@ export const SentinelOneIsolateHostResponseSchema = schema.object({ export const SentinelOneGetRemoteScriptsParamsSchema = schema.object({ query: schema.nullable(schema.string()), + // Possible values (multiples comma delimiter): `linux` or `macos` or `windows` osTypes: schema.nullable(schema.string()), + // possible values (multiples comma delimiter): `action` or `artifactCollection` or `dataCollection` + scriptType: schema.nullable(schema.string()), }); export const SentinelOneFetchAgentFilesParamsSchema = schema.object({ diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index a3ef8accb2b32..5cbe8f43a1322 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -2525,8 +2525,8 @@ "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", - "textBasedLanguages.advancedSettings.enableESQL.discussLinkText": "discuss.elastic.co/c/elastic-stack/kibana", - "textBasedLanguages.advancedSettings.enableESQLTitle": "Activer ES|QL", + "esql.advancedSettings.enableESQL.discussLinkText": "discuss.elastic.co/c/elastic-stack/kibana", + "esql.advancedSettings.enableESQLTitle": "Activer ES|QL", "domDragDrop.announce.cancelled": "Mouvement annulé. {label} revenu à sa position initiale", "domDragDrop.announce.cancelledItem": "Mouvement annulé. {label} revenu au groupe {groupLabel} à la position {position}", "domDragDrop.announce.dropped.combineCompatible": "Combinaisons de {label} dans le {groupLabel} vers {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition} dans le calque {dropLayerNumber}", @@ -13310,7 +13310,6 @@ "xpack.elasticAssistant.assistant.settings.knowledgeBasedSettings.knowledgeBaseDescription": "Pour commencer, configurez ELSER dans {machineLearning}. {seeDocs}", "xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.knowledgeBaseInstalledDescription": "Initialisé sur `{kbIndexPattern}`", "xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.latestAndRiskiestOpenAlertsLabel": "Envoyez à l'Assistant d'IA des informations sur vos {alertsCount} alertes ouvertes ou confirmées les plus récentes et les plus risquées.", - "xpack.elasticAssistant.assistant.technicalPreview.tooltipContent": "Les réponses des systèmes d'IA ne sont pas toujours tout à fait exactes. Pour en savoir plus sur la fonctionnalité d'assistant et son utilisation, consultez {documentationLink}.", "xpack.elasticAssistant.dataAnonymizationEditor.contextEditor.selectAllFields": "Sélectionnez l'ensemble des {totalFields} champs", "xpack.elasticAssistant.dataAnonymizationEditor.contextEditor.selectedFields": "{selected} champs sélectionnés", "xpack.elasticAssistant.dataAnonymizationEditor.stats.allowedStat.allowedTooltip": "{allowed} champs sur {total} dans ce contexte sont autorisés à être inclus dans la conversation", @@ -13511,9 +13510,6 @@ "xpack.elasticAssistant.knowledgeBase.setupError": "Erreur lors de la configuration de la base de connaissances", "xpack.elasticAssistant.knowledgeBase.statusError": "Erreur lors de la récupération du statut de la base de connaissances", "xpack.elasticAssistant.securityAssistant.content.prompts.welcome.enterprisePrompt": "L'assistant d'IA d'Elastic n'est accessible qu'aux entreprises. Veuillez mettre votre licence à niveau pour bénéficier de cette fonctionnalité.", - "xpack.elasticAssistant.securityAssistant.content.prompts.welcome.welcomeGeneral2Prompt": "Avant toute chose, il faut configurer un Connecteur d'intelligence artificielle générative pour lancer cette expérience de chat ! Avec le connecteur d'IA générative, vous pourrez configurer l'accès à un service OpenAI ou à un service Amazon Bedrock, mais sachez que vous serez en mesure de déployer vos propres modèles au sein d'une instance Elastic Cloud et de les y utiliser à l'avenir... 😉", - "xpack.elasticAssistant.securityAssistant.content.prompts.welcome.welcomeGeneral3Prompt": "Cliquez sur le bouton \"Ajouter un connecteur\" ci-dessous pour continuer la conversation.", - "xpack.elasticAssistant.securityAssistant.content.prompts.welcome.welcomeGeneralPrompt": "Bienvenue sur votre assistant d’intelligence artificielle Elastic. Je suis votre portail 100 % open-code vers votre vie Elastic. Avec le temps, je serai capable de répondre à vos questions et de vous apporter mon aide concernant l’ensemble de vos informations contenues dans Elastic, et bien plus encore. En attendant, j’espère que cet aperçu anticipé vous donnera une idée de ce que nous pouvons créer en travaillant ensemble, en toute transparence. À bientôt !", "xpack.embeddableEnhanced.actions.panelNotifications.manyDrilldowns": "Le panneau comporte {count} recherches", "xpack.embeddableEnhanced.actions.panelNotifications.oneDrilldown": "Le panneau comporte 1 recherche", "xpack.embeddableEnhanced.Drilldowns": "Explorations", @@ -23425,7 +23421,7 @@ "xpack.lens.shared.axisNameLabel": "Titre de l'axe", "xpack.lens.shared.chartValueLabelVisibilityLabel": "Étiquettes", "xpack.lens.shared.chartValueLabelVisibilityTooltip": "Si l'espace est insuffisant, les étiquettes de valeurs pourront être masquées", - "xpack.lens.shared.curveLabel": "Options visuelles", + "xpack.lens.shared.visualOptionsLabel": "Options visuelles", "xpack.lens.shared.legendAlignmentLabel": "Alignement", "xpack.lens.shared.legendInsideColumnsLabel": "Nombre de colonnes", "xpack.lens.shared.legendInsideLocationAlignmentLabel": "Alignement", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 69c0d2396ec0e..a7dcd7e5416c6 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2522,8 +2522,8 @@ "unifiedDocViewer.sourceViewer.errorMessage": "現在データを取得できませんでした。タブを更新して、再試行してください。", "unifiedDocViewer.sourceViewer.errorMessageTitle": "エラーが発生しました", "unifiedDocViewer.sourceViewer.refresh": "更新", - "textBasedLanguages.advancedSettings.enableESQL.discussLinkText": "discuss.elastic.co/c/elastic-stack/kibana", - "textBasedLanguages.advancedSettings.enableESQLTitle": "ES|QLを有効化", + "esql.advancedSettings.enableESQL.discussLinkText": "discuss.elastic.co/c/elastic-stack/kibana", + "esql.advancedSettings.enableESQLTitle": "ES|QLを有効化", "domDragDrop.announce.cancelled": "移動がキャンセルされました。{label}は初期位置に戻りました", "domDragDrop.announce.cancelledItem": "移動がキャンセルされました。{label}は位置{position}の{groupLabel}グループに戻りました", "domDragDrop.announce.dropped.combineCompatible": "レイヤー{dropLayerNumber}の位置{dropPosition}で、グループ{groupLabel}の{label}をグループ{dropGroupLabel}の{dropLabel}と結合しました", @@ -13289,7 +13289,6 @@ "xpack.elasticAssistant.assistant.settings.knowledgeBasedSettings.knowledgeBaseDescription": "{machineLearning}内でELSERを構成して開始します。{seeDocs}", "xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.knowledgeBaseInstalledDescription": "`{kbIndexPattern}`に初期化されました", "xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.latestAndRiskiestOpenAlertsLabel": "{alertsCount}件の最新の最もリスクが高い未解決または確認済みのアラートに関する情報をAI Assistantに送信します。", - "xpack.elasticAssistant.assistant.technicalPreview.tooltipContent": "AIシステムからの応答は、必ずしも完全に正確であるとは限りません。アシスタント機能とその使用方法の詳細については、{documentationLink}を参照してください。", "xpack.elasticAssistant.dataAnonymizationEditor.contextEditor.selectAllFields": "すべての{totalFields}フィールドを選択", "xpack.elasticAssistant.dataAnonymizationEditor.contextEditor.selectedFields": "選択した{selected}フィールド", "xpack.elasticAssistant.dataAnonymizationEditor.stats.allowedStat.allowedTooltip": "このコンテキストの{total}フィールドのうち{allowed}個を会話に含めることができます", @@ -13490,9 +13489,6 @@ "xpack.elasticAssistant.knowledgeBase.setupError": "ナレッジベースの設定エラー", "xpack.elasticAssistant.knowledgeBase.statusError": "ナレッジベースステータスの取得エラー", "xpack.elasticAssistant.securityAssistant.content.prompts.welcome.enterprisePrompt": "Elastic AI Assistantはエンタープライズユーザーのみご利用いただけます。この機能を使用するには、ライセンスをアップグレードしてください。", - "xpack.elasticAssistant.securityAssistant.content.prompts.welcome.welcomeGeneral2Prompt": "まず最初に、このチャットエクスペリエンスを開始するために生成AIコネクターを設定する必要があります。生成AIコネクターを使用すると、OpenAI ServiceまたはAmazon Bedrockサービスへのアクセスを設定できます。しかし、将来的にはElastic Cloudインスタンス内に独自のモデルをデプロイして、それをここで使うことができるようになると考えてください... 😉", - "xpack.elasticAssistant.securityAssistant.content.prompts.welcome.welcomeGeneral3Prompt": "会話を続けるには、以下の[コネクターの追加]ボタンをクリックしてください。", - "xpack.elasticAssistant.securityAssistant.content.prompts.welcome.welcomeGeneralPrompt": "Elastic AI Assistantへようこそ!Elasticを活用するための100%オープンコードのポータルです。いずれは、Elasticにあるすべての情報、そしてもっともっと多くのことについて、質問に答えたり、サポートを提供したりできるようになるでしょう。それまでは、この早期プレビューが、オープンな場で協力するときに生み出せるものの可能性を知るきっかけになることを願っています。どうぞよろしくお願いいたします。", "xpack.embeddableEnhanced.actions.panelNotifications.manyDrilldowns": "パネルには{count}個のドリルダウンがあります", "xpack.embeddableEnhanced.actions.panelNotifications.oneDrilldown": "パネルには 1 個のドリルダウンがあります", "xpack.embeddableEnhanced.Drilldowns": "ドリルダウン", @@ -23406,7 +23402,7 @@ "xpack.lens.shared.axisNameLabel": "軸のタイトル", "xpack.lens.shared.chartValueLabelVisibilityLabel": "ラベル", "xpack.lens.shared.chartValueLabelVisibilityTooltip": "十分なスペースがない場合、値ラベルが非表示になることがあります。", - "xpack.lens.shared.curveLabel": "視覚オプション", + "xpack.lens.shared.visualOptionsLabel": "視覚オプション", "xpack.lens.shared.legendAlignmentLabel": "アラインメント", "xpack.lens.shared.legendInsideColumnsLabel": "列の数", "xpack.lens.shared.legendInsideLocationAlignmentLabel": "アラインメント", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 794c9258dc233..b6203a35743e3 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2526,8 +2526,8 @@ "unifiedDocViewer.sourceViewer.errorMessage": "当前无法获取数据。请刷新选项卡以重试。", "unifiedDocViewer.sourceViewer.errorMessageTitle": "发生错误", "unifiedDocViewer.sourceViewer.refresh": "刷新", - "textBasedLanguages.advancedSettings.enableESQL.discussLinkText": "discuss.elastic.co/c/elastic-stack/kibana", - "textBasedLanguages.advancedSettings.enableESQLTitle": "启用 ES|QL", + "esql.advancedSettings.enableESQL.discussLinkText": "discuss.elastic.co/c/elastic-stack/kibana", + "esql.advancedSettings.enableESQLTitle": "启用 ES|QL", "domDragDrop.announce.cancelled": "移动已取消。{label} 将返回至其初始位置", "domDragDrop.announce.cancelledItem": "移动已取消。{label} 返回至 {groupLabel} 组中的位置 {position}", "domDragDrop.announce.dropped.combineCompatible": "已将组 {groupLabel} 中的 {label} 组合到图层 {dropLayerNumber} 的组 {dropGroupLabel} 中的位置 {dropPosition} 上的 {dropLabel}", @@ -13315,7 +13315,6 @@ "xpack.elasticAssistant.assistant.settings.knowledgeBasedSettings.knowledgeBaseDescription": "在 {machineLearning} 中配置 ELSER 以开始。{seeDocs}", "xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.knowledgeBaseInstalledDescription": "已初始化为 `{kbIndexPattern}`", "xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.latestAndRiskiestOpenAlertsLabel": "发送有关 {alertsCount} 个最新和风险最高的未决或已确认告警的 AI 助手信息。", - "xpack.elasticAssistant.assistant.technicalPreview.tooltipContent": "来自 AI 系统的响应可能不会始终完全准确。有关辅助功能及其用法的详细信息,请参阅 {documentationLink}。", "xpack.elasticAssistant.dataAnonymizationEditor.contextEditor.selectAllFields": "选择所有 {totalFields} 个字段", "xpack.elasticAssistant.dataAnonymizationEditor.contextEditor.selectedFields": "已选定 {selected} 个字段", "xpack.elasticAssistant.dataAnonymizationEditor.stats.allowedStat.allowedTooltip": "允许在对话中包含此上下文中的 {allowed} 个(共 {total} 个)字段", @@ -13516,9 +13515,6 @@ "xpack.elasticAssistant.knowledgeBase.setupError": "设置知识库时出错", "xpack.elasticAssistant.knowledgeBase.statusError": "提取知识库状态时出错", "xpack.elasticAssistant.securityAssistant.content.prompts.welcome.enterprisePrompt": "Elastic AI 助手仅对企业用户可用。请升级许可证以使用此功能。", - "xpack.elasticAssistant.securityAssistant.content.prompts.welcome.welcomeGeneral2Prompt": "首先,我们需要设置生成式 AI 连接器以继续这种聊天体验!使用生成式 AI 连接器,您将能够配置 OpenAI 服务或 Amazon Bedrock 服务的访问权限,但请您相信,您将能够在 Elastic Cloud 实例中部署自己的模型,并于未来在此处使用那些模型……😉", - "xpack.elasticAssistant.securityAssistant.content.prompts.welcome.welcomeGeneral3Prompt": "接下来,单击下面的“添加连接器”按钮继续对话!", - "xpack.elasticAssistant.securityAssistant.content.prompts.welcome.welcomeGeneralPrompt": "欢迎使用 Elastic AI 助手!我是您的 100% 开放源代码门户,可帮助您熟练使用 Elastic。一段时间后,我将能够回答问题,并利用 Elastic 中的所有信息提供帮助等。到那时,我希望这个早期预览版本将为您打开思路,为我们的公开协作创造各种可能性。加油!", "xpack.embeddableEnhanced.actions.panelNotifications.manyDrilldowns": "面板有 {count} 个向下钻取", "xpack.embeddableEnhanced.actions.panelNotifications.oneDrilldown": "面板有 1 个向下钻取", "xpack.embeddableEnhanced.Drilldowns": "向下钻取", @@ -23439,7 +23435,7 @@ "xpack.lens.shared.axisNameLabel": "轴标题", "xpack.lens.shared.chartValueLabelVisibilityLabel": "标签", "xpack.lens.shared.chartValueLabelVisibilityTooltip": "如果没有足够的空间,可能会隐藏值标签", - "xpack.lens.shared.curveLabel": "视觉选项", + "xpack.lens.shared.visualOptionsLabel": "视觉选项", "xpack.lens.shared.legendAlignmentLabel": "对齐方式", "xpack.lens.shared.legendInsideColumnsLabel": "列数目", "xpack.lens.shared.legendInsideLocationAlignmentLabel": "对齐方式", diff --git a/x-pack/test/cloud_security_posture_functional/cloud_tests/basic_ui_sanity.ts b/x-pack/test/cloud_security_posture_functional/cloud_tests/basic_ui_sanity.ts deleted file mode 100644 index 495105017e5c2..0000000000000 --- a/x-pack/test/cloud_security_posture_functional/cloud_tests/basic_ui_sanity.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../ftr_provider_context'; - -// eslint-disable-next-line import/no-default-export -export default ({ getPageObjects, getService }: FtrProviderContext) => { - const retry = getService('retry'); - const pageObjects = getPageObjects(['common', 'cloudPostureDashboard', 'header']); - - describe('Cloud Posture Dashboard Page', function () { - this.tags(['cloud_security_posture_ui_sanity']); - let cspDashboard: typeof pageObjects.cloudPostureDashboard; - let dashboard: typeof pageObjects.cloudPostureDashboard.dashboard; - - before(async () => { - cspDashboard = pageObjects.cloudPostureDashboard; - dashboard = pageObjects.cloudPostureDashboard.dashboard; - await cspDashboard.waitForPluginInitialized(); - await cspDashboard.navigateToComplianceDashboardPage(); - await retry.waitFor( - 'Cloud posture integration dashboard to be displayed', - async () => !!dashboard.getIntegrationDashboardContainer() - ); - }); - - describe('Cloud Dashboard', () => { - it('displays compliance score greater than 40', async () => { - await pageObjects.header.waitUntilLoadingHasFinished(); - const scoreElement = await dashboard.getCloudComplianceScore(); - const score = parseInt((await scoreElement.getVisibleText()).replace('%', ''), 10); - expect(score).to.be.greaterThan(40); - }); - - it('displays all compliance scores', async () => { - const scoresElements = await dashboard.getAllCloudComplianceScores(); - const scores: string[] = []; - for (const scoreElement of scoresElements) { - scores.push(await scoreElement.getVisibleText()); - } - // 3 scores for each cloud provider + 1 summary score - expect(scores.length).to.be(4); - }); - - it('displays a number of resources evaluated greater than 3000', async () => { - const resourcesEvaluated = await dashboard.getCloudResourcesEvaluated(); - const visibleText = await resourcesEvaluated.getVisibleText(); - const resourcesEvaluatedCount = parseInt(visibleText.replace(/,/g, ''), 10); - expect(resourcesEvaluatedCount).greaterThan(3000); - }); - }); - - describe('Kubernetes Dashboard', () => { - it('displays compliance score greater than 80', async () => { - await pageObjects.header.waitUntilLoadingHasFinished(); - const scoreElement = await dashboard.getKubernetesComplianceScore(); - const score = parseInt((await scoreElement.getVisibleText()).replace('%', ''), 10); - expect(score).to.be.greaterThan(80); - }); - - it('displays a number of resources evaluated greater than 150', async () => { - const resourcesEvaluated = await dashboard.getKubernetesResourcesEvaluated(); - const resourcesEvaluatedCount = parseInt( - (await resourcesEvaluated.getVisibleText()).replace(/,/g, ''), - 10 - ); - expect(resourcesEvaluatedCount).greaterThan(150); - }); - }); - }); -}; diff --git a/x-pack/test/cloud_security_posture_functional/cloud_tests/benchmark_sanity.ts b/x-pack/test/cloud_security_posture_functional/cloud_tests/benchmark_sanity.ts new file mode 100644 index 0000000000000..277c1038e51ae --- /dev/null +++ b/x-pack/test/cloud_security_posture_functional/cloud_tests/benchmark_sanity.ts @@ -0,0 +1,50 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const pageObjects = getPageObjects(['common', 'benchmark']); + + describe('Benchmark Page - Sanity Tests', function () { + this.tags(['cloud_security_posture_ui_sanity']); + let benchmark: typeof pageObjects.benchmark; + + before(async () => { + benchmark = pageObjects.benchmark; + await benchmark.navigateToBenchnmarkPage(); + await benchmark.waitForPluginInitialized(); + }); + + it('Benchmark table exists', async () => { + expect(await benchmark.benchmarkPage.doesBenchmarkTableExists()); + }); + + it('Benchmarks count is more than 0', async () => { + const benchmarksRows = await benchmark.benchmarkPage.getBenchmarkTableRows(); + expect(benchmarksRows.length).to.be.greaterThan(0); + }); + + it('For each benchmark, evaluation and complience are not empty', async () => { + const benchmarksRows = await benchmark.benchmarkPage.getBenchmarkTableRows(); + for (const row of benchmarksRows) { + const benchmarkName = await benchmark.benchmarkPage.getCisNameCellData(row); + const evaluated = await benchmark.benchmarkPage.getEvaluatedCellData(row); + const compliance = await benchmark.benchmarkPage.getComplianceCellData(row); + expect(await evaluated).to.not.contain( + 'Add', + `The ${benchmarkName} does not have evaluated data` + ); + expect(await compliance).to.not.contain( + 'No', + `The ${benchmarkName} does not have compliance data` + ); + } + }); + }); +}; diff --git a/x-pack/test/cloud_security_posture_functional/cloud_tests/dashboard_sanity.ts b/x-pack/test/cloud_security_posture_functional/cloud_tests/dashboard_sanity.ts new file mode 100644 index 0000000000000..6eaafcb154134 --- /dev/null +++ b/x-pack/test/cloud_security_posture_functional/cloud_tests/dashboard_sanity.ts @@ -0,0 +1,139 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const retry = getService('retry'); + const pageObjects = getPageObjects(['common', 'cloudPostureDashboard', 'header', 'findings']); + + describe('Cloud Posture Dashboard Page - Sanity Tests', function () { + this.tags(['cloud_security_posture_ui_sanity']); + let cspDashboard: typeof pageObjects.cloudPostureDashboard; + let dashboard: typeof pageObjects.cloudPostureDashboard.dashboard; + let findings: typeof pageObjects.findings; + let TAB_TYPES: typeof pageObjects.cloudPostureDashboard.TAB_TYPES; + + before(async () => { + cspDashboard = pageObjects.cloudPostureDashboard; + dashboard = pageObjects.cloudPostureDashboard.dashboard; + findings = pageObjects.findings; + TAB_TYPES = pageObjects.cloudPostureDashboard.TAB_TYPES; + await cspDashboard.waitForPluginInitialized(); + await cspDashboard.navigateToComplianceDashboardPage(); + await retry.waitFor( + 'Cloud posture integration dashboard to be displayed', + async () => !!dashboard.getIntegrationDashboardContainer() + ); + }); + + describe('Cloud Dashboard', () => { + it('displays compliance score greater than 40', async () => { + await pageObjects.header.waitUntilLoadingHasFinished(); + const scoreElement = await dashboard.getCloudComplianceScore(); + const score = parseInt((await scoreElement.getVisibleText()).replace('%', ''), 10); + expect(score).to.be.greaterThan(40); + }); + + it('displays all compliance scores', async () => { + const scoresElements = await dashboard.getAllCloudComplianceScores(); + const scores: string[] = []; + for (const scoreElement of scoresElements) { + scores.push(await scoreElement.getVisibleText()); + } + // 3 scores for each cloud provider + 1 summary score + expect(scores.length).to.be(4); + }); + + it('displays a number of resources evaluated greater than 1500', async () => { + const resourcesEvaluated = await dashboard.getCloudResourcesEvaluated(); + const visibleText = await resourcesEvaluated.getVisibleText(); + const resourcesEvaluatedCount = parseInt(visibleText.replace(/,/g, ''), 10); + expect(resourcesEvaluatedCount).greaterThan(1500); + }); + + it('Compliance By CIS sections have non empty values', async () => { + const complianceScoresChartPanel = await dashboard.getAllComplianceScoresByCisSection( + TAB_TYPES.CLOUD + ); + expect(complianceScoresChartPanel.length).to.be.greaterThan(0); + for (const score of complianceScoresChartPanel) { + const scoreValue = await score.getVisibleText(); + // Check if the score is a percentage + expect(scoreValue).to.match(/^\d+%$/); + } + }); + + it('Navigation to Findings page', async () => { + const findingsLinkCount = await dashboard.getFindingsLinksCount(TAB_TYPES.CLOUD); + for (let i = 0; i < findingsLinkCount; i++) { + const link = await dashboard.getFindingsLinkAtIndex(TAB_TYPES.CLOUD, i); + // for (const link of findingsLink) { + await link.click(); + await pageObjects.header.waitUntilLoadingHasFinished(); + const groupSelector = await findings.groupSelector(); + await groupSelector.openDropDown(); + await groupSelector.setValue('None'); + expect( + await findings.createDataTableObject('latest_findings_table').getRowsCount() + ).to.be.greaterThan(0); + await cspDashboard.navigateToComplianceDashboardPage(); + await pageObjects.header.waitUntilLoadingHasFinished(); + } + }); + }); + + describe('Kubernetes Dashboard', () => { + it('displays compliance score greater than 80', async () => { + await pageObjects.header.waitUntilLoadingHasFinished(); + const scoreElement = await dashboard.getKubernetesComplianceScore(); + const score = parseInt((await scoreElement.getVisibleText()).replace('%', ''), 10); + expect(score).to.be.greaterThan(80); + }); + + it('displays a number of resources evaluated greater than 150', async () => { + const resourcesEvaluated = await dashboard.getKubernetesResourcesEvaluated(); + const resourcesEvaluatedCount = parseInt( + (await resourcesEvaluated.getVisibleText()).replace(/,/g, ''), + 10 + ); + expect(resourcesEvaluatedCount).greaterThan(150); + }); + + it('Compliance By CIS sections have non empty values', async () => { + const complianceScoresChartPanel = await dashboard.getAllComplianceScoresByCisSection( + 'Kubernetes' + ); + expect(complianceScoresChartPanel.length).to.be.greaterThan(0); + for (const score of complianceScoresChartPanel) { + const scoreValue = await score.getVisibleText(); + // Check if the score is a percentage + expect(scoreValue).to.match(/^\d+%$/); + } + }); + + it('Navigation to Findings page', async () => { + const findingsLinkCount = await dashboard.getFindingsLinksCount(TAB_TYPES.KUBERNETES); + for (let i = 0; i < findingsLinkCount; i++) { + const link = await dashboard.getFindingsLinkAtIndex(TAB_TYPES.KUBERNETES, i); + await link.click(); + await pageObjects.header.waitUntilLoadingHasFinished(); + const groupSelector = await findings.groupSelector(); + await groupSelector.openDropDown(); + await groupSelector.setValue('None'); + expect( + await findings.createDataTableObject('latest_findings_table').getRowsCount() + ).to.be.greaterThan(0); + await cspDashboard.navigateToComplianceDashboardPage(); + await pageObjects.header.waitUntilLoadingHasFinished(); + await dashboard.getKubernetesDashboard(); + } + }); + }); + }); +}; diff --git a/x-pack/test/cloud_security_posture_functional/cloud_tests/findings_sanity.ts b/x-pack/test/cloud_security_posture_functional/cloud_tests/findings_sanity.ts new file mode 100644 index 0000000000000..fd8c8a956b1d5 --- /dev/null +++ b/x-pack/test/cloud_security_posture_functional/cloud_tests/findings_sanity.ts @@ -0,0 +1,175 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const pageObjects = getPageObjects(['common', 'findings', 'header']); + const queryBar = getService('queryBar'); + const testSubjects = getService('testSubjects'); + + describe('Findings Page - Sanity Tests', function () { + this.tags(['cloud_security_posture_ui_sanity']); + let findings: typeof pageObjects.findings; + let latestFindingsTable: typeof findings.latestFindingsTable; + + before(async () => { + findings = pageObjects.findings; + latestFindingsTable = pageObjects.findings.latestFindingsTable; + await findings.navigateToLatestFindingsPage(); + await findings.waitForPluginInitialized(); + }); + + describe('Findings - Querying data', () => { + afterEach(async () => { + // Reset the group selector to None + const groupSelector = await findings.groupSelector(); + await groupSelector.openDropDown(); + await groupSelector.setValue('None'); + // Reset search query + await queryBar.clearQuery(); + await queryBar.submitQuery(); + }); + + const testCases = [ + { + searchQuery: + 'cloud.provider : "aws" and cloud.region : "eu-west-3" and result.evaluation : "failed" and rule.tags : "CIS 5.4"', + provider: 'aws', + expectedRowsCount: 3, + expectedGroupCount: '1 cloud account', + expectedUnitCount: '3 findings', + }, + { + searchQuery: + 'cloud.provider : "gcp" and rule.benchmark.rule_number : "3.1" and result.evaluation : "failed"', + provider: 'gcp', + expectedRowsCount: 1, + expectedGroupCount: '1 cloud account', + expectedUnitCount: '1 finding', + }, + { + searchQuery: + 'cloud.provider : "azure" and rule.benchmark.rule_number : "7.1" and result.evaluation : "failed"', + provider: 'azure', + expectedRowsCount: 1, + expectedGroupCount: '1 cloud account', + expectedUnitCount: '1 finding', + }, + { + searchQuery: + 'rule.benchmark.id : "cis_k8s" and rule.benchmark.rule_number : "4.2.4" and result.evaluation : "failed"', + provider: 'k8s', + expectedRowsCount: 2, + expectedGroupCount: '0 cloud accounts', + expectedUnitCount: '2 findings', + }, + { + searchQuery: 'rule.benchmark.id : "cis_eks" and rule.benchmark.rule_number : "3.1.1"', + provider: 'eks', + expectedRowsCount: 2, + expectedGroupCount: '0 cloud accounts', + expectedUnitCount: '2 findings', + }, + ]; + + testCases.forEach( + ({ searchQuery, provider, expectedRowsCount, expectedGroupCount, expectedUnitCount }) => { + it(`Querying ${provider} provider data`, async () => { + // Execute the query + await queryBar.setQuery(searchQuery); + await queryBar.submitQuery(); + // Get the number of rows in the data table + const rowsCount = await findings + .createDataTableObject('latest_findings_table') + .getRowsCount(); + + // Check that the number of rows matches the expected count + expect(rowsCount).to.be(expectedRowsCount); + const groupSelector = await findings.groupSelector(); + await groupSelector.openDropDown(); + await groupSelector.setValue('Cloud account'); + const grouping = await findings.findingsGrouping(); + // Check that the group count and unit count matches the expected values + const groupCount = await grouping.getGroupCount(); + expect(groupCount).to.be(expectedGroupCount); + + const unitCount = await grouping.getUnitCount(); + expect(unitCount).to.be(expectedUnitCount); + }); + } + ); + }); + + describe('Findings - Sorting data', () => { + afterEach(async () => { + const paginationBtn = await testSubjects.find('tablePaginationPopoverButton'); + await paginationBtn.click(); + const pageSizeOption = await testSubjects.find('tablePagination-50-rows'); + await pageSizeOption.click(); + }); + + type SortDirection = 'asc' | 'desc'; + const paginationAndsortingTestCases: Array<{ + searchQuery: string; + paginationRowsCount: string; + columnName: string; + sortType: SortDirection; + expectedResult: string; + }> = [ + { + searchQuery: + 'cloud.provider : "aws" and resource.sub_type : "aws-iam-user" and result.evaluation : "passed"', + paginationRowsCount: '250', + columnName: 'rule.benchmark.rule_number', + sortType: 'desc', + expectedResult: '1.7', + }, + { + searchQuery: 'cloud.provider : "azure" and result.evaluation : "failed"', + paginationRowsCount: '500', + columnName: 'rule.benchmark.rule_number', + sortType: 'asc', + expectedResult: '1.23', + }, + { + searchQuery: 'cloud.provider : "gcp" and result.evaluation : "passed"', + paginationRowsCount: '500', + columnName: 'resource.sub_type', + sortType: 'desc', + expectedResult: 'gcp-storage-bucket', + }, + ]; + + paginationAndsortingTestCases.forEach( + ({ searchQuery, paginationRowsCount, columnName, sortType, expectedResult }) => { + it(`Paginating and sorting data`, async () => { + // Run query + await queryBar.clearQuery(); + await queryBar.setQuery(searchQuery); + await queryBar.submitQuery(); + // Update latest findings table pagination + const paginationBtn = await testSubjects.find('tablePaginationPopoverButton'); + await paginationBtn.click(); + const pageSizeOption = await testSubjects.find( + `tablePagination-${paginationRowsCount}-rows` + ); + await pageSizeOption.click(); + // Sort by column + await latestFindingsTable.toggleColumnSort(columnName, sortType); + await pageObjects.header.waitUntilLoadingHasFinished(); + const values = (await latestFindingsTable.getColumnValues(columnName)).filter(Boolean); + // Check that the first value matches the expected result + // Whole sorting logic functionality is checked in the findings.ts + expect(values[0]).to.equal(expectedResult); + }); + } + ); + }); + }); +}; diff --git a/x-pack/test/cloud_security_posture_functional/cloud_tests/index.ts b/x-pack/test/cloud_security_posture_functional/cloud_tests/index.ts index b08970ccaed13..80afb04563326 100644 --- a/x-pack/test/cloud_security_posture_functional/cloud_tests/index.ts +++ b/x-pack/test/cloud_security_posture_functional/cloud_tests/index.ts @@ -10,6 +10,8 @@ import { FtrProviderContext } from '../ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function ({ loadTestFile }: FtrProviderContext) { describe('Cloud Security Posture', function () { - loadTestFile(require.resolve('./basic_ui_sanity')); + loadTestFile(require.resolve('./dashboard_sanity')); + loadTestFile(require.resolve('./benchmark_sanity')); + loadTestFile(require.resolve('./findings_sanity')); }); } diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/benchmark_page.ts b/x-pack/test/cloud_security_posture_functional/page_objects/benchmark_page.ts index 39856fa34d3fb..25ae1181f38b5 100644 --- a/x-pack/test/cloud_security_posture_functional/page_objects/benchmark_page.ts +++ b/x-pack/test/cloud_security_posture_functional/page_objects/benchmark_page.ts @@ -40,6 +40,28 @@ export function BenchmarkPagePageProvider({ getService, getPageObjects }: FtrPro doesBenchmarkTableExists: async () => { return await testSubjects.find('csp_benchmarks_table'); }, + + getBenchmarkTableRows: async () => { + const benchmarkTable = await testSubjects.find(CSP_BECNHMARK_TABLE); + return await benchmarkTable.findAllByXpath(`//tbody//tr`); + }, + + getCellData: async (row: any, cellDataTestSubj: string) => { + const cell = await row.findByTestSubject(cellDataTestSubj); + return await cell.getVisibleText(); + }, + + getEvaluatedCellData: async (row: any) => { + return await benchmarkPage.getCellData(row, 'benchmark-table-column-evaluated'); + }, + + getComplianceCellData: async (row: any) => { + return await benchmarkPage.getCellData(row, 'benchmark-table-column-compliance'); + }, + + getCisNameCellData: async (row: any) => { + return await benchmarkPage.getCellData(row, 'benchmark-table-column-cis-name'); + }, }; const navigateToBenchnmarkPage = async () => { diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/csp_dashboard_page.ts b/x-pack/test/cloud_security_posture_functional/page_objects/csp_dashboard_page.ts index 4343662e32efd..6f40f7b07003d 100644 --- a/x-pack/test/cloud_security_posture_functional/page_objects/csp_dashboard_page.ts +++ b/x-pack/test/cloud_security_posture_functional/page_objects/csp_dashboard_page.ts @@ -52,6 +52,11 @@ export function CspDashboardPageProvider({ getService, getPageObjects }: FtrProv }, }; + const TAB_TYPES = { + CLOUD: 'Cloud', + KUBERNETES: 'Kubernetes', + } as const; + const dashboard = { getDashboardPageHeader: () => testSubjects.find('cloud-posture-dashboard-page-header'), @@ -63,31 +68,65 @@ export function CspDashboardPageProvider({ getService, getPageObjects }: FtrProv getCloudTab: async () => { const tabs = await dashboard.getDashboardTabs(); - return await tabs.findByXpath(`//span[text()="Cloud"]`); + return await tabs.findByXpath(`//span[text()="${TAB_TYPES.CLOUD}"]`); }, getKubernetesTab: async () => { const tabs = await dashboard.getDashboardTabs(); - return await tabs.findByXpath(`//span[text()="Kubernetes"]`); + return await tabs.findByXpath(`//span[text()="${TAB_TYPES.KUBERNETES}"]`); }, - clickTab: async (tab: 'Cloud' | 'Kubernetes') => { - if (tab === 'Cloud') { + clickTab: async (tab: typeof TAB_TYPES[keyof typeof TAB_TYPES]) => { + if (tab === TAB_TYPES.CLOUD) { const cloudTab = await dashboard.getCloudTab(); await cloudTab.click(); } - if (tab === 'Kubernetes') { + if (tab === TAB_TYPES.KUBERNETES) { const k8sTab = await dashboard.getKubernetesTab(); await k8sTab.click(); } }, + getAllComplianceScoresByCisSection: async (tab: typeof TAB_TYPES[keyof typeof TAB_TYPES]) => { + await dashboard.getDashoard(tab); + const pageContainer = await testSubjects.find('pageContainer'); + return await pageContainer.findAllByTestSubject('cloudSecurityFindingsComplianceScore'); + }, + + getDashoard: async (tab: typeof TAB_TYPES[keyof typeof TAB_TYPES]) => { + if (tab === TAB_TYPES.CLOUD) { + return await dashboard.getCloudDashboard(); + } + if (tab === TAB_TYPES.KUBERNETES) { + return await dashboard.getKubernetesDashboard(); + } + }, + + getFindingsLinks: async (tab: typeof TAB_TYPES[keyof typeof TAB_TYPES]) => { + await dashboard.getDashoard(tab); + const pageContainer = await testSubjects.find('pageContainer'); + return await pageContainer.findAllByXpath(`//button[contains(@class, 'euiLink')]`); + }, + + getFindingsLinkAtIndex: async ( + tab: typeof TAB_TYPES[keyof typeof TAB_TYPES], + linkIndex = 0 + ) => { + const allLinks = await dashboard.getFindingsLinks(tab); + return await allLinks[linkIndex]; + }, + + getFindingsLinksCount: async (tab: typeof TAB_TYPES[keyof typeof TAB_TYPES]) => { + const allLinks = await dashboard.getFindingsLinks(tab); + return await allLinks.length; + }, + getIntegrationDashboardContainer: () => testSubjects.find('dashboard-container'), // Cloud Dashboard getCloudDashboard: async () => { - await dashboard.clickTab('Cloud'); + await dashboard.clickTab(TAB_TYPES.CLOUD); return await testSubjects.find('cloud-dashboard-container'); }, @@ -119,7 +158,7 @@ export function CspDashboardPageProvider({ getService, getPageObjects }: FtrProv // Kubernetes Dashboard getKubernetesDashboard: async () => { - await dashboard.clickTab('Kubernetes'); + await dashboard.clickTab(TAB_TYPES.KUBERNETES); return await testSubjects.find('kubernetes-dashboard-container'); }, @@ -161,5 +200,6 @@ export function CspDashboardPageProvider({ getService, getPageObjects }: FtrProv navigateToComplianceDashboardPage, dashboard, index, + TAB_TYPES, }; } diff --git a/x-pack/test/fleet_cypress/artifact_manager.ts b/x-pack/test/fleet_cypress/artifact_manager.ts index 0fe6609f28efc..23efabe2d976a 100644 --- a/x-pack/test/fleet_cypress/artifact_manager.ts +++ b/x-pack/test/fleet_cypress/artifact_manager.ts @@ -15,6 +15,10 @@ export async function getLatestVersion(): Promise { return pRetry(() => axios('https://artifacts-api.elastic.co/v1/versions'), { maxRetryTime: 60 * 1000, // 1 minute }) - .then((response) => last(response.data.versions as string[]) || DEFAULT_VERSION) + .then( + (response) => + last((response.data.versions as string[]).filter((v) => v.includes('-SNAPSHOT'))) || + DEFAULT_VERSION + ) .catch(() => DEFAULT_VERSION); } diff --git a/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts index 1974a48e77841..d6729e7bef923 100644 --- a/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts @@ -10,8 +10,9 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; import { USER } from '../../../services/ml/security_common'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - const PageObjects = getPageObjects(['common', 'error']); + const PageObjects = getPageObjects(['common', 'error', 'dashboard']); const ml = getService('ml'); + const esArchiver = getService('esArchiver'); const testUsers = [{ user: USER.ML_UNAUTHORIZED, discoverAvailable: true }]; @@ -56,5 +57,28 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); } + + describe('for user with no ML access and Kibana features access', function () { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await ml.testResources.createDataViewIfNeeded('ft_farequote', '@timestamp'); + await ml.securityUI.loginAs(USER.ML_DISABLED); + await ml.api.cleanMlIndices(); + }); + + after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await ml.securityUI.logout(); + }); + + it('should not register ML embeddables in the dashboard', async () => { + await ml.testExecution.logTestStep( + 'should not contain ML embeddable in the Add panel list' + ); + await PageObjects.dashboard.navigateToApp(); + await PageObjects.dashboard.clickCreateDashboardPrompt(); + await ml.dashboardEmbeddables.assertMlSectionExists(false); + }); + }); }); } diff --git a/x-pack/test/functional/services/ml/dashboard_embeddables.ts b/x-pack/test/functional/services/ml/dashboard_embeddables.ts index b22622ead61d0..3d88b3b9fd9c8 100644 --- a/x-pack/test/functional/services/ml/dashboard_embeddables.ts +++ b/x-pack/test/functional/services/ml/dashboard_embeddables.ts @@ -114,6 +114,13 @@ export function MachineLearningDashboardEmbeddablesProvider( }); }, + async assertMlSectionExists(expectExist = true) { + await retry.tryForTime(60 * 1000, async () => { + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.verifyEmbeddableFactoryGroupExists('ml', expectExist); + }); + }, + async openAnomalyJobSelectionFlyout( mlEmbeddableType: 'ml_anomaly_swimlane' | 'ml_anomaly_charts' | 'ml_single_metric_viewer' ) { diff --git a/x-pack/test/functional/services/ml/security_common.ts b/x-pack/test/functional/services/ml/security_common.ts index 6952183e7bdef..6d9aee298beaa 100644 --- a/x-pack/test/functional/services/ml/security_common.ts +++ b/x-pack/test/functional/services/ml/security_common.ts @@ -21,6 +21,7 @@ export enum USER { ML_VIEWER_SPACE1 = 'ft_ml_viewer_space1', ML_VIEWER_ALL_SPACES = 'ft_ml_viewer_all_spaces', ML_UNAUTHORIZED = 'ft_ml_unauthorized', + ML_DISABLED = 'ft_ml_disabled', } export function MachineLearningSecurityCommonProvider({ getService }: FtrProviderContext) { @@ -133,6 +134,29 @@ export function MachineLearningSecurityCommonProvider({ getService }: FtrProvide elasticsearch: { cluster: [], indices: [], run_as: [] }, kibana: [{ base: [], feature: { discover: ['read'] }, spaces: ['default'] }], }, + { + name: 'ft_ml_disabled', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [ + { + base: [], + feature: { + // FIXME: We need permission to save search in Discover to test the data viz embeddable + // change permission back to read once tests are moved out of ML + discover: ['all'], + visualize: ['add'], + dashboard: ['all'], + actions: ['all'], + savedObjectsManagement: ['all'], + advancedSettings: ['all'], + indexPatterns: ['all'], + generalCases: ['all'], + ml: ['none'], + }, + spaces: ['*'], + }, + ], + }, { name: 'ft_all_space_ml_none', elasticsearch: { cluster: [], indices: [], run_as: [] }, @@ -230,6 +254,12 @@ export function MachineLearningSecurityCommonProvider({ getService }: FtrProvide password: 'mlu001', roles: ['ft_default_space_ml_none', 'ft_ml_source_readonly'], }, + { + name: 'ft_ml_disabled', + full_name: 'ML Disabled', + password: 'mlud001', + roles: ['ft_ml_disabled'], + }, ]; return { diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/exceptions/workflows/basic_license_essentials_tier/create_rule_exceptions_ess.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/exceptions/workflows/basic_license_essentials_tier/create_rule_exceptions_ess.ts index 9406964106170..a1708cf4d6ae0 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/exceptions/workflows/basic_license_essentials_tier/create_rule_exceptions_ess.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/exceptions/workflows/basic_license_essentials_tier/create_rule_exceptions_ess.ts @@ -14,7 +14,6 @@ import { getRuleSOById, createRuleThroughAlertingEndpoint, getRuleSavedObjectWithLegacyInvestigationFields, - checkInvestigationFieldSoValue, } from '../../../../utils'; import { createAlertsIndex, @@ -79,25 +78,15 @@ export default ({ getService }: FtrProviderContext) => { }) .expect(200); - /** - * Confirm type on SO so that it's clear in the tests whether it's expected that - * the SO itself is migrated to the inteded object type, or if the transformation is - * happening just on the response. In this case, change will - * NOT include a migration on SO. - */ const { hits: { hits: [{ _source: ruleSO }], }, } = await getRuleSOById(es, ruleWithLegacyInvestigationField.id); - const isInvestigationFieldMigratedInSo = await checkInvestigationFieldSoValue(ruleSO, { - field_names: ['client.address', 'agent.name'], - }); expect( ruleSO?.alert.params.exceptionsList.some((list) => list.type === 'rule_default') ).to.eql(true); - expect(isInvestigationFieldMigratedInSo).to.eql(false); }); }); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/trial_license_complete_tier/patch_rules_bulk.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/trial_license_complete_tier/patch_rules_bulk.ts index 7e496ea73194d..947b191469e3d 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/trial_license_complete_tier/patch_rules_bulk.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/trial_license_complete_tier/patch_rules_bulk.ts @@ -532,7 +532,7 @@ export default ({ getService }: FtrProviderContext) => { ); }); - it('should patch a rule with a legacy investigation field and transform field in response', async () => { + it('should patch a rule with a legacy investigation field and migrate field', async () => { // patch a simple rule's name const { body } = await securitySolutionApi .bulkPatchRules({ @@ -548,19 +548,13 @@ export default ({ getService }: FtrProviderContext) => { }); expect(bodyToCompareLegacyField.name).to.eql('some other name'); - /** - * Confirm type on SO so that it's clear in the tests whether it's expected that - * the SO itself is migrated to the inteded object type, or if the transformation is - * happening just on the response. In this case, change should - * NOT include a migration on SO. - */ const isInvestigationFieldMigratedInSo = await checkInvestigationFieldSoValue( undefined, { field_names: ['client.address', 'agent.name'] }, es, body[0].id ); - expect(isInvestigationFieldMigratedInSo).to.eql(false); + expect(isInvestigationFieldMigratedInSo).to.eql(true); }); it('should patch a rule with a legacy investigation field - empty array - and transform field in response', async () => { diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/trial_license_complete_tier/patch_rules_ess.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/trial_license_complete_tier/patch_rules_ess.ts index 30398cd2cd1e9..d28358519e307 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/trial_license_complete_tier/patch_rules_ess.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/trial_license_complete_tier/patch_rules_ess.ts @@ -137,7 +137,7 @@ export default ({ getService }: FtrProviderContext) => { ); }); - it('should patch a rule with a legacy investigation field and transform response', async () => { + it('should patch a rule with a legacy investigation field and migrate field', async () => { const { body } = await supertest .patch(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') @@ -152,12 +152,7 @@ export default ({ getService }: FtrProviderContext) => { expect(bodyToCompare.investigation_fields).to.eql({ field_names: ['client.address', 'agent.name'], }); - /** - * Confirm type on SO so that it's clear in the tests whether it's expected that - * the SO itself is migrated to the inteded object type, or if the transformation is - * happening just on the response. In this case, change should - * NOT include a migration on SO. - */ + const isInvestigationFieldMigratedInSo = await checkInvestigationFieldSoValue( undefined, { @@ -166,7 +161,7 @@ export default ({ getService }: FtrProviderContext) => { es, body.id ); - expect(isInvestigationFieldMigratedInSo).to.eql(false); + expect(isInvestigationFieldMigratedInSo).to.eql(true); }); it('should patch a rule with a legacy investigation field - empty array - and transform response', async () => { diff --git a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/endpoint_authz.ts b/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/endpoint_authz.ts index 3f69edf091707..a9aa7af829225 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/endpoint_authz.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/endpoint_authz.ts @@ -36,7 +36,7 @@ export default function ({ getService }: FtrProviderContext) { method: keyof Pick; path: string; version?: string; - body: Record | undefined; + body: Record | (() => Record) | undefined; } describe('@ess @serverless When attempting to call an endpoint api', function () { @@ -92,13 +92,13 @@ export default function ({ getService }: FtrProviderContext) { { method: 'post', path: ISOLATE_HOST_ROUTE_V2, - body: { endpoint_ids: ['one'] }, + body: () => ({ endpoint_ids: [agentId] }), version: '2023-10-31', }, { method: 'post', path: UNISOLATE_HOST_ROUTE_V2, - body: { endpoint_ids: ['one'] }, + body: () => ({ endpoint_ids: [agentId] }), version: '2023-10-31', }, ]; @@ -107,19 +107,19 @@ export default function ({ getService }: FtrProviderContext) { { method: 'post', path: GET_PROCESSES_ROUTE, - body: { endpoint_ids: ['one'] }, + body: () => ({ endpoint_ids: [agentId] }), version: '2023-10-31', }, { method: 'post', path: KILL_PROCESS_ROUTE, - body: { endpoint_ids: ['one'], parameters: { entity_id: 'abc123' } }, + body: () => ({ endpoint_ids: [agentId], parameters: { entity_id: 'abc123' } }), version: '2023-10-31', }, { method: 'post', path: SUSPEND_PROCESS_ROUTE, - body: { endpoint_ids: ['one'], parameters: { entity_id: 'abc123' } }, + body: () => ({ endpoint_ids: [agentId], parameters: { entity_id: 'abc123' } }), version: '2023-10-31', }, ]; @@ -128,7 +128,7 @@ export default function ({ getService }: FtrProviderContext) { { method: 'post', path: GET_FILE_ROUTE, - body: { endpoint_ids: ['one'], parameters: { path: '/opt/file/doc.txt' } }, + body: () => ({ endpoint_ids: [agentId], parameters: { path: '/opt/file/doc.txt' } }), version: '2023-10-31', }, ]; @@ -138,7 +138,7 @@ export default function ({ getService }: FtrProviderContext) { method: 'post', path: EXECUTE_ROUTE, version: '2023-10-31', - body: { endpoint_ids: ['one'], parameters: { command: 'ls -la' } }, + body: () => ({ endpoint_ids: [agentId], parameters: { command: 'ls -la' } }), }, ]; @@ -155,6 +155,10 @@ export default function ({ getService }: FtrProviderContext) { return path.replace('{action_id}', actionId).replace('{agentId}', agentId); } + function getBodyPayload(apiCall: ApiCallsInterface): ApiCallsInterface['body'] { + return typeof apiCall.body === 'function' ? apiCall.body() : apiCall.body; + } + before(async () => { indexedData = await endpointTestResources.loadEndpointData(); agentId = indexedData.hosts[0].agent.id; @@ -179,7 +183,7 @@ export default function ({ getService }: FtrProviderContext) { .auth(ROLE.t1_analyst, 'changeme') .set('kbn-xsrf', 'xxx') .set(apiListItem.version ? 'Elastic-Api-Version' : 'foo', '2023-10-31') - .send(apiListItem.body) + .send(getBodyPayload(apiListItem)) .expect(403, { statusCode: 403, error: 'Forbidden', @@ -196,7 +200,7 @@ export default function ({ getService }: FtrProviderContext) { await supertestWithoutAuth[apiListItem.method](replacePathIds(apiListItem.path)) .auth(ROLE.t1_analyst, 'changeme') .set('kbn-xsrf', 'xxx') - .send(apiListItem.body) + .send(getBodyPayload(apiListItem)) .expect(200); }); } @@ -214,7 +218,7 @@ export default function ({ getService }: FtrProviderContext) { await supertestWithoutAuth[apiListItem.method](replacePathIds(apiListItem.path)) .auth(ROLE.platform_engineer, 'changeme') .set('kbn-xsrf', 'xxx') - .send(apiListItem.body) + .send(getBodyPayload(apiListItem)) .expect(403, { statusCode: 403, error: 'Forbidden', @@ -234,7 +238,7 @@ export default function ({ getService }: FtrProviderContext) { await supertestWithoutAuth[apiListItem.method](replacePathIds(apiListItem.path)) .auth(ROLE.platform_engineer, 'changeme') .set('kbn-xsrf', 'xxx') - .send(apiListItem.body) + .send(getBodyPayload(apiListItem)) .expect(200); }); } @@ -248,7 +252,7 @@ export default function ({ getService }: FtrProviderContext) { await supertestWithoutAuth[apiListItem.method](replacePathIds(apiListItem.path)) .auth(ROLE.endpoint_operations_analyst, 'changeme') .set('kbn-xsrf', 'xxx') - .send(apiListItem.body) + .send(getBodyPayload(apiListItem)) .expect(403, { statusCode: 403, error: 'Forbidden', @@ -270,7 +274,7 @@ export default function ({ getService }: FtrProviderContext) { await supertestWithoutAuth[apiListItem.method](replacePathIds(apiListItem.path)) .auth(ROLE.endpoint_operations_analyst, 'changeme') .set('kbn-xsrf', 'xxx') - .send(apiListItem.body) + .send(getBodyPayload(apiListItem)) .expect(200); }); } @@ -291,7 +295,7 @@ export default function ({ getService }: FtrProviderContext) { }]`, async () => { await supertest[apiListItem.method](replacePathIds(apiListItem.path)) .set('kbn-xsrf', 'xxx') - .send(apiListItem.body) + .send(getBodyPayload(apiListItem)) .expect(200); }); } diff --git a/x-pack/test_serverless/api_integration/test_suites/common/index_management/datastreams.ts b/x-pack/test_serverless/api_integration/test_suites/common/index_management/datastreams.ts index b5f51258c8df4..5e0a5baedb280 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/index_management/datastreams.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/index_management/datastreams.ts @@ -23,7 +23,7 @@ export default function ({ getService }: FtrProviderContext) { describe('Data streams', function () { // see details: https://github.com/elastic/kibana/issues/187372 - this.tags(['skipSvlSec']); + this.tags(['failsOnMKI']); before(async () => { roleAuthc = await svlUserManager.createApiKeyForRole('admin'); internalReqHeader = svlCommonApi.getInternalRequestHeader(); diff --git a/x-pack/test_serverless/functional/test_suites/observability/dataset_quality/home.ts b/x-pack/test_serverless/functional/test_suites/observability/dataset_quality/home.ts index 7deeeff7e9bc1..a1c285146e5e5 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/dataset_quality/home.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/dataset_quality/home.ts @@ -49,6 +49,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('dataset quality table exists', async () => { await PageObjects.datasetQuality.navigateTo(); + await PageObjects.datasetQuality.waitUntilTableLoaded(); await testSubjects.existOrFail( PageObjects.datasetQuality.testSubjectSelectors.datasetQualityTable ); diff --git a/yarn.lock b/yarn.lock index 67e950497963b..cad73b99f3c3e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4853,6 +4853,10 @@ version "0.0.0" uid "" +"@kbn/esql@link:src/plugins/esql": + version "0.0.0" + uid "" + "@kbn/event-annotation-common@link:packages/kbn-event-annotation-common": version "0.0.0" uid "" @@ -5833,6 +5837,10 @@ version "0.0.0" uid "" +"@kbn/recently-accessed@link:packages/kbn-recently-accessed": + version "0.0.0" + uid "" + "@kbn/remote-clusters-plugin@link:x-pack/plugins/remote_clusters": version "0.0.0" uid "" @@ -6661,10 +6669,6 @@ version "0.0.0" uid "" -"@kbn/text-based-languages@link:src/plugins/text_based_languages": - version "0.0.0" - uid "" - "@kbn/third-party-lens-navigation-prompt-plugin@link:x-pack/examples/third_party_lens_navigation_prompt": version "0.0.0" uid ""